diff --git a/models/AuthenticationEvent.js b/models/AuthenticationEvent.js
new file mode 100644
index 000000000..d81f15ece
--- /dev/null
+++ b/models/AuthenticationEvent.js
@@ -0,0 +1,43 @@
+const mongoose = require('mongoose');
+
+// Log authentication verification events
+const authEventSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true
+ },
+ sessionId: String,
+ keystrokeMetrics: {
+ avgKeystrokeTime: Number,
+ stdDevKeystrokeTime: Number,
+ avgPauseTime: Number,
+ typingSpeed: Number,
+ sampleSize: Number
+ },
+ deviceInfo: {
+ deviceId: String,
+ osType: String,
+ timezone: String
+ },
+ verificationResult: {
+ status: {
+ type: String,
+ enum: ['success', 'failed', 'challenge'],
+ default: 'challenge'
+ },
+ confidenceScore: Number,
+ modelPrediction: Number,
+ matchPercentage: Number,
+ flags: [String]
+ },
+ fallbackMethod: {
+ type: String,
+ enum: ['none', 'otp', 'email_link', 'security_question'],
+ default: 'none'
+ },
+ fallbackStatus: String,
+ timestamp: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('AuthenticationEvent', authEventSchema);
diff --git a/models/BiometricProfile.js b/models/BiometricProfile.js
new file mode 100644
index 000000000..0f980447e
--- /dev/null
+++ b/models/BiometricProfile.js
@@ -0,0 +1,38 @@
+const mongoose = require('mongoose');
+
+// Track keystroke profiles for behavioral authentication
+const biometricProfileSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ required: true
+ },
+ baselineMetrics: {
+ avgKeystrokeTime: Number,
+ stdDevKeystrokeTime: Number,
+ avgPauseTime: Number,
+ typingSpeed: Number,
+ deletionRate: Number,
+ rhythmPattern: [Number]
+ },
+ deviceSignature: {
+ keyboardType: String,
+ osType: String,
+ browserAgent: String,
+ screenResolution: String,
+ timezone: String
+ },
+ status: {
+ type: String,
+ enum: ['collecting_baseline', 'active', 'suspended'],
+ default: 'collecting_baseline'
+ },
+ baselineSessionsCount: { type: Number, default: 0 },
+ baselineRequired: { type: Number, default: 5 },
+ failureCount: { type: Number, default: 0 },
+ lastVerificationFailed: Date,
+ createdAt: { type: Date, default: Date.now },
+ lastUpdated: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('BiometricProfile', biometricProfileSchema);
diff --git a/models/DeviceProfile.js b/models/DeviceProfile.js
new file mode 100644
index 000000000..f369fe19e
--- /dev/null
+++ b/models/DeviceProfile.js
@@ -0,0 +1,32 @@
+const mongoose = require('mongoose');
+
+// Track device profiles and typing behavior per device
+const deviceProfileSchema = new mongoose.Schema({
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ deviceId: String,
+
+ deviceInfo: {
+ osType: String,
+ browserAgent: String,
+ screenResolution: String,
+ timezone: String,
+ inputMethod: String
+ },
+
+ behaviorProfile: {
+ avgKeystrokeTime: Number,
+ typingSpeed: Number,
+ errorRate: Number,
+ pasteFrequency: Number
+ },
+
+ trustScore: { type: Number, default: 100 },
+ isKnownDevice: { type: Boolean, default: false },
+
+ firstSeen: Date,
+ lastSeen: Date,
+
+ createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('DeviceProfile', deviceProfileSchema);
diff --git a/models/EncryptedKeystrokeData.js b/models/EncryptedKeystrokeData.js
new file mode 100644
index 000000000..0ab1fcbc2
--- /dev/null
+++ b/models/EncryptedKeystrokeData.js
@@ -0,0 +1,27 @@
+const mongoose = require('mongoose');
+
+// Store encrypted keystroke data for privacy
+const encryptedKeystrokeSchema = new mongoose.Schema({
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ sessionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Text' },
+
+ encryptedData: {
+ iv: String,
+ encryptedData: String,
+ authTag: String
+ },
+
+ anonymizedMetrics: {
+ avgKeystrokeTime: Number,
+ stdDevKeystrokeTime: Number,
+ pauseFrequency: Number
+ },
+
+ encryptionTimestamp: Date,
+ dataEncryptionEnabled: { type: Boolean, default: true },
+ userConsent: { type: Boolean, default: true },
+
+ createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('EncryptedKeystrokeData', encryptedKeystrokeSchema);
diff --git a/models/PasteDetectionLog.js b/models/PasteDetectionLog.js
new file mode 100644
index 000000000..5b21f58ab
--- /dev/null
+++ b/models/PasteDetectionLog.js
@@ -0,0 +1,55 @@
+const mongoose = require('mongoose');
+
+// Track paste and AI-generated content detection
+const pasteDetectionLogSchema = new mongoose.Schema({
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ textSessionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Text' },
+
+ detectionResults: [{
+ segmentId: String,
+ startPosition: Number,
+ endPosition: Number,
+ segmentText: String,
+ keystrokeAnalysis: {
+ hasKeystrokeIntervals: Boolean,
+ isInstantAppearance: Boolean,
+ uniformityScore: Number,
+ variationCv: Number,
+ expectedIntervals: Number,
+ actualIntervals: Number
+ },
+ linguisticMarkers: {
+ vocabularyShift: Number,
+ sentenceComplexity: Number,
+ punctuationConsistency: Number,
+ grammarAnomalies: [String]
+ },
+ aiConfidenceScore: Number,
+ pasteConfidenceScore: Number,
+ verdict: {
+ type: String,
+ enum: ['human_typed', 'pasted', 'ai_generated', 'uncertain'],
+ default: 'uncertain'
+ },
+ flags: [String]
+ }],
+
+ summaryAnalysis: {
+ totalSegments: Number,
+ pastedSegments: Number,
+ significantPastedSegments: Number,
+ trivialPastedSegments: Number,
+ ignoredPastedSegments: Number,
+ aiSegments: Number,
+ mixedContentDetected: Boolean,
+ suspicionLevel: {
+ type: String,
+ enum: ['low', 'medium', 'high'],
+ default: 'low'
+ }
+ },
+
+ timestamp: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('PasteDetectionLog', pasteDetectionLogSchema);
diff --git a/models/Report.js b/models/Report.js
new file mode 100644
index 000000000..7a5d8d517
--- /dev/null
+++ b/models/Report.js
@@ -0,0 +1,45 @@
+const mongoose = require('mongoose');
+const crypto = require('crypto');
+
+// Generate and manage shareable verification reports
+const reportSchema = new mongoose.Schema({
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ textSessionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Text' },
+
+ reportContent: {
+ title: String,
+ summary: String,
+ trustScore: Number,
+ analysisDate: Date,
+ findings: [{
+ category: String,
+ result: String,
+ confidence: Number
+ }]
+ },
+
+ sharingToken: { type: String, unique: true },
+ isPublic: { type: Boolean, default: false },
+ expiresAt: Date,
+ accessControl: {
+ allowVerification: { type: Boolean, default: true },
+ restrictedViewers: [String]
+ },
+
+ viewCount: { type: Number, default: 0 },
+ viewLog: [{
+ timestamp: Date,
+ viewerIP: String
+ }],
+
+ createdAt: { type: Date, default: Date.now }
+});
+
+// Auto-generate secure token before saving
+reportSchema.pre('save', function() {
+ if (!this.sharingToken) {
+ this.sharingToken = crypto.randomBytes(32).toString('hex');
+ }
+});
+
+module.exports = mongoose.model('Report', reportSchema);
diff --git a/models/SessionTrustScore.js b/models/SessionTrustScore.js
new file mode 100644
index 000000000..5d834ebb3
--- /dev/null
+++ b/models/SessionTrustScore.js
@@ -0,0 +1,32 @@
+const mongoose = require('mongoose');
+
+// Real-time session trust score tracking
+const sessionTrustScoreSchema = new mongoose.Schema({
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ textSessionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Text' },
+
+ scoreHistory: [{
+ timestamp: Date,
+ score: Number,
+ factors: {
+ keystrokeBehavior: Number,
+ pasteDetection: Number,
+ aiLikelihood: Number,
+ anomalyScore: Number,
+ devicesConsistency: Number
+ }
+ }],
+
+ currentScore: Number,
+ riskLevel: { type: String, enum: ['low', 'medium', 'high'] },
+
+ warnings: [{
+ timestamp: Date,
+ message: String,
+ severity: String
+ }],
+
+ createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('SessionTrustScore', sessionTrustScoreSchema);
diff --git a/models/Text.js b/models/Text.js
new file mode 100644
index 000000000..b8dd18fef
--- /dev/null
+++ b/models/Text.js
@@ -0,0 +1,71 @@
+const mongoose = require('mongoose');
+
+const textSchema = new mongoose.Schema({
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User'
+ },
+ content: String,
+ startTime: Date,
+ endTime: Date,
+ duration: Number,
+ pasteCount: Number,
+ pastedTextLength: Number,
+ totalKeystrokes: Number,
+ sessionMetrics: {
+ avgKeystrokeTime: Number,
+ stdDevKeystrokeTime: Number,
+ avgPauseTime: Number,
+ longestPause: Number,
+ pauseCount: Number,
+ deletionCount: Number,
+ focusLossCount: Number,
+ revisionCount: Number,
+ typingSpeed: Number,
+ errorRate: Number,
+ pasteRatio: Number,
+ sampleSize: Number,
+ rhythmPattern: [Number]
+ },
+ linguisticMetrics: {
+ wordCount: Number,
+ sentenceCount: Number,
+ paragraphCount: Number,
+ vocabularyDiversity: Number,
+ averageSentenceLength: Number
+ },
+ deviceSnapshot: {
+ deviceId: String,
+ osType: String,
+ browserAgent: String,
+ screenResolution: String,
+ timezone: String,
+ inputMethod: String
+ },
+ analysisSummary: {
+ pasteVerdict: String,
+ aiVerdict: String,
+ trustScore: Number,
+ riskLevel: String,
+ biometricStatus: String,
+ deviceTrustScore: Number,
+ reportToken: String
+ },
+ artifactRefs: {
+ authenticationEventId: String,
+ deviceProfileId: String,
+ biometricProfileId: String,
+ pasteLogId: String,
+ writingVersionId: String,
+ encryptedTelemetryId: String,
+ trustScoreId: String,
+ reportId: String
+ },
+ createdAt: {
+ type: Date,
+ default: Date.now
+ }
+
+});
+
+module.exports = mongoose.model('Text', textSchema);
diff --git a/models/User.js b/models/User.js
new file mode 100644
index 000000000..a53b7a64b
--- /dev/null
+++ b/models/User.js
@@ -0,0 +1,9 @@
+const mongoose = require('mongoose');
+
+const userSchema = new mongoose.Schema({
+ username: String,
+ email: String,
+ password: String
+});
+
+module.exports = mongoose.model('User', userSchema);
\ No newline at end of file
diff --git a/models/WritingVersion.js b/models/WritingVersion.js
new file mode 100644
index 000000000..981cb601b
--- /dev/null
+++ b/models/WritingVersion.js
@@ -0,0 +1,32 @@
+const mongoose = require('mongoose');
+
+// Track writing versions and keystroke replay data
+const writingVersionSchema = new mongoose.Schema({
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
+ textSessionId: { type: mongoose.Schema.Types.ObjectId, ref: 'Text' },
+
+ versions: [{
+ versionNumber: Number,
+ timestamp: Date,
+ content: String,
+ characterCount: Number,
+ changesSinceLast: {
+ inserted: Number,
+ deleted: Number,
+ modified: Number
+ }
+ }],
+
+ keystrokeReplay: [{
+ timestamp: Number,
+ type: { type: String, enum: ['keystroke', 'delete', 'paste'] },
+ character: String,
+ text: String,
+ position: Number,
+ interval: Number
+ }],
+
+ createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('WritingVersion', writingVersionSchema);
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..f1314f544
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1174 @@
+{
+ "name": "vi-notes",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vi-notes",
+ "version": "1.0.0",
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.3.1"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
+ "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/bcryptjs": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
+ "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
+ "license": "BSD-3-Clause",
+ "bin": {
+ "bcrypt": "bin/bcrypt"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz",
+ "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "license": "MIT"
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.0.tgz",
+ "integrity": "sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.1.1",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.3.1.tgz",
+ "integrity": "sha512-58DuQti+LlRS74/UfWN4F3wZsC0Yr1dgTWZ2Wd3/TuSvm6rIdyAjDWbx2xGyuBooqJYdAWotVv4mQgVdivh+3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "kareem": "3.2.0",
+ "mongodb": "~7.1",
+ "mpath": "0.9.0",
+ "mquery": "6.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
+ "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
+ "license": "MIT"
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..1ba5e9dda
--- /dev/null
+++ b/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "vi-notes",
+ "version": "1.0.0",
+ "private": true,
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node --watch server.js"
+ },
+ "dependencies": {
+ "bcryptjs": "^3.0.3",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.3.1"
+ }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 000000000..25645ea91
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,1052 @@
+
+
+
+ Vi-Notes Editor
+
+
+
+
+
+
Vi-Notes Writing Editor
+
+
+
+
+
+
+
Keystrokes0
+
Pastes0
+
Deletes0
+
Focus Interruptions0
+
+
+
+
+
+
+
+
+
+
+
Latest Analysis
+
Save a session to see trust score, paste detection, device trust, and biometric status.
+
+
+
+
Saved Sessions
+
Click View Sessions to load saved sessions.
+
+
+
+
Replay Viewer
+
+
+
+
+
+
+
+
+
Pick a saved session and click Replay.
+
The replay will show typed characters and paste bursts.
+
+
+
+
+
+
diff --git a/public/login.html b/public/login.html
new file mode 100644
index 000000000..d0ff5d6e5
--- /dev/null
+++ b/public/login.html
@@ -0,0 +1,147 @@
+
+
+
+ Login
+
+
+
+
+
+
+
+
+
+
diff --git a/public/register.html b/public/register.html
new file mode 100644
index 000000000..066ad43c6
--- /dev/null
+++ b/public/register.html
@@ -0,0 +1,147 @@
+
+
+
+ Register
+
+
+
+
+
+
+
+
+
+
diff --git a/public/report.html b/public/report.html
new file mode 100644
index 000000000..81c3dae6b
--- /dev/null
+++ b/public/report.html
@@ -0,0 +1,91 @@
+
+
+
+ Verification Report
+
+
+
+
+
+
+
+
diff --git a/routes/biometrics.js b/routes/biometrics.js
new file mode 100644
index 000000000..649ea6e31
--- /dev/null
+++ b/routes/biometrics.js
@@ -0,0 +1,281 @@
+const express = require('express');
+const router = express.Router();
+const BiometricProfile = require('../models/BiometricProfile');
+const AuthenticationEvent = require('../models/AuthenticationEvent');
+const { requireAuth } = require('../utils/auth');
+
+// 1. Initialize biometric profile
+router.post('/biometrics/init', async (req, res) => {
+ try {
+ const { userId } = req.body;
+
+ let profile = await BiometricProfile.findOne({ userId });
+ if (profile) {
+ return res.status(400).json({ message: 'Profile already exists' });
+ }
+
+ profile = new BiometricProfile({
+ userId,
+ status: 'collecting_baseline'
+ });
+
+ await profile.save();
+
+ res.json({
+ message: 'Biometric profile initialized',
+ profileId: profile._id,
+ mode: 'baseline_collection'
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// 2. Record baseline keystroke sample
+router.post('/biometrics/baseline-sample', async (req, res) => {
+ try {
+ const { userId, keystrokeMetrics, deviceInfo } = req.body;
+
+ const profile = await BiometricProfile.findOne({ userId });
+ if (!profile) {
+ return res.status(404).json({ message: 'Profile not found' });
+ }
+
+ if (profile.status !== 'collecting_baseline') {
+ return res.status(400).json({ message: 'Not in baseline collection mode' });
+ }
+
+ const metrics = {
+ avgKeystrokeTime: keystrokeMetrics.avgKeystrokeTime,
+ stdDevKeystrokeTime: keystrokeMetrics.stdDevKeystrokeTime,
+ avgPauseTime: keystrokeMetrics.avgPauseTime,
+ typingSpeed: keystrokeMetrics.typingSpeed,
+ deletionRate: keystrokeMetrics.deletionRate
+ };
+
+ const rhythmPattern = calculateRhythmPattern(keystrokeMetrics.intervalArray || []);
+
+ // Update baseline with exponential moving average
+ if (profile.baselineSessionsCount === 0) {
+ profile.baselineMetrics = { ...metrics, rhythmPattern };
+ } else {
+ const alpha = 0.3; // EMA smoothing factor
+ profile.baselineMetrics.avgKeystrokeTime =
+ (1 - alpha) * profile.baselineMetrics.avgKeystrokeTime +
+ alpha * metrics.avgKeystrokeTime;
+
+ profile.baselineMetrics.stdDevKeystrokeTime =
+ (1 - alpha) * profile.baselineMetrics.stdDevKeystrokeTime +
+ alpha * metrics.stdDevKeystrokeTime;
+
+ profile.baselineMetrics.typingSpeed =
+ (1 - alpha) * profile.baselineMetrics.typingSpeed +
+ alpha * metrics.typingSpeed;
+ }
+
+ profile.baselineSessionsCount += 1;
+
+ if (profile.baselineSessionsCount === 1) {
+ profile.deviceSignature = deviceInfo;
+ }
+
+ // Activate after sufficient baseline
+ if (profile.baselineSessionsCount >= profile.baselineRequired) {
+ profile.status = 'active';
+ }
+
+ profile.lastUpdated = new Date();
+ await profile.save();
+
+ res.json({
+ message: 'Baseline sample recorded',
+ sessionsCollected: profile.baselineSessionsCount,
+ sessionsRequired: profile.baselineRequired,
+ profileStatus: profile.status
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// 3. Verify biometric on login
+router.post('/biometrics/verify', async (req, res) => {
+ try {
+ const { userId, keystrokeMetrics, deviceInfo, sessionId } = req.body;
+
+ const profile = await BiometricProfile.findOne({ userId });
+
+ if (!profile || profile.status !== 'active') {
+ return res.json({
+ status: 'not_ready',
+ message: 'Biometric verification not yet active'
+ });
+ }
+
+ const isNewDevice = deviceInfo.deviceId !== profile.deviceSignature?.deviceId;
+ const verificationScore = calculateBiometricMatch(
+ keystrokeMetrics,
+ profile.baselineMetrics,
+ isNewDevice
+ );
+
+ const threshold = 75;
+ let verdict = 'success';
+ let flags = [];
+
+ if (verificationScore < threshold) {
+ verdict = isNewDevice ? 'challenge' : 'failed';
+ if (isNewDevice) flags.push('new_device');
+ if (Math.abs(keystrokeMetrics.typingSpeed - profile.baselineMetrics.typingSpeed) >
+ profile.baselineMetrics.stdDevKeystrokeTime * 2) {
+ flags.push('keystroke_anomaly');
+ }
+ }
+
+ const authEvent = new AuthenticationEvent({
+ userId,
+ sessionId,
+ keystrokeMetrics: {
+ avgKeystrokeTime: keystrokeMetrics.avgKeystrokeTime,
+ stdDevKeystrokeTime: keystrokeMetrics.stdDevKeystrokeTime,
+ avgPauseTime: keystrokeMetrics.avgPauseTime,
+ typingSpeed: keystrokeMetrics.typingSpeed,
+ sampleSize: keystrokeMetrics.sampleSize
+ },
+ deviceInfo,
+ verificationResult: {
+ status: verdict,
+ confidenceScore: verificationScore,
+ matchPercentage: verificationScore,
+ flags
+ }
+ });
+
+ if (verdict === 'success') {
+ profile.failureCount = 0;
+ } else {
+ profile.failureCount += 1;
+ profile.lastVerificationFailed = new Date();
+
+ if (profile.failureCount >= 3) {
+ profile.status = 'suspended';
+ }
+ }
+
+ profile.lastUpdated = new Date();
+ await profile.save();
+ await authEvent.save();
+
+ res.json({
+ status: verdict,
+ confidenceScore: verificationScore,
+ flags,
+ requiresFallback: verdict !== 'success',
+ fallbackMethods: verdict !== 'success' ? ['otp', 'email_link'] : []
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// 4. Get biometric profile status
+router.get('/biometrics/status', requireAuth, async (req, res) => {
+ try {
+ const profile = await BiometricProfile.findOne({ userId: req.userId });
+
+ if (!profile) {
+ return res.json({
+ message: 'No biometric profile',
+ initialized: false
+ });
+ }
+
+ res.json({
+ initialized: true,
+ status: profile.status,
+ baselineSessionsCollected: profile.baselineSessionsCount,
+ baselineSessionsRequired: profile.baselineRequired,
+ failureCount: profile.failureCount,
+ lastVerificationFailed: profile.lastVerificationFailed
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Helper: Calculate biometric match score
+function calculateBiometricMatch(current, baseline, isNewDevice) {
+ const weights = {
+ keystrokeTime: 0.35,
+ typingSpeed: 0.25,
+ pauseTime: 0.20,
+ variance: 0.15,
+ deviceBonus: 0.05
+ };
+
+ let score = 0;
+
+ // Keystroke timing comparison
+ const keystrokeScore = calculateSimilarityScore(
+ current.avgKeystrokeTime,
+ baseline.avgKeystrokeTime
+ );
+ score += keystrokeScore * weights.keystrokeTime;
+
+ // Typing speed comparison
+ const speedScore = calculateSimilarityScore(
+ current.typingSpeed,
+ baseline.typingSpeed
+ );
+ score += speedScore * weights.typingSpeed;
+
+ // Pause time comparison
+ const pauseScore = calculateSimilarityScore(
+ current.avgPauseTime,
+ baseline.avgPauseTime
+ );
+ score += pauseScore * weights.pauseTime;
+
+ // Variance comparison
+ const varScore = calculateSimilarityScore(
+ current.stdDevKeystrokeTime,
+ baseline.stdDevKeystrokeTime
+ );
+ score += varScore * weights.variance;
+
+ // Device consistency bonus
+ if (!isNewDevice) {
+ score += weights.deviceBonus * 100;
+ }
+
+ return Math.round(Math.min(100, Math.max(0, score)));
+}
+
+// Helper: Calculate rhythm pattern
+function calculateSimilarityScore(currentValue, baselineValue) {
+ if (!Number.isFinite(currentValue) || !Number.isFinite(baselineValue) || baselineValue <= 0) {
+ return 0;
+ }
+
+ const diff = Math.abs(currentValue - baselineValue);
+ return Math.max(0, 100 - (diff / baselineValue) * 100);
+}
+
+function calculateRhythmPattern(intervals) {
+ if (!intervals || intervals.length === 0) return [0, 0, 0, 0, 0];
+
+ const max = Math.max(...intervals);
+ if (max === 0) return [0, 0, 0, 0, 0];
+
+ const normalized = intervals.map(i => i / max);
+ const buckets = [0, 0, 0, 0, 0];
+
+ normalized.forEach(val => {
+ const bucketIdx = Math.min(4, Math.floor(val * 5));
+ buckets[bucketIdx]++;
+ });
+
+ return buckets;
+}
+
+module.exports = router;
diff --git a/routes/deviceTracking.js b/routes/deviceTracking.js
new file mode 100644
index 000000000..4d86b03c3
--- /dev/null
+++ b/routes/deviceTracking.js
@@ -0,0 +1,124 @@
+const express = require('express');
+const router = express.Router();
+const crypto = require('crypto');
+const DeviceProfile = require('../models/DeviceProfile');
+
+// Register device
+router.post('/device/register', async (req, res) => {
+ try {
+ const { userId, deviceInfo } = req.body;
+ const deviceId = generateDeviceFingerprint(deviceInfo);
+
+ let profile = await DeviceProfile.findOne({ userId, deviceId });
+
+ if (!profile) {
+ profile = new DeviceProfile({
+ userId,
+ deviceId,
+ deviceInfo,
+ firstSeen: new Date(),
+ isKnownDevice: false
+ });
+ }
+
+ profile.lastSeen = new Date();
+ await profile.save();
+
+ res.json({
+ deviceId,
+ isNewDevice: !profile.isKnownDevice,
+ trustScore: profile.trustScore
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Verify device consistency
+router.post('/device/verify-consistency', async (req, res) => {
+ try {
+ const { userId, currentDeviceInfo, behaviorMetrics } = req.body;
+ const userDevices = await DeviceProfile.find({ userId });
+
+ if (userDevices.length === 0) {
+ return res.json({ consistent: true, consistency: 100 });
+ }
+
+ const avgBehavior = calculateAverageBehavior(userDevices);
+ const consistency = calculateConsistency(behaviorMetrics, avgBehavior);
+
+ res.json({
+ consistency,
+ isConsistent: consistency > 70,
+ deviceCount: userDevices.length
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get all devices for user
+router.get('/device/list/:userId', async (req, res) => {
+ try {
+ const devices = await DeviceProfile.find({ userId: req.params.userId })
+ .select('deviceId deviceInfo trustScore firstSeen lastSeen')
+ .lean();
+
+ res.json({ devices });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Mark device as trusted
+router.post('/device/:deviceId/trust', async (req, res) => {
+ try {
+ const device = await DeviceProfile.findOneAndUpdate(
+ { deviceId: req.params.deviceId },
+ { isKnownDevice: true, trustScore: 100 },
+ { new: true }
+ );
+
+ res.json({ message: 'Device marked as trusted', device });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Helper: Generate device fingerprint
+function generateDeviceFingerprint(deviceInfo) {
+ const data = JSON.stringify({
+ osType: deviceInfo.osType,
+ browserAgent: deviceInfo.browserAgent,
+ screenResolution: deviceInfo.screenResolution,
+ timezone: deviceInfo.timezone
+ });
+
+ return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);
+}
+
+// Helper: Calculate average behavior across devices
+function calculateAverageBehavior(devices) {
+ const avgKeystrokeTime = devices.reduce((sum, d) =>
+ sum + (d.behaviorProfile?.avgKeystrokeTime || 100), 0
+ ) / devices.length;
+
+ const avgTypingSpeed = devices.reduce((sum, d) =>
+ sum + (d.behaviorProfile?.typingSpeed || 5), 0
+ ) / devices.length;
+
+ return { avgKeystrokeTime, avgTypingSpeed };
+}
+
+// Helper: Calculate consistency score
+function calculateConsistency(current, avg) {
+ const keystrokeDiff = Math.abs(current.avgKeystrokeTime - avg.avgKeystrokeTime);
+ const keystrokeScore = Math.max(0, 100 - (keystrokeDiff / avg.avgKeystrokeTime) * 100);
+
+ const speedDiff = Math.abs(current.typingSpeed - avg.avgTypingSpeed);
+ const speedScore = Math.max(0, 100 - (speedDiff / avg.avgTypingSpeed) * 100);
+
+ return Math.round((keystrokeScore + speedScore) / 2);
+}
+
+module.exports = router;
diff --git a/routes/pasteDetection.js b/routes/pasteDetection.js
new file mode 100644
index 000000000..f4d086728
--- /dev/null
+++ b/routes/pasteDetection.js
@@ -0,0 +1,283 @@
+const express = require('express');
+const router = express.Router();
+const PasteDetectionLog = require('../models/PasteDetectionLog');
+
+// Analyze text session for paste/AI injection
+router.post('/detect-paste-injection', async (req, res) => {
+ try {
+ const { keystrokeData, contentText, sessionId, userId } = req.body;
+
+ const segments = segmentContent(contentText);
+ const detectionResults = [];
+
+ for (const segment of segments) {
+ const analysis = analyzeSegment(
+ segment,
+ keystrokeData,
+ contentText
+ );
+ detectionResults.push(analysis);
+ }
+
+ const summary = calculateSummary(detectionResults);
+
+ const log = new PasteDetectionLog({
+ userId,
+ textSessionId: sessionId,
+ detectionResults,
+ summaryAnalysis: summary
+ });
+
+ await log.save();
+
+ res.json({
+ sessionId,
+ analysis: detectionResults,
+ summary,
+ overallVerdict: summary.suspicionLevel,
+ recommendations: generateRecommendations(summary)
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Real-time paste detection
+router.post('/detect-live-paste', async (req, res) => {
+ try {
+ const { recentText, keystrokeIntervals, previousContent } = req.body;
+
+ const isInstantAppearance = detectInstantAppearance(
+ recentText,
+ keystrokeIntervals
+ );
+
+ if (isInstantAppearance) {
+ return res.json({
+ pasteDetected: true,
+ confidence: 95,
+ message: "Text appears to be pasted",
+ flag: 'instant_appearance',
+ action: 'highlight_segment'
+ });
+ }
+
+ const uniformityScore = calculateUniformity(keystrokeIntervals);
+
+ if (uniformityScore > 85) {
+ return res.json({
+ aiLikelyDetected: true,
+ uniformityScore,
+ confidence: 70,
+ message: "Text shows highly uniform typing pattern (possible AI)",
+ flag: 'uniform_keystroke',
+ action: 'flag_for_review'
+ });
+ }
+
+ res.json({
+ pasteDetected: false,
+ aiLikelyDetected: false,
+ confidence: 95,
+ message: "Text appears human-typed"
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Helper: Detect instant appearance
+function detectInstantAppearance(newText, keystrokeIntervals) {
+ const textLength = newText.length;
+ const expectedIntervals = textLength - 1;
+ const missingIntervals = expectedIntervals - keystrokeIntervals.length;
+
+ return missingIntervals > expectedIntervals * 0.5;
+}
+
+// Helper: Calculate keystroke uniformity
+function calculateUniformity(intervals) {
+ if (intervals.length < 3) return 0;
+
+ const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
+ const variance = intervals.reduce((sum, val) =>
+ sum + Math.pow(val - mean, 2), 0) / intervals.length;
+ const stdDev = Math.sqrt(variance);
+
+ const cv = (stdDev / mean) * 100;
+ return Math.max(0, 100 - cv * 2);
+}
+
+// Helper: Segment content
+function segmentContent(text) {
+ return text.split(/[.!?]\s+/).map((seg, idx, arr) => {
+ if (idx < arr.length - 1) {
+ return seg + '.';
+ }
+ return seg;
+ }).filter(seg => seg.trim().length > 0);
+}
+
+// Helper: Analyze single segment
+function analyzeSegment(segment, keystrokeData, fullText) {
+ const startPos = fullText.indexOf(segment);
+ const endPos = startPos + segment.length;
+
+ const segmentKeystrokes = keystrokeData.intervals.filter(k =>
+ k.position >= startPos && k.position <= endPos
+ );
+
+ const isInstant = detectInstantAppearance(segment, segmentKeystrokes);
+ const uniformity = calculateUniformity(
+ segmentKeystrokes.map(k => k.interval)
+ );
+
+ const linguisticMarkers = analyzeLinguistics(segment);
+ const aiConfidence = calculateAIConfidence(
+ uniformity,
+ linguisticMarkers,
+ segmentKeystrokes.length
+ );
+
+ const pasteConfidence = isInstant ? 90 : 10;
+
+ let verdict = 'human_typed';
+ let flags = [];
+
+ if (isInstant) {
+ verdict = 'pasted';
+ flags.push('instant_appearance');
+ } else if (aiConfidence > 80) {
+ verdict = 'ai_generated';
+ flags.push('uniform_typing', 'linguistic_markers');
+ } else if (uniformity > 85) {
+ verdict = 'uncertain';
+ flags.push('high_uniformity');
+ }
+
+ if (linguisticMarkers.grammarAnomalies.length > 0) {
+ flags.push('grammar_anomaly');
+ }
+
+ return {
+ segmentId: `seg_${startPos}_${endPos}`,
+ startPosition: startPos,
+ endPosition: endPos,
+ segmentText: segment,
+ keystrokeAnalysis: {
+ hasKeystrokeIntervals: segmentKeystrokes.length > 0,
+ isInstantAppearance: isInstant,
+ uniformityScore: uniformity,
+ variationCv: segmentKeystrokes.length > 0 ?
+ (Math.sqrt(
+ segmentKeystrokes.reduce((sum, k) => sum + Math.pow(k.interval, 2), 0) /
+ segmentKeystrokes.length
+ ) / (segmentKeystrokes.reduce((a, b) => a + b.interval, 0) / segmentKeystrokes.length)) * 100
+ : 0,
+ expectedIntervals: segment.length - 1,
+ actualIntervals: segmentKeystrokes.length
+ },
+ linguisticMarkers,
+ aiConfidenceScore: aiConfidence,
+ pasteConfidenceScore: pasteConfidence,
+ verdict,
+ flags
+ };
+}
+
+// Helper: Analyze linguistic features
+function analyzeLinguistics(text) {
+ const sentences = text.split(/[.!?]/).filter(s => s.trim());
+ const words = text.toLowerCase().split(/\s+/);
+ const uniqueWords = new Set(words).size;
+ const vocabularyDiversity = (uniqueWords / words.length) * 100;
+
+ const avgCharsPerSentence = text.length / (sentences.length || 1);
+ const punctuationCount = (text.match(/[.!?,;:-]/g) || []).length;
+ const punctuationConsistency = (punctuationCount / words.length) * 100;
+
+ const aiMarkers = [
+ 'furthermore', 'moreover', 'in addition', 'consequently',
+ 'undoubtedly', 'certainly', 'undeniably', 'appears to be'
+ ];
+
+ const grammarAnomalies = [];
+ aiMarkers.forEach(marker => {
+ if (text.toLowerCase().includes(marker)) {
+ grammarAnomalies.push(`AI_marker: ${marker}`);
+ }
+ });
+
+ return {
+ vocabularyShift: 100 - vocabularyDiversity,
+ sentenceComplexity: Math.round(avgCharsPerSentence),
+ punctuationConsistency: Math.round(punctuationConsistency),
+ grammarAnomalies
+ };
+}
+
+// Helper: Calculate AI confidence
+function calculateAIConfidence(uniformity, linguistic, keystrokeCount) {
+ const weights = {
+ uniformity: 0.4,
+ linguistic: 0.3,
+ keystrokeCount: 0.3
+ };
+
+ let uniformityScore = uniformity;
+ const linguisticAnomalies = linguistic.grammarAnomalies.length;
+ const linguisticScore = (linguisticAnomalies / 5) * 100;
+ const keystrokeScore = keystrokeCount < 20 ? 50 : Math.max(0, 100 - keystrokeCount / 2);
+
+ const aiConfidence =
+ uniformityScore * weights.uniformity +
+ linguisticScore * weights.linguistic +
+ keystrokeScore * weights.keystrokeCount;
+
+ return Math.round(Math.min(100, aiConfidence));
+}
+
+// Helper: Calculate summary
+function calculateSummary(results) {
+ const pastedCount = results.filter(r => r.verdict === 'pasted').length;
+ const aiCount = results.filter(r => r.verdict === 'ai_generated').length;
+
+ const totalSegments = results.length;
+ const mixedContent = (pastedCount + aiCount) > 0 && totalSegments > (pastedCount + aiCount);
+
+ let suspicionLevel = 'low';
+ if (pastedCount + aiCount > totalSegments * 0.5) {
+ suspicionLevel = 'high';
+ } else if (pastedCount + aiCount > totalSegments * 0.2) {
+ suspicionLevel = 'medium';
+ }
+
+ return {
+ totalSegments,
+ pastedSegments: pastedCount,
+ aiSegments: aiCount,
+ mixedContentDetected: mixedContent,
+ suspicionLevel
+ };
+}
+
+// Helper: Generate recommendations
+function generateRecommendations(summary) {
+ const recs = [];
+
+ if (summary.suspicionLevel === 'high') {
+ recs.push('⚠️ High suspicion of non-human content');
+ recs.push('Recommend manual review');
+ recs.push('Consider requiring re-submission');
+ } else if (summary.suspicionLevel === 'medium') {
+ recs.push('⚠️ Some segments show questionable patterns');
+ recs.push('Review highlighted segments');
+ recs.push('May require clarification from author');
+ } else {
+ recs.push('✓ Content appears naturally authored');
+ }
+
+ return recs;
+}
+
+module.exports = router;
diff --git a/routes/privacy.js b/routes/privacy.js
new file mode 100644
index 000000000..f4921f435
--- /dev/null
+++ b/routes/privacy.js
@@ -0,0 +1,144 @@
+const express = require('express');
+const router = express.Router();
+const EncryptionManager = require('../utils/encryption');
+const EncryptedKeystrokeData = require('../models/EncryptedKeystrokeData');
+const { requireAuth } = require('../utils/auth');
+
+function hasValidAdminKey(candidateKey) {
+ return Boolean(process.env.ADMIN_KEY && candidateKey && candidateKey === process.env.ADMIN_KEY);
+}
+
+// Store encrypted keystroke data
+router.post('/privacy/encrypt-keystroke', async (req, res) => {
+ try {
+ const { userId, sessionId, keystrokeData } = req.body;
+
+ const encryptionManager = new EncryptionManager(process.env.ENCRYPTION_KEY);
+
+ const encrypted = encryptionManager.encrypt(keystrokeData);
+ const anonymized = EncryptionManager.anonymize(keystrokeData);
+
+ const encData = new EncryptedKeystrokeData({
+ userId,
+ sessionId,
+ encryptedData: encrypted,
+ anonymizedMetrics: {
+ avgKeystrokeTime: anonymized.avgKeystrokeTime || keystrokeData.avgKeystrokeTime,
+ stdDevKeystrokeTime: anonymized.stdDevKeystrokeTime || keystrokeData.stdDevKeystrokeTime,
+ pauseFrequency: (keystrokeData.pauseIntervals?.length || 0)
+ },
+ encryptionTimestamp: new Date()
+ });
+
+ await encData.save();
+
+ res.json({
+ message: 'Data encrypted and stored securely',
+ encryptedId: encData._id
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Retrieve decrypted keystroke data (admin only)
+router.get('/privacy/decrypt/:encryptedId', async (req, res) => {
+ try {
+ // Check authorization (this should be admin-only)
+ if (!hasValidAdminKey(req.query.adminKey)) {
+ return res.status(403).json({ message: 'Unauthorized' });
+ }
+
+ const encData = await EncryptedKeystrokeData.findById(req.params.encryptedId);
+
+ if (!encData) {
+ return res.status(404).json({ message: 'Encrypted data not found' });
+ }
+
+ const encryptionManager = new EncryptionManager(process.env.ENCRYPTION_KEY);
+
+ const decrypted = encryptionManager.decrypt(encData.encryptedData);
+
+ res.json({
+ decryptedData: decrypted,
+ decryptedAt: new Date()
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get anonymized metrics (no decryption needed)
+router.get('/privacy/anonymized/:sessionId', async (req, res) => {
+ try {
+ const encData = await EncryptedKeystrokeData.findOne({ sessionId: req.params.sessionId });
+
+ if (!encData) {
+ return res.status(404).json({ message: 'No data found' });
+ }
+
+ res.json({
+ anonymizedMetrics: encData.anonymizedMetrics,
+ timestamp: encData.encryptionTimestamp
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// User consent management
+router.post('/privacy/consent/:userId', async (req, res) => {
+ try {
+ const { consentGiven } = req.body;
+
+ const updated = await EncryptedKeystrokeData.updateMany(
+ { userId: req.params.userId },
+ { userConsent: consentGiven }
+ );
+
+ res.json({
+ message: `Consent updated: ${consentGiven ? 'given' : 'revoked'}`,
+ updatedCount: updated.modifiedCount
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Delete user's encrypted data (GDPR right to be forgotten)
+router.delete('/privacy/delete-user/:userId', requireAuth, async (req, res) => {
+ try {
+ // Verify ownership
+ if (req.userId !== req.params.userId && !hasValidAdminKey(req.query.adminKey)) {
+ return res.status(403).json({ message: 'Unauthorized' });
+ }
+
+ const deleted = await EncryptedKeystrokeData.deleteMany({ userId: req.params.userId });
+
+ res.json({
+ message: 'All encrypted keystroke data deleted',
+ deletedCount: deleted.deletedCount
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get privacy status
+router.get('/privacy/status/:userId', async (req, res) => {
+ try {
+ const data = await EncryptedKeystrokeData.findOne({ userId: req.params.userId })
+ .select('dataEncryptionEnabled userConsent encryptionTimestamp')
+ .lean();
+
+ res.json({
+ encryptionEnabled: data?.dataEncryptionEnabled || false,
+ userConsent: data?.userConsent || false,
+ lastEncrypted: data?.encryptionTimestamp
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/routes/reportSharing.js b/routes/reportSharing.js
new file mode 100644
index 000000000..0fc6cf36e
--- /dev/null
+++ b/routes/reportSharing.js
@@ -0,0 +1,159 @@
+const express = require('express');
+const router = express.Router();
+const Report = require('../models/Report');
+const Text = require('../models/Text');
+
+// Generate shareable report
+router.post('/report/generate', async (req, res) => {
+ try {
+ const { userId, sessionId, findings } = req.body;
+
+ const session = await Text.findById(sessionId);
+
+ if (!session) {
+ return res.status(404).json({ message: 'Session not found' });
+ }
+
+ const report = new Report({
+ userId,
+ textSessionId: sessionId,
+ reportContent: {
+ title: `Authenticity Report - ${new Date().toLocaleDateString()}`,
+ summary: `Analysis of "${session.content.slice(0, 50)}..."`,
+ trustScore: findings.trustScore || 0,
+ analysisDate: new Date(),
+ findings: findings.findings || []
+ },
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
+ });
+
+ await report.save();
+
+ const frontendUrl = process.env.FRONTEND_URL || `http://localhost:${process.env.PORT || 3000}`;
+ const shareUrl = `${frontendUrl}/verify/${report.sharingToken}`;
+
+ res.json({
+ reportId: report._id,
+ sharingLink: shareUrl,
+ token: report.sharingToken,
+ expiresAt: report.expiresAt
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// View shared report
+router.get('/report/verify/:token', async (req, res) => {
+ try {
+ const report = await Report.findOne({ sharingToken: req.params.token });
+
+ if (!report) {
+ return res.status(404).json({ message: 'Report not found' });
+ }
+
+ if (report.expiresAt && report.expiresAt < new Date()) {
+ return res.status(410).json({ message: 'Report has expired' });
+ }
+
+ // Check access restrictions
+ if (report.accessControl?.restrictedViewers && report.accessControl.restrictedViewers.length > 0) {
+ const viewerEmail = req.query.email;
+ if (!report.accessControl.restrictedViewers.includes(viewerEmail)) {
+ return res.status(403).json({ message: 'Access denied' });
+ }
+ }
+
+ // Log view
+ report.viewCount = (report.viewCount || 0) + 1;
+ report.viewLog = report.viewLog || [];
+ report.viewLog.push({
+ timestamp: new Date(),
+ viewerIP: req.ip
+ });
+ await report.save();
+
+ // Return report without sensitive data if not authorized
+ const reportData = {
+ reportContent: report.reportContent,
+ createdAt: report.createdAt,
+ expiresAt: report.expiresAt,
+ sharingToken: report.sharingToken
+ };
+
+ res.json(reportData);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get report details (authenticated)
+router.get('/report/:reportId', async (req, res) => {
+ try {
+ const report = await Report.findById(req.params.reportId);
+
+ if (!report) {
+ return res.status(404).json({ message: 'Report not found' });
+ }
+
+ res.json(report);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Update sharing settings
+router.put('/report/:reportId/sharing', async (req, res) => {
+ try {
+ const { isPublic, restrictedViewers, expiresAt } = req.body;
+
+ const report = await Report.findById(req.params.reportId);
+ if (!report) {
+ return res.status(404).json({ message: 'Report not found' });
+ }
+
+ if (typeof isPublic === 'boolean') {
+ report.isPublic = isPublic;
+ }
+
+ if (Array.isArray(restrictedViewers)) {
+ report.accessControl = report.accessControl || {};
+ report.accessControl.restrictedViewers = restrictedViewers;
+ }
+
+ if (expiresAt !== undefined) {
+ report.expiresAt = expiresAt ? new Date(expiresAt) : null;
+ }
+
+ await report.save();
+
+ res.json({ message: 'Sharing settings updated', report });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Revoke report sharing
+router.post('/report/:reportId/revoke', async (req, res) => {
+ try {
+ await Report.findByIdAndDelete(req.params.reportId);
+ res.json({ message: 'Report access revoked' });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get report list for user
+router.get('/reports/user/:userId', async (req, res) => {
+ try {
+ const reports = await Report.find({ userId: req.params.userId })
+ .select('reportContent sharingToken expiresAt createdAt viewCount')
+ .lean();
+
+ res.json({ reports });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/routes/trustScore.js b/routes/trustScore.js
new file mode 100644
index 000000000..4b24fa565
--- /dev/null
+++ b/routes/trustScore.js
@@ -0,0 +1,117 @@
+const express = require('express');
+const router = express.Router();
+const SessionTrustScore = require('../models/SessionTrustScore');
+
+// Calculate real-time trust score
+router.post('/trust-score/calculate', async (req, res) => {
+ try {
+ const { userId, sessionId, sessionMetrics, recentAnalysis } = req.body;
+
+ const keystrokeBehaviorScore = calculateKeystrokeBehavior(sessionMetrics);
+ const pasteDetectionScore = recentAnalysis?.pasteConfidence || 0;
+ const aiLikelihoodScore = recentAnalysis?.aiConfidenceScore || 0;
+ const anomalyScore = calculateAnomalyScore(sessionMetrics);
+ const deviceConsistencyScore = calculateDeviceConsistency(sessionMetrics.deviceInfo);
+
+ const weights = {
+ keystroke: 0.25,
+ paste: 0.20,
+ ai: 0.20,
+ anomaly: 0.20,
+ device: 0.15
+ };
+
+ const trustScore =
+ keystrokeBehaviorScore * weights.keystroke +
+ (100 - pasteDetectionScore) * weights.paste +
+ (100 - aiLikelihoodScore) * weights.ai +
+ (100 - anomalyScore) * weights.anomaly +
+ deviceConsistencyScore * weights.device;
+
+ let riskLevel = 'low';
+ if (trustScore < 50) riskLevel = 'high';
+ else if (trustScore < 75) riskLevel = 'medium';
+
+ let scoreDoc = await SessionTrustScore.findOne({ textSessionId: sessionId });
+ if (!scoreDoc) {
+ scoreDoc = new SessionTrustScore({ userId, textSessionId: sessionId });
+ }
+
+ scoreDoc.scoreHistory.push({
+ timestamp: new Date(),
+ score: Math.round(trustScore),
+ factors: {
+ keystrokeBehavior: keystrokeBehaviorScore,
+ pasteDetection: 100 - pasteDetectionScore,
+ aiLikelihood: 100 - aiLikelihoodScore,
+ anomalyScore: 100 - anomalyScore,
+ devicesConsistency: deviceConsistencyScore
+ }
+ });
+
+ scoreDoc.currentScore = Math.round(trustScore);
+ scoreDoc.riskLevel = riskLevel;
+
+ await scoreDoc.save();
+
+ res.json({
+ trustScore: Math.round(trustScore),
+ riskLevel,
+ factors: {
+ keystrokeBehavior: keystrokeBehaviorScore,
+ pasteDetection: 100 - pasteDetectionScore,
+ aiLikelihood: 100 - aiLikelihoodScore,
+ anomalyScore: 100 - anomalyScore,
+ devicesConsistency: deviceConsistencyScore
+ }
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get trust score history
+router.get('/trust-score/history/:sessionId', async (req, res) => {
+ try {
+ const scoreDoc = await SessionTrustScore.findOne({ textSessionId: req.params.sessionId });
+
+ if (!scoreDoc) {
+ return res.json({ scoreHistory: [] });
+ }
+
+ res.json({
+ currentScore: scoreDoc.currentScore,
+ riskLevel: scoreDoc.riskLevel,
+ scoreHistory: scoreDoc.scoreHistory
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Helper: Calculate keystroke behavior score
+function calculateKeystrokeBehavior(metrics) {
+ const baselineAvg = metrics.baselineAvgKeystrokeTime || 100;
+ const baselineStdDev = metrics.baselineStdDev || 30;
+
+ const diff = Math.abs(metrics.avgKeystrokeTime - baselineAvg);
+ const score = Math.max(0, 100 - (diff / baselineAvg) * 100);
+
+ return Math.round(score);
+}
+
+// Helper: Calculate anomaly score (scaled 0-100)
+function calculateAnomalyScore(metrics) {
+ // Placeholder: Would integrate with ML model
+ const isAnomaly = metrics.isAnomalous ? 50 : 0;
+ return Math.min(100, Math.max(0, isAnomaly));
+}
+
+// Helper: Calculate device consistency
+function calculateDeviceConsistency(deviceInfo) {
+ // Check if device is known/expected
+ // Placeholder: Would check historical devices
+ return 100; // Default: assume known device
+}
+
+module.exports = router;
diff --git a/routes/versionControl.js b/routes/versionControl.js
new file mode 100644
index 000000000..4d6616f42
--- /dev/null
+++ b/routes/versionControl.js
@@ -0,0 +1,106 @@
+const express = require('express');
+const router = express.Router();
+const WritingVersion = require('../models/WritingVersion');
+
+// Record keystroke for replay
+router.post('/version/record-keystroke', async (req, res) => {
+ try {
+ const { userId, sessionId, keystrokeEvent } = req.body;
+
+ let version = await WritingVersion.findOne({ textSessionId: sessionId });
+ if (!version) {
+ version = new WritingVersion({
+ userId,
+ textSessionId: sessionId,
+ keystrokeReplay: []
+ });
+ }
+
+ version.keystrokeReplay.push({
+ timestamp: keystrokeEvent.timestamp,
+ type: keystrokeEvent.type,
+ character: keystrokeEvent.character,
+ text: String(keystrokeEvent.text || '').slice(0, 500),
+ position: keystrokeEvent.position,
+ interval: keystrokeEvent.interval
+ });
+
+ // Save version snapshot every 50 keystrokes
+ if (version.keystrokeReplay.length % 50 === 0) {
+ version.versions = version.versions || [];
+ version.versions.push({
+ versionNumber: version.versions.length + 1,
+ timestamp: new Date(),
+ content: keystrokeEvent.currentContent || '',
+ characterCount: keystrokeEvent.currentContent?.length || 0,
+ changesSinceLast: {
+ inserted: 50,
+ deleted: 0,
+ modified: 0
+ }
+ });
+ }
+
+ await version.save();
+ res.json({ saved: true, keystrokeCount: version.keystrokeReplay.length });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get keystroke replay data
+router.get('/version/replay/:sessionId', async (req, res) => {
+ try {
+ const version = await WritingVersion.findOne({ textSessionId: req.params.sessionId });
+
+ if (!version) {
+ return res.json({ keystrokeReplay: [] });
+ }
+
+ res.json({
+ keystrokeReplay: version.keystrokeReplay,
+ versions: version.versions || []
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get versions list
+router.get('/version/versions/:sessionId', async (req, res) => {
+ try {
+ const version = await WritingVersion.findOne({ textSessionId: req.params.sessionId });
+
+ res.json({
+ versions: version?.versions || []
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Restore to specific version
+router.post('/version/restore/:sessionId/:versionNumber', async (req, res) => {
+ try {
+ const version = await WritingVersion.findOne({ textSessionId: req.params.sessionId });
+
+ if (!version) {
+ return res.status(404).json({ message: 'Version not found' });
+ }
+
+ const targetVersion = version.versions.find(v => v.versionNumber === parseInt(req.params.versionNumber));
+
+ if (!targetVersion) {
+ return res.status(404).json({ message: 'Version number not found' });
+ }
+
+ res.json({
+ restoredContent: targetVersion.content,
+ versionTimestamp: targetVersion.timestamp
+ });
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+module.exports = router;
diff --git a/server-integration.js b/server-integration.js
new file mode 100644
index 000000000..e861f7c80
--- /dev/null
+++ b/server-integration.js
@@ -0,0 +1,10 @@
+const { app, startServer } = require('./server');
+
+if (require.main === module) {
+ startServer();
+}
+
+module.exports = {
+ app,
+ startServer
+};
diff --git a/server.js b/server.js
new file mode 100644
index 000000000..91505b576
--- /dev/null
+++ b/server.js
@@ -0,0 +1,367 @@
+const path = require('path');
+
+require('./utils/loadEnv');
+
+const express = require('express');
+const mongoose = require('mongoose');
+const bcrypt = require('bcryptjs');
+
+const User = require('./models/User');
+const Text = require('./models/Text');
+const BiometricProfile = require('./models/BiometricProfile');
+
+const { requireAuth, signAuthToken } = require('./utils/auth');
+const {
+ buildSessionContext,
+ buildTextSessionPayload,
+ fetchSessionArtifacts,
+ runSessionAnalysis
+} = require('./utils/sessionPipeline');
+
+const biometricsRoutes = require('./routes/biometrics');
+const pasteDetectionRoutes = require('./routes/pasteDetection');
+const trustScoreRoutes = require('./routes/trustScore');
+const versionControlRoutes = require('./routes/versionControl');
+const deviceTrackingRoutes = require('./routes/deviceTracking');
+const reportSharingRoutes = require('./routes/reportSharing');
+const privacyRoutes = require('./routes/privacy');
+
+const app = express();
+
+const HOST = process.env.HOST || '127.0.0.1';
+const PORT = Number(process.env.PORT) || 3000;
+const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/vi-notes';
+
+let mongoConnectPromise = null;
+let databaseListenersAttached = false;
+
+app.use(express.json());
+app.use(express.static(path.join(__dirname, 'public')));
+
+function getDatabaseStatus() {
+ const stateMap = {
+ 0: 'disconnected',
+ 1: 'connected',
+ 2: 'connecting',
+ 3: 'disconnecting'
+ };
+
+ return stateMap[mongoose.connection.readyState] || 'unknown';
+}
+
+function isDatabaseReady() {
+ return mongoose.connection.readyState === 1;
+}
+
+function sendDatabaseUnavailable(res) {
+ return res.status(503).json({
+ message: 'Database is not connected. Check MONGODB_URI and MongoDB availability.',
+ database: getDatabaseStatus()
+ });
+}
+
+async function connectToDatabase() {
+ if (mongoose.connection.readyState === 1) {
+ return mongoose.connection;
+ }
+
+ if (mongoConnectPromise) {
+ return mongoConnectPromise;
+ }
+
+ if (!databaseListenersAttached) {
+ databaseListenersAttached = true;
+
+ mongoose.connection.on('connected', () => {
+ console.log(`MongoDB connected: ${MONGODB_URI}`);
+ });
+
+ mongoose.connection.on('disconnected', () => {
+ console.warn('MongoDB disconnected');
+ });
+
+ mongoose.connection.on('error', (err) => {
+ console.error(`MongoDB runtime error: ${err.message}`);
+ });
+ }
+
+ mongoConnectPromise = mongoose.connect(MONGODB_URI, {
+ serverSelectionTimeoutMS: 5000
+ }).then(() => mongoose.connection)
+ .catch((err) => {
+ console.error(`MongoDB connection error: ${err.message}`);
+ return null;
+ })
+ .finally(() => {
+ mongoConnectPromise = null;
+ });
+
+ return mongoConnectPromise;
+}
+
+function normalizeEmail(email) {
+ return String(email || '').trim().toLowerCase();
+}
+
+function getUsernameFromInput(username, email) {
+ const trimmedUsername = String(username || '').trim();
+ if (trimmedUsername) {
+ return trimmedUsername;
+ }
+
+ const normalizedEmail = normalizeEmail(email);
+ return normalizedEmail.includes('@') ? normalizedEmail.split('@')[0] : normalizedEmail;
+}
+
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'ok',
+ timestamp: new Date(),
+ database: getDatabaseStatus()
+ });
+});
+
+app.get('/', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+app.get('/verify/:token', (req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'report.html'));
+});
+
+app.post('/register', async (req, res) => {
+ try {
+ if (!isDatabaseReady()) {
+ return sendDatabaseUnavailable(res);
+ }
+
+ const { username, email, password } = req.body;
+ const normalizedEmail = normalizeEmail(email);
+ const resolvedUsername = getUsernameFromInput(username, normalizedEmail);
+
+ if (!normalizedEmail || !password) {
+ return res.status(400).json({
+ message: 'Email and password are required'
+ });
+ }
+
+ const existingUser = await User.findOne({ email: normalizedEmail });
+ if (existingUser) {
+ return res.status(409).json({ message: 'User already exists' });
+ }
+
+ const hashedPassword = await bcrypt.hash(password, 10);
+
+ const user = new User({
+ username: resolvedUsername,
+ email: normalizedEmail,
+ password: hashedPassword
+ });
+
+ await user.save();
+
+ res.status(201).json({
+ message: 'User registered successfully',
+ userId: user._id
+ });
+ } catch (err) {
+ console.error('Register error:', err);
+ res.status(500).json({ message: 'Error registering user' });
+ }
+});
+
+app.post('/login', async (req, res) => {
+ try {
+ if (!isDatabaseReady()) {
+ return sendDatabaseUnavailable(res);
+ }
+
+ const { email, password } = req.body;
+ const normalizedEmail = normalizeEmail(email);
+
+ if (!normalizedEmail || !password) {
+ return res.status(400).json({
+ message: 'Email and password are required'
+ });
+ }
+
+ const user = await User.findOne({ email: normalizedEmail });
+
+ if (!user) {
+ return res.status(400).json({ message: 'User not found' });
+ }
+
+ const isMatch = await bcrypt.compare(password, user.password);
+
+ if (!isMatch) {
+ return res.status(400).json({ message: 'Invalid password' });
+ }
+
+ const token = signAuthToken({ id: user._id });
+ const bioProfile = await BiometricProfile.findOne({ userId: user._id });
+
+ res.json({
+ message: 'Login successful',
+ token,
+ userId: user._id,
+ requiresBiometricVerification: bioProfile?.status === 'active'
+ });
+ } catch (err) {
+ console.error('Login error:', err);
+ res.status(500).json({ message: 'Error logging in' });
+ }
+});
+
+app.post('/save', requireAuth, async (req, res) => {
+ try {
+ if (!isDatabaseReady()) {
+ return sendDatabaseUnavailable(res);
+ }
+
+ const sessionData = buildSessionContext(req.body);
+ if (!sessionData.content.trim()) {
+ return res.status(400).json({
+ message: 'Content must not be empty'
+ });
+ }
+
+ const textSession = new Text({
+ _id: new mongoose.Types.ObjectId(),
+ ...buildTextSessionPayload(req.userId, sessionData)
+ });
+
+ const analysis = await runSessionAnalysis(req.userId, textSession, sessionData);
+
+ textSession.analysisSummary = analysis.textSummary;
+ textSession.artifactRefs = analysis.artifactRefs;
+ await textSession.save();
+
+ res.json({
+ message: 'Session saved and advanced analysis generated',
+ sessionId: textSession._id,
+ analysis: analysis.textSummary,
+ records: analysis.records
+ });
+ } catch (err) {
+ console.error('Save session error:', err.stack || err);
+ res.status(500).json({
+ message: process.env.NODE_ENV === 'development'
+ ? err.message
+ : 'Error saving session'
+ });
+ }
+});
+
+app.get('/my-sessions', requireAuth, async (req, res) => {
+ try {
+ if (!isDatabaseReady()) {
+ return sendDatabaseUnavailable(res);
+ }
+
+ const sessions = await Text.find({ userId: req.userId })
+ .select('content pasteCount sessionMetrics analysisSummary artifactRefs createdAt')
+ .sort({ createdAt: -1 })
+ .lean();
+
+ res.json(sessions);
+ } catch (err) {
+ console.error('Fetch sessions error:', err);
+ res.status(500).json({ message: 'Error fetching sessions' });
+ }
+});
+
+app.get('/session-details/:sessionId', requireAuth, async (req, res) => {
+ try {
+ if (!isDatabaseReady()) {
+ return sendDatabaseUnavailable(res);
+ }
+
+ const session = await Text.findOne({
+ _id: req.params.sessionId,
+ userId: req.userId
+ }).lean();
+
+ if (!session) {
+ return res.status(404).json({ message: 'Session not found' });
+ }
+
+ const artifacts = await fetchSessionArtifacts(req.userId, session);
+
+ res.json({
+ session,
+ ...artifacts
+ });
+ } catch (err) {
+ console.error('Session details error:', err);
+ res.status(500).json({ message: 'Error fetching session details' });
+ }
+});
+
+app.use('/api', (req, res, next) => {
+ if (!isDatabaseReady()) {
+ return sendDatabaseUnavailable(res);
+ }
+
+ next();
+});
+
+app.use('/api', biometricsRoutes);
+app.use('/api', pasteDetectionRoutes);
+app.use('/api', trustScoreRoutes);
+app.use('/api', versionControlRoutes);
+app.use('/api', deviceTrackingRoutes);
+app.use('/api', reportSharingRoutes);
+app.use('/api', privacyRoutes);
+
+app.use((err, req, res, next) => {
+ console.error('Unhandled error:', err);
+ res.status(500).json({
+ message: 'Internal Server Error',
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
+ });
+});
+
+async function startServer() {
+ const maxAttempts = 10;
+
+ const listenOnPort = (port) => new Promise((resolve) => {
+ const server = app.listen(port, HOST, () => {
+ console.log(`Server running at http://${HOST}:${port}`);
+ console.log(`Advanced API routes available at http://${HOST}:${port}/api/*`);
+ resolve(server);
+ });
+
+ server.on('error', (err) => {
+ if (err.code === 'EADDRINUSE') {
+ console.warn(`Port ${port} on ${HOST} is busy, trying the next port.`);
+ resolve(null);
+ return;
+ }
+
+ console.error(`HTTP server error: ${err.message}`);
+ resolve(null);
+ });
+ });
+
+ void connectToDatabase();
+
+ for (let offset = 0; offset < maxAttempts; offset += 1) {
+ const candidatePort = PORT + offset;
+ const server = await listenOnPort(candidatePort);
+ if (server) {
+ return server;
+ }
+ }
+
+ throw new Error(`Unable to start the server on ports ${PORT} through ${PORT + maxAttempts - 1}.`);
+}
+
+if (require.main === module) {
+ startServer();
+}
+
+module.exports = {
+ app,
+ connectToDatabase,
+ startServer
+};
diff --git a/utils/auth.js b/utils/auth.js
new file mode 100644
index 000000000..1de2a0ef8
--- /dev/null
+++ b/utils/auth.js
@@ -0,0 +1,63 @@
+require('./loadEnv');
+
+const jwt = require('jsonwebtoken');
+
+function getJwtSecret() {
+ return process.env.JWT_SECRET || 'vi-notes-development-secret';
+}
+
+function getJwtExpiry() {
+ return process.env.JWT_EXPIRY || '24h';
+}
+
+function normalizeAuthToken(headerValue) {
+ if (!headerValue || typeof headerValue !== 'string') {
+ return null;
+ }
+
+ return headerValue.startsWith('Bearer ')
+ ? headerValue.slice('Bearer '.length).trim()
+ : headerValue.trim();
+}
+
+function signAuthToken(payload, options = {}) {
+ const { expiresIn = getJwtExpiry(), ...restOptions } = options;
+
+ return jwt.sign(payload, getJwtSecret(), {
+ expiresIn,
+ ...restOptions
+ });
+}
+
+function verifyAuthToken(headerValue) {
+ const token = normalizeAuthToken(headerValue);
+
+ if (!token) {
+ throw new Error('Missing authorization token');
+ }
+
+ return jwt.verify(token, getJwtSecret());
+}
+
+function requireAuth(req, res, next) {
+ try {
+ const headerValue = req.headers.authorization || req.headers.Authorization;
+ const decoded = verifyAuthToken(headerValue);
+
+ req.userId = String(decoded.id);
+ req.user = decoded;
+ next();
+ } catch (err) {
+ const message = err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token';
+ res.status(401).json({ message });
+ }
+}
+
+module.exports = {
+ getJwtSecret,
+ getJwtExpiry,
+ normalizeAuthToken,
+ signAuthToken,
+ verifyAuthToken,
+ requireAuth
+};
diff --git a/utils/encryption.js b/utils/encryption.js
new file mode 100644
index 000000000..996183199
--- /dev/null
+++ b/utils/encryption.js
@@ -0,0 +1,85 @@
+const crypto = require('crypto');
+
+// Encryption Manager for sensitive keystroke data
+class EncryptionManager {
+ constructor(encryptionKey = null) {
+ this.ALGORITHM = 'aes-256-gcm';
+ this.ENCRYPTION_KEY = EncryptionManager.resolveKey(encryptionKey);
+ }
+
+ // Encrypt keystroke data
+ encrypt(data) {
+ const iv = crypto.randomBytes(16);
+ const cipher = crypto.createCipheriv(this.ALGORITHM, this.ENCRYPTION_KEY, iv);
+
+ const payload = data === undefined ? null : data;
+
+ let encrypted = cipher.update(JSON.stringify(payload), 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ const authTag = cipher.getAuthTag();
+
+ return {
+ iv: iv.toString('hex'),
+ encryptedData: encrypted,
+ authTag: authTag.toString('hex')
+ };
+ }
+
+ // Decrypt keystroke data
+ decrypt(encryptedObj) {
+ const decipher = crypto.createDecipheriv(
+ this.ALGORITHM,
+ this.ENCRYPTION_KEY,
+ Buffer.from(encryptedObj.iv, 'hex')
+ );
+
+ decipher.setAuthTag(Buffer.from(encryptedObj.authTag, 'hex'));
+
+ let decrypted = decipher.update(encryptedObj.encryptedData, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+
+ return JSON.parse(decrypted);
+ }
+
+ // Anonymize keystroke data (remove PII, keep patterns)
+ static anonymize(keystrokeData = {}) {
+ return {
+ intervals: Array.isArray(keystrokeData.intervals) ? keystrokeData.intervals : [],
+ pausePatterns: Array.isArray(keystrokeData.pausePatterns) ? keystrokeData.pausePatterns : [],
+ totalDuration: Number(keystrokeData.totalDuration) || 0,
+ avgKeystrokeTime: Number(keystrokeData.avgKeystrokeTime) || 0,
+ stdDevKeystrokeTime: Number(keystrokeData.stdDevKeystrokeTime) || 0,
+ // Removed: character content, exact positions, detailed timing
+ };
+ }
+
+ static resolveKey(encryptionKey) {
+ if (Buffer.isBuffer(encryptionKey) && encryptionKey.length > 0) {
+ return encryptionKey.length === 32
+ ? encryptionKey
+ : crypto.createHash('sha256').update(encryptionKey).digest();
+ }
+
+ if (typeof encryptionKey === 'string' && encryptionKey.trim()) {
+ const normalizedKey = encryptionKey.trim();
+
+ if (/^[0-9a-fA-F]{64}$/.test(normalizedKey)) {
+ return Buffer.from(normalizedKey, 'hex');
+ }
+
+ return crypto.createHash('sha256').update(normalizedKey).digest();
+ }
+
+ const fallbackSecret = process.env.JWT_SECRET || 'vi-notes-development-key';
+ return crypto.createHash('sha256').update(fallbackSecret).digest();
+ }
+
+ // Hash device fingerprint
+ static hashDeviceFingerprint(deviceInfo) {
+ const data = JSON.stringify(deviceInfo);
+ return crypto.createHash('sha256').update(data).digest('hex');
+ }
+}
+
+module.exports = EncryptionManager;
diff --git a/utils/loadEnv.js b/utils/loadEnv.js
new file mode 100644
index 000000000..41dcc182c
--- /dev/null
+++ b/utils/loadEnv.js
@@ -0,0 +1,35 @@
+const fs = require('fs');
+const path = require('path');
+
+const envPath = path.join(__dirname, '..', '.env');
+
+if (fs.existsSync(envPath)) {
+ const envFile = fs.readFileSync(envPath, 'utf8');
+
+ for (const rawLine of envFile.split(/\r?\n/)) {
+ const line = rawLine.trim();
+
+ if (!line || line.startsWith('#') || !line.includes('=')) {
+ continue;
+ }
+
+ const separatorIndex = line.indexOf('=');
+ const key = line.slice(0, separatorIndex).trim();
+ let value = line.slice(separatorIndex + 1).trim();
+
+ if (!key || process.env[key] !== undefined) {
+ continue;
+ }
+
+ if (
+ (value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith('\'') && value.endsWith('\''))
+ ) {
+ value = value.slice(1, -1);
+ }
+
+ process.env[key] = value;
+ }
+}
+
+module.exports = process.env;
diff --git a/utils/sessionPipeline.js b/utils/sessionPipeline.js
new file mode 100644
index 000000000..48dd4879e
--- /dev/null
+++ b/utils/sessionPipeline.js
@@ -0,0 +1,1053 @@
+const AuthenticationEvent = require('../models/AuthenticationEvent');
+const BiometricProfile = require('../models/BiometricProfile');
+const DeviceProfile = require('../models/DeviceProfile');
+const EncryptedKeystrokeData = require('../models/EncryptedKeystrokeData');
+const PasteDetectionLog = require('../models/PasteDetectionLog');
+const Report = require('../models/Report');
+const SessionTrustScore = require('../models/SessionTrustScore');
+const WritingVersion = require('../models/WritingVersion');
+
+const EncryptionManager = require('./encryption');
+
+function toNumber(value, fallback = 0) {
+ return Number.isFinite(Number(value)) ? Number(value) : fallback;
+}
+
+function round(value, digits = 2) {
+ const factor = 10 ** digits;
+ return Math.round(toNumber(value) * factor) / factor;
+}
+
+function clamp(value, min, max) {
+ return Math.min(max, Math.max(min, toNumber(value)));
+}
+
+function average(values) {
+ if (!Array.isArray(values) || values.length === 0) {
+ return 0;
+ }
+
+ return values.reduce((sum, value) => sum + toNumber(value), 0) / values.length;
+}
+
+function standardDeviation(values) {
+ if (!Array.isArray(values) || values.length === 0) {
+ return 0;
+ }
+
+ const mean = average(values);
+ const variance = average(values.map((value) => (toNumber(value) - mean) ** 2));
+ return Math.sqrt(variance);
+}
+
+function normalizeText(text) {
+ return typeof text === 'string' ? text : '';
+}
+
+function normalizeTelemetry(telemetry = {}) {
+ const intervals = Array.isArray(telemetry.intervals)
+ ? telemetry.intervals.map((value) => clamp(value, 0, 60000)).filter((value) => value > 0)
+ : [];
+
+ const pauseIntervals = Array.isArray(telemetry.pauseIntervals)
+ ? telemetry.pauseIntervals.map((value) => clamp(value, 0, 60000)).filter((value) => value > 0)
+ : [];
+
+ const pastedSegments = Array.isArray(telemetry.pastedSegments)
+ ? telemetry.pastedSegments.map((segment, index) => ({
+ startPosition: toNumber(segment.startPosition),
+ endPosition: toNumber(segment.endPosition),
+ text: normalizeText(segment.text).slice(0, 180),
+ length: toNumber(segment.length, normalizeText(segment.text).length),
+ trimmedLength: toNumber(segment.trimmedLength, normalizeText(segment.text).trim().length),
+ timestamp: segment.timestamp || new Date().toISOString(),
+ segmentId: `paste_${index + 1}`,
+ isFormattingOnly: Boolean(segment.isFormattingOnly),
+ clipboardMatched: segment.clipboardMatched !== false,
+ replacedTextLength: toNumber(segment.replacedTextLength),
+ isDuplicateReplacement: Boolean(segment.isDuplicateReplacement)
+ }))
+ : [];
+
+ const keystrokeEvents = Array.isArray(telemetry.keystrokeEvents)
+ ? telemetry.keystrokeEvents.slice(-400).map((event) => ({
+ timestamp: toNumber(event.timestamp, Date.now()),
+ type: ['keystroke', 'delete', 'paste'].includes(event.type) ? event.type : 'keystroke',
+ character: normalizeText(event.character).slice(0, 20) || '[Key]',
+ text: normalizeText(event.text).slice(0, 500),
+ position: toNumber(event.position),
+ interval: toNumber(event.interval)
+ }))
+ : [];
+
+ const versionHistory = Array.isArray(telemetry.versionHistory)
+ ? telemetry.versionHistory.slice(-20).map((version, index) => ({
+ versionNumber: toNumber(version.versionNumber, index + 1),
+ timestamp: version.timestamp || new Date().toISOString(),
+ reason: normalizeText(version.reason) || 'typing',
+ content: normalizeText(version.content),
+ characterCount: toNumber(version.characterCount, normalizeText(version.content).length),
+ changesSinceLast: {
+ inserted: toNumber(version.changesSinceLast?.inserted),
+ deleted: toNumber(version.changesSinceLast?.deleted),
+ modified: toNumber(version.changesSinceLast?.modified)
+ }
+ }))
+ : [];
+
+ const rawDeviceInfo = telemetry.deviceInfo || {};
+
+ return {
+ intervals,
+ pauseIntervals,
+ deleteCount: toNumber(telemetry.deleteCount),
+ focusLossCount: toNumber(telemetry.focusLossCount),
+ pastedSegments,
+ keystrokeEvents,
+ versionHistory,
+ deviceInfo: {
+ osType: normalizeText(rawDeviceInfo.osType) || 'Unknown',
+ browserAgent: normalizeText(rawDeviceInfo.browserAgent) || 'Unknown',
+ screenResolution: normalizeText(rawDeviceInfo.screenResolution) || 'Unknown',
+ timezone: normalizeText(rawDeviceInfo.timezone) || 'Unknown',
+ inputMethod: normalizeText(rawDeviceInfo.inputMethod) || 'keyboard',
+ language: normalizeText(rawDeviceInfo.language) || 'Unknown'
+ }
+ };
+}
+
+function buildDeviceSnapshot(deviceInfo) {
+ const baseDeviceInfo = {
+ osType: deviceInfo.osType,
+ browserAgent: deviceInfo.browserAgent,
+ screenResolution: deviceInfo.screenResolution,
+ timezone: deviceInfo.timezone,
+ inputMethod: deviceInfo.inputMethod
+ };
+
+ return {
+ deviceId: EncryptionManager.hashDeviceFingerprint(baseDeviceInfo).slice(0, 16),
+ ...baseDeviceInfo
+ };
+}
+
+function buildRhythmPattern(intervals) {
+ if (!intervals.length) {
+ return [0, 0, 0, 0, 0];
+ }
+
+ const maxValue = Math.max(...intervals);
+ if (!maxValue) {
+ return [0, 0, 0, 0, 0];
+ }
+
+ const buckets = [0, 0, 0, 0, 0];
+
+ intervals.forEach((interval) => {
+ const bucketIndex = Math.min(4, Math.floor((interval / maxValue) * 5));
+ buckets[bucketIndex] += 1;
+ });
+
+ return buckets;
+}
+
+function buildLinguisticMetrics(content) {
+ const safeContent = normalizeText(content);
+ const words = safeContent.match(/\b[\w'-]+\b/g) || [];
+ const sentences = safeContent
+ .split(/[.!?]+/)
+ .map((value) => value.trim())
+ .filter(Boolean);
+ const paragraphs = safeContent
+ .split(/\n+/)
+ .map((value) => value.trim())
+ .filter(Boolean);
+ const uniqueWords = new Set(words.map((word) => word.toLowerCase()));
+
+ return {
+ wordCount: words.length,
+ sentenceCount: sentences.length,
+ paragraphCount: paragraphs.length || (safeContent ? 1 : 0),
+ vocabularyDiversity: round(words.length ? (uniqueWords.size / words.length) * 100 : 0),
+ averageSentenceLength: round(sentences.length ? words.length / sentences.length : 0)
+ };
+}
+
+function buildSessionMetrics(content, duration, totalKeystrokes, pasteCount, telemetry, linguisticMetrics) {
+ const avgKeystrokeTime = round(average(telemetry.intervals));
+ const stdDevKeystrokeTime = round(standardDeviation(telemetry.intervals));
+ const avgPauseTime = round(average(telemetry.pauseIntervals));
+ const longestPause = round(Math.max(0, ...telemetry.pauseIntervals));
+ const typingSpeed = round(duration > 0 ? (linguisticMetrics.wordCount / duration) * 60 : 0);
+
+ return {
+ avgKeystrokeTime,
+ stdDevKeystrokeTime,
+ avgPauseTime,
+ longestPause,
+ pauseCount: telemetry.pauseIntervals.length,
+ deletionCount: telemetry.deleteCount,
+ focusLossCount: telemetry.focusLossCount,
+ revisionCount: telemetry.versionHistory.length,
+ typingSpeed,
+ errorRate: round(totalKeystrokes > 0 ? (telemetry.deleteCount / totalKeystrokes) * 100 : 0),
+ pasteCount,
+ pasteRatio: round(content.length ? (pasteCount / Math.max(1, content.length)) * 100 : 0),
+ rhythmPattern: buildRhythmPattern(telemetry.intervals),
+ sampleSize: telemetry.intervals.length
+ };
+}
+
+function buildLinguisticMarkers(text) {
+ const safeText = normalizeText(text);
+ const metrics = buildLinguisticMetrics(safeText);
+ const punctuationCount = (safeText.match(/[.!?,;:]/g) || []).length;
+ const aiMarkers = [
+ 'furthermore',
+ 'moreover',
+ 'in conclusion',
+ 'in summary',
+ 'additionally',
+ 'overall'
+ ];
+
+ return {
+ vocabularyShift: round(100 - metrics.vocabularyDiversity),
+ sentenceComplexity: round(metrics.averageSentenceLength),
+ punctuationConsistency: round(metrics.wordCount ? (punctuationCount / metrics.wordCount) * 100 : 0),
+ grammarAnomalies: aiMarkers
+ .filter((marker) => safeText.toLowerCase().includes(marker))
+ .map((marker) => `marker:${marker}`)
+ };
+}
+
+function buildUniformityScore(intervals) {
+ if (!intervals.length) {
+ return 0;
+ }
+
+ const mean = average(intervals);
+ if (!mean) {
+ return 0;
+ }
+
+ const coefficientOfVariation = standardDeviation(intervals) / mean;
+ return round(clamp(100 - coefficientOfVariation * 100, 0, 100));
+}
+
+function classifyPasteSegment(segment, segmentText) {
+ const safeSegmentText = normalizeText(segmentText);
+ const trimmedLength = toNumber(segment.trimmedLength, safeSegmentText.trim().length);
+ const isFormattingOnly = Boolean(segment.isFormattingOnly || trimmedLength === 0);
+ const isDuplicateReplacement = Boolean(segment.isDuplicateReplacement);
+ const isShortPaste = trimmedLength > 0 && trimmedLength < 20;
+ const isMeaningfulPaste = !isFormattingOnly && !isDuplicateReplacement && trimmedLength >= 20;
+ const severity = isFormattingOnly
+ ? 'ignored'
+ : isMeaningfulPaste
+ ? trimmedLength >= 80 ? 'high' : 'medium'
+ : 'low';
+ const flags = [];
+
+ if (isFormattingOnly) {
+ flags.push('formatting_only_paste');
+ }
+
+ if (isDuplicateReplacement) {
+ flags.push('duplicate_replacement');
+ }
+
+ if (isShortPaste) {
+ flags.push('short_paste');
+ }
+
+ if (segment.clipboardMatched === false) {
+ flags.push('clipboard_mismatch');
+ }
+
+ if (trimmedLength >= 80) {
+ flags.push('long_paste');
+ }
+
+ return {
+ trimmedLength,
+ isFormattingOnly,
+ isDuplicateReplacement,
+ isShortPaste,
+ isMeaningfulPaste,
+ severity,
+ flags
+ };
+}
+
+function buildPasteAnalysis(content, telemetry, sessionMetrics, linguisticMetrics) {
+ const safeContent = normalizeText(content);
+ const uniformityScore = buildUniformityScore(telemetry.intervals);
+ const baseAiConfidence = round(clamp(
+ uniformityScore * 0.45 +
+ (sessionMetrics.pauseCount === 0 ? 20 : 0) +
+ (sessionMetrics.deletionCount <= 1 ? 12 : 0) +
+ (linguisticMetrics.vocabularyDiversity < 45 ? 12 : 0),
+ 0,
+ 100
+ ));
+
+ const detectionResults = telemetry.pastedSegments.map((segment) => {
+ const segmentText = segment.text || safeContent.slice(segment.startPosition, segment.endPosition);
+ const pasteClassification = classifyPasteSegment(segment, segmentText);
+ const verdict = pasteClassification.isFormattingOnly ? 'human_typed' : 'pasted';
+ const pasteConfidenceScore = pasteClassification.isFormattingOnly
+ ? 0
+ : pasteClassification.severity === 'low'
+ ? 25
+ : pasteClassification.severity === 'medium'
+ ? 70
+ : 90;
+
+ return {
+ segmentId: segment.segmentId,
+ startPosition: segment.startPosition,
+ endPosition: segment.endPosition,
+ segmentText,
+ keystrokeAnalysis: {
+ hasKeystrokeIntervals: false,
+ isInstantAppearance: true,
+ uniformityScore,
+ variationCv: round(
+ sessionMetrics.avgKeystrokeTime
+ ? sessionMetrics.stdDevKeystrokeTime / sessionMetrics.avgKeystrokeTime
+ : 0
+ ),
+ expectedIntervals: Math.max(0, segment.length - 1),
+ actualIntervals: 0
+ },
+ linguisticMarkers: buildLinguisticMarkers(segmentText),
+ aiConfidenceScore: round(clamp(baseAiConfidence + (segment.length > 80 ? 10 : 0), 0, 100)),
+ pasteConfidenceScore,
+ verdict,
+ flags: verdict === 'pasted'
+ ? ['clipboard_insert', ...pasteClassification.flags]
+ : pasteClassification.flags
+ };
+ });
+
+ if (!detectionResults.length && safeContent) {
+ const verdict = baseAiConfidence >= 70 ? 'ai_generated' : baseAiConfidence >= 50 ? 'uncertain' : 'human_typed';
+
+ detectionResults.push({
+ segmentId: 'full_text',
+ startPosition: 0,
+ endPosition: safeContent.length,
+ segmentText: safeContent.slice(0, 240),
+ keystrokeAnalysis: {
+ hasKeystrokeIntervals: telemetry.intervals.length > 0,
+ isInstantAppearance: false,
+ uniformityScore,
+ variationCv: round(
+ sessionMetrics.avgKeystrokeTime
+ ? sessionMetrics.stdDevKeystrokeTime / sessionMetrics.avgKeystrokeTime
+ : 0
+ ),
+ expectedIntervals: Math.max(0, safeContent.length - 1),
+ actualIntervals: telemetry.intervals.length
+ },
+ linguisticMarkers: buildLinguisticMarkers(safeContent),
+ aiConfidenceScore: baseAiConfidence,
+ pasteConfidenceScore: 0,
+ verdict,
+ flags: verdict === 'ai_generated' ? ['uniform_typing_pattern'] : []
+ });
+ }
+
+ const pastedSegments = detectionResults.filter((result) => result.verdict === 'pasted').length;
+ const significantPastedSegments = detectionResults.filter((result) => (
+ result.verdict === 'pasted' &&
+ !result.flags.includes('short_paste') &&
+ !result.flags.includes('duplicate_replacement')
+ )).length;
+ const trivialPastedSegments = detectionResults.filter((result) => (
+ result.verdict === 'pasted' &&
+ (result.flags.includes('short_paste') || result.flags.includes('duplicate_replacement'))
+ )).length;
+ const ignoredPastedSegments = detectionResults.filter((result) => result.flags.includes('formatting_only_paste')).length;
+ const aiSegments = detectionResults.filter((result) => result.verdict === 'ai_generated').length;
+ const suspiciousSegments = detectionResults.filter((result) => (
+ result.verdict === 'ai_generated' ||
+ result.verdict === 'uncertain' ||
+ (result.verdict === 'pasted' && !result.flags.includes('short_paste') && !result.flags.includes('duplicate_replacement'))
+ )).length;
+ const suspicionRatio = detectionResults.length ? suspiciousSegments / detectionResults.length : 0;
+
+ return {
+ detectionResults,
+ summaryAnalysis: {
+ totalSegments: detectionResults.length,
+ pastedSegments,
+ significantPastedSegments,
+ trivialPastedSegments,
+ ignoredPastedSegments,
+ aiSegments,
+ mixedContentDetected: significantPastedSegments > 0 && safeContent.length > 0,
+ suspicionLevel: significantPastedSegments > 0 || suspicionRatio >= 0.6
+ ? 'high'
+ : trivialPastedSegments > 0 || suspicionRatio >= 0.3
+ ? 'medium'
+ : 'low'
+ },
+ overallAiConfidence: baseAiConfidence
+ };
+}
+
+function calculateSimilarityScore(currentValue, baselineValue) {
+ if (!baselineValue) {
+ return 100;
+ }
+
+ const difference = Math.abs(toNumber(currentValue) - toNumber(baselineValue));
+ return round(clamp(100 - (difference / Math.max(1, baselineValue)) * 100, 0, 100));
+}
+
+function buildDeviceConsistencyScore(sessionMetrics, profiles) {
+ if (!profiles.length) {
+ return 100;
+ }
+
+ const avgKeystrokeTime = average(
+ profiles.map((profile) => profile.behaviorProfile?.avgKeystrokeTime || sessionMetrics.avgKeystrokeTime)
+ );
+ const avgTypingSpeed = average(
+ profiles.map((profile) => profile.behaviorProfile?.typingSpeed || sessionMetrics.typingSpeed)
+ );
+
+ return round(
+ (
+ calculateSimilarityScore(sessionMetrics.avgKeystrokeTime, avgKeystrokeTime) +
+ calculateSimilarityScore(sessionMetrics.typingSpeed, avgTypingSpeed)
+ ) / 2
+ );
+}
+
+async function upsertDeviceProfile(userId, deviceSnapshot, sessionMetrics, pasteCount) {
+ const userProfiles = await DeviceProfile.find({ userId });
+ const consistencyScore = buildDeviceConsistencyScore(sessionMetrics, userProfiles);
+ const existingProfile = userProfiles.find((profile) => profile.deviceId === deviceSnapshot.deviceId);
+
+ const deviceInfo = {
+ osType: deviceSnapshot.osType,
+ browserAgent: deviceSnapshot.browserAgent,
+ screenResolution: deviceSnapshot.screenResolution,
+ timezone: deviceSnapshot.timezone,
+ inputMethod: deviceSnapshot.inputMethod
+ };
+
+ const behaviorProfile = existingProfile
+ ? {
+ avgKeystrokeTime: round(average([existingProfile.behaviorProfile?.avgKeystrokeTime, sessionMetrics.avgKeystrokeTime])),
+ typingSpeed: round(average([existingProfile.behaviorProfile?.typingSpeed, sessionMetrics.typingSpeed])),
+ errorRate: round(average([existingProfile.behaviorProfile?.errorRate, sessionMetrics.errorRate])),
+ pasteFrequency: round(average([existingProfile.behaviorProfile?.pasteFrequency, pasteCount]))
+ }
+ : {
+ avgKeystrokeTime: sessionMetrics.avgKeystrokeTime,
+ typingSpeed: sessionMetrics.typingSpeed,
+ errorRate: sessionMetrics.errorRate,
+ pasteFrequency: pasteCount
+ };
+
+ const profile = existingProfile || new DeviceProfile({
+ userId,
+ deviceId: deviceSnapshot.deviceId,
+ firstSeen: new Date()
+ });
+
+ profile.deviceInfo = deviceInfo;
+ profile.behaviorProfile = behaviorProfile;
+ profile.trustScore = existingProfile ? round(average([profile.trustScore, consistencyScore])) : (userProfiles.length ? consistencyScore : 100);
+ profile.isKnownDevice = Boolean(existingProfile || userProfiles.length === 0);
+ profile.lastSeen = new Date();
+
+ await profile.save();
+
+ return {
+ profileId: String(profile._id),
+ deviceId: profile.deviceId,
+ trustScore: round(profile.trustScore),
+ consistencyScore,
+ isKnownDevice: profile.isKnownDevice
+ };
+}
+
+function getBiometricDeviceKey(deviceSnapshot) {
+ return [
+ deviceSnapshot.osType,
+ deviceSnapshot.browserAgent,
+ deviceSnapshot.screenResolution,
+ deviceSnapshot.timezone
+ ].join('|');
+}
+
+async function upsertBiometricProfile(userId, deviceSnapshot, sessionMetrics) {
+ const currentMetrics = {
+ avgKeystrokeTime: sessionMetrics.avgKeystrokeTime,
+ stdDevKeystrokeTime: sessionMetrics.stdDevKeystrokeTime,
+ avgPauseTime: sessionMetrics.avgPauseTime,
+ typingSpeed: sessionMetrics.typingSpeed,
+ deletionRate: sessionMetrics.errorRate,
+ rhythmPattern: sessionMetrics.rhythmPattern
+ };
+
+ let profile = await BiometricProfile.findOne({ userId });
+
+ if (!profile) {
+ profile = new BiometricProfile({
+ userId,
+ baselineMetrics: currentMetrics,
+ deviceSignature: {
+ keyboardType: deviceSnapshot.inputMethod,
+ osType: deviceSnapshot.osType,
+ browserAgent: deviceSnapshot.browserAgent,
+ screenResolution: deviceSnapshot.screenResolution,
+ timezone: deviceSnapshot.timezone
+ },
+ baselineRequired: 3,
+ baselineSessionsCount: 1,
+ status: 'collecting_baseline',
+ lastUpdated: new Date()
+ });
+
+ await profile.save();
+
+ return {
+ profileId: String(profile._id),
+ profileStatus: profile.status,
+ verificationStatus: 'success',
+ verificationScore: 100,
+ flags: ['baseline_started']
+ };
+ }
+
+ const baseline = profile.baselineMetrics || {};
+ const verificationScore = round(average([
+ calculateSimilarityScore(sessionMetrics.avgKeystrokeTime, baseline.avgKeystrokeTime),
+ calculateSimilarityScore(sessionMetrics.stdDevKeystrokeTime, baseline.stdDevKeystrokeTime),
+ calculateSimilarityScore(sessionMetrics.avgPauseTime, baseline.avgPauseTime),
+ calculateSimilarityScore(sessionMetrics.typingSpeed, baseline.typingSpeed)
+ ]));
+
+ const previousDeviceKey = [
+ profile.deviceSignature?.osType,
+ profile.deviceSignature?.browserAgent,
+ profile.deviceSignature?.screenResolution,
+ profile.deviceSignature?.timezone
+ ].join('|');
+ const currentDeviceKey = getBiometricDeviceKey(deviceSnapshot);
+ const flags = [];
+
+ if (previousDeviceKey && previousDeviceKey !== currentDeviceKey) {
+ flags.push('new_device_signature');
+ }
+
+ if (verificationScore < 60) {
+ flags.push('behavior_shift');
+ }
+
+ const verificationStatus = verificationScore < 55
+ ? 'failed'
+ : verificationScore < 75
+ ? 'challenge'
+ : 'success';
+
+ const alpha = 0.35;
+ profile.baselineMetrics = {
+ avgKeystrokeTime: round((1 - alpha) * toNumber(baseline.avgKeystrokeTime, currentMetrics.avgKeystrokeTime) + alpha * currentMetrics.avgKeystrokeTime),
+ stdDevKeystrokeTime: round((1 - alpha) * toNumber(baseline.stdDevKeystrokeTime, currentMetrics.stdDevKeystrokeTime) + alpha * currentMetrics.stdDevKeystrokeTime),
+ avgPauseTime: round((1 - alpha) * toNumber(baseline.avgPauseTime, currentMetrics.avgPauseTime) + alpha * currentMetrics.avgPauseTime),
+ typingSpeed: round((1 - alpha) * toNumber(baseline.typingSpeed, currentMetrics.typingSpeed) + alpha * currentMetrics.typingSpeed),
+ deletionRate: round((1 - alpha) * toNumber(baseline.deletionRate, currentMetrics.deletionRate) + alpha * currentMetrics.deletionRate),
+ rhythmPattern: currentMetrics.rhythmPattern
+ };
+ profile.baselineSessionsCount += 1;
+ profile.status = profile.baselineSessionsCount >= profile.baselineRequired ? 'active' : profile.status;
+ profile.failureCount = verificationStatus === 'failed' ? profile.failureCount + 1 : 0;
+ profile.lastVerificationFailed = verificationStatus === 'failed' ? new Date() : profile.lastVerificationFailed;
+ profile.deviceSignature = {
+ keyboardType: deviceSnapshot.inputMethod,
+ osType: deviceSnapshot.osType,
+ browserAgent: deviceSnapshot.browserAgent,
+ screenResolution: deviceSnapshot.screenResolution,
+ timezone: deviceSnapshot.timezone
+ };
+ profile.lastUpdated = new Date();
+
+ await profile.save();
+
+ return {
+ profileId: String(profile._id),
+ profileStatus: profile.status,
+ verificationStatus,
+ verificationScore,
+ flags
+ };
+}
+
+function buildEventMetrics(sessionMetrics) {
+ return {
+ avgKeystrokeTime: sessionMetrics.avgKeystrokeTime,
+ typingSpeed: sessionMetrics.typingSpeed,
+ sampleSize: sessionMetrics.sampleSize
+ };
+}
+
+async function createAuthenticationEvent(userId, sessionId, deviceSnapshot, sessionMetrics, biometricSummary) {
+ const authEvent = new AuthenticationEvent({
+ userId,
+ sessionId: String(sessionId),
+ keystrokeMetrics: buildEventMetrics(sessionMetrics),
+ deviceInfo: {
+ deviceId: deviceSnapshot.deviceId,
+ osType: deviceSnapshot.osType,
+ timezone: deviceSnapshot.timezone
+ },
+ verificationResult: {
+ status: biometricSummary.verificationStatus,
+ confidenceScore: biometricSummary.verificationScore,
+ matchPercentage: biometricSummary.verificationScore,
+ flags: biometricSummary.flags
+ },
+ fallbackMethod: biometricSummary.verificationStatus === 'challenge' ? 'email_link' : 'none',
+ fallbackStatus: biometricSummary.verificationStatus === 'challenge' ? 'recommended' : 'not_required'
+ });
+
+ await authEvent.save();
+ return authEvent;
+}
+
+async function createWritingVersion(userId, sessionId, content, telemetry) {
+ const versions = telemetry.versionHistory.length
+ ? telemetry.versionHistory
+ : [{
+ versionNumber: 1,
+ timestamp: new Date().toISOString(),
+ reason: 'save',
+ content,
+ characterCount: content.length,
+ changesSinceLast: {
+ inserted: content.length,
+ deleted: 0,
+ modified: 0
+ }
+ }];
+
+ const versionDoc = new WritingVersion({
+ userId,
+ textSessionId: sessionId,
+ versions: versions.map((version) => ({
+ versionNumber: version.versionNumber,
+ timestamp: new Date(version.timestamp),
+ content: version.content,
+ characterCount: version.characterCount,
+ changesSinceLast: version.changesSinceLast
+ })),
+ keystrokeReplay: telemetry.keystrokeEvents.map((event) => ({
+ timestamp: event.timestamp,
+ type: event.type,
+ character: event.character,
+ text: normalizeText(event.text).slice(0, 500),
+ position: event.position,
+ interval: event.interval
+ }))
+ });
+
+ await versionDoc.save();
+ return versionDoc;
+}
+
+async function createEncryptedTelemetry(userId, sessionId, telemetry, sessionMetrics) {
+ const encryptionManager = new EncryptionManager(process.env.ENCRYPTION_KEY);
+
+ const encryptedDoc = new EncryptedKeystrokeData({
+ userId,
+ sessionId,
+ encryptedData: encryptionManager.encrypt({
+ intervals: telemetry.intervals,
+ pauseIntervals: telemetry.pauseIntervals,
+ pastedSegments: telemetry.pastedSegments,
+ keystrokeEvents: telemetry.keystrokeEvents
+ }),
+ anonymizedMetrics: {
+ avgKeystrokeTime: sessionMetrics.avgKeystrokeTime,
+ stdDevKeystrokeTime: sessionMetrics.stdDevKeystrokeTime,
+ pauseFrequency: telemetry.pauseIntervals.length
+ },
+ encryptionTimestamp: new Date()
+ });
+
+ await encryptedDoc.save();
+ return encryptedDoc;
+}
+
+async function createPasteDetectionLog(userId, sessionId, pasteAnalysis) {
+ const log = new PasteDetectionLog({
+ userId,
+ textSessionId: sessionId,
+ detectionResults: pasteAnalysis.detectionResults,
+ summaryAnalysis: pasteAnalysis.summaryAnalysis
+ });
+
+ await log.save();
+ return log;
+}
+
+function buildTrustSummary(sessionMetrics, pasteAnalysis, deviceSummary, biometricSummary) {
+ const significantPastedSegments = toNumber(pasteAnalysis.summaryAnalysis.significantPastedSegments);
+ const trivialPastedSegments = toNumber(pasteAnalysis.summaryAnalysis.trivialPastedSegments);
+ const ignoredPastedSegments = toNumber(pasteAnalysis.summaryAnalysis.ignoredPastedSegments);
+ const keystrokeBehavior = round((
+ clamp(100 - Math.abs(sessionMetrics.avgKeystrokeTime - 220) / 3, 0, 100) +
+ clamp(sessionMetrics.stdDevKeystrokeTime * 1.4, 0, 100) +
+ clamp(35 + sessionMetrics.deletionCount * 8 + sessionMetrics.pauseCount * 6, 0, 100)
+ ) / 3);
+ const pasteDetection = round(clamp(
+ 100 - significantPastedSegments * 35 - trivialPastedSegments * 8,
+ 0,
+ 100
+ ));
+ const aiLikelihood = round(clamp(100 - pasteAnalysis.overallAiConfidence, 0, 100));
+ const anomalyScore = round(clamp(
+ 100 - (sessionMetrics.focusLossCount * 8 + (sessionMetrics.deletionCount === 0 ? 8 : 0)),
+ 0,
+ 100
+ ));
+ const devicesConsistency = round(deviceSummary.consistencyScore);
+
+ const trustScore = round(
+ keystrokeBehavior * 0.30 +
+ pasteDetection * 0.25 +
+ aiLikelihood * 0.20 +
+ anomalyScore * 0.10 +
+ devicesConsistency * 0.15
+ );
+
+ const warnings = [];
+ if (significantPastedSegments > 0) {
+ warnings.push({
+ timestamp: new Date(),
+ message: 'Meaningful clipboard paste detected during writing session',
+ severity: 'high'
+ });
+ } else if (trivialPastedSegments > 0) {
+ warnings.push({
+ timestamp: new Date(),
+ message: 'Minor clipboard paste detected, but it looks low risk',
+ severity: 'low'
+ });
+ }
+
+ if (pasteAnalysis.overallAiConfidence >= 60) {
+ warnings.push({
+ timestamp: new Date(),
+ message: 'Typing pattern looks unusually uniform',
+ severity: 'medium'
+ });
+ }
+
+ if (!deviceSummary.isKnownDevice) {
+ warnings.push({
+ timestamp: new Date(),
+ message: 'New device profile observed',
+ severity: 'medium'
+ });
+ }
+
+ return {
+ trustScore,
+ riskLevel: trustScore < 55 ? 'high' : trustScore < 75 ? 'medium' : 'low',
+ factors: {
+ keystrokeBehavior,
+ pasteDetection,
+ aiLikelihood,
+ anomalyScore,
+ devicesConsistency
+ },
+ warnings,
+ pasteVerdict: significantPastedSegments > 0
+ ? 'meaningful_paste_detected'
+ : trivialPastedSegments > 0
+ ? 'minor_paste_activity'
+ : ignoredPastedSegments > 0
+ ? 'formatting_paste_ignored'
+ : 'no_direct_paste_detected',
+ aiVerdict: pasteAnalysis.overallAiConfidence >= 70
+ ? 'high_ai_suspicion'
+ : pasteAnalysis.overallAiConfidence >= 50
+ ? 'medium_ai_suspicion'
+ : 'human_like_behavior',
+ biometricStatus: biometricSummary.profileStatus,
+ deviceTrustScore: deviceSummary.trustScore
+ };
+}
+
+async function createTrustScore(userId, sessionId, trustSummary) {
+ const trustDoc = new SessionTrustScore({
+ userId,
+ textSessionId: sessionId,
+ scoreHistory: [{
+ timestamp: new Date(),
+ score: trustSummary.trustScore,
+ factors: trustSummary.factors
+ }],
+ currentScore: trustSummary.trustScore,
+ riskLevel: trustSummary.riskLevel,
+ warnings: trustSummary.warnings
+ });
+
+ await trustDoc.save();
+ return trustDoc;
+}
+
+function buildReportFindings(sessionMetrics, linguisticMetrics, trustSummary, deviceSummary, biometricSummary) {
+ return [
+ {
+ category: 'Behavior',
+ result: `Average keystroke time ${sessionMetrics.avgKeystrokeTime} ms with ${sessionMetrics.pauseCount} pauses and ${sessionMetrics.deletionCount} deletions.`,
+ confidence: trustSummary.factors.keystrokeBehavior
+ },
+ {
+ category: 'Paste Detection',
+ result: `Verdict: ${trustSummary.pasteVerdict.replaceAll('_', ' ')}.`,
+ confidence: trustSummary.factors.pasteDetection
+ },
+ {
+ category: 'AI Suspicion',
+ result: `Verdict: ${trustSummary.aiVerdict.replaceAll('_', ' ')}.`,
+ confidence: 100 - trustSummary.factors.aiLikelihood
+ },
+ {
+ category: 'Device',
+ result: `Device ${deviceSummary.isKnownDevice ? 'matches' : 'does not match'} existing profile. Trust score ${deviceSummary.trustScore}.`,
+ confidence: deviceSummary.consistencyScore
+ },
+ {
+ category: 'Biometric Profile',
+ result: `Profile status ${biometricSummary.profileStatus} with verification ${biometricSummary.verificationStatus}.`,
+ confidence: biometricSummary.verificationScore
+ },
+ {
+ category: 'Linguistics',
+ result: `Vocabulary diversity ${linguisticMetrics.vocabularyDiversity}% across ${linguisticMetrics.wordCount} words.`,
+ confidence: clamp(linguisticMetrics.vocabularyDiversity, 0, 100)
+ }
+ ];
+}
+
+async function createReport(userId, sessionId, content, sessionMetrics, linguisticMetrics, trustSummary, deviceSummary, biometricSummary) {
+ const report = new Report({
+ userId,
+ textSessionId: sessionId,
+ reportContent: {
+ title: `Vi-Notes Session Report - ${new Date().toLocaleDateString()}`,
+ summary: `Trust score ${trustSummary.trustScore}/100 for "${normalizeText(content).slice(0, 60)}"`,
+ trustScore: trustSummary.trustScore,
+ analysisDate: new Date(),
+ findings: buildReportFindings(sessionMetrics, linguisticMetrics, trustSummary, deviceSummary, biometricSummary)
+ },
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
+ });
+
+ await report.save();
+ return report;
+}
+
+function buildTextSessionPayload(userId, session) {
+ return {
+ userId,
+ content: session.content,
+ startTime: session.startTime,
+ endTime: session.endTime,
+ duration: session.duration,
+ pasteCount: session.pasteCount,
+ pastedTextLength: session.pastedTextLength,
+ totalKeystrokes: session.totalKeystrokes,
+ sessionMetrics: session.sessionMetrics,
+ linguisticMetrics: session.linguisticMetrics,
+ deviceSnapshot: session.deviceSnapshot
+ };
+}
+
+function buildSessionContext(body = {}) {
+ const content = normalizeText(body.content);
+ const startTime = body.startTime ? new Date(body.startTime) : new Date();
+ const endTime = body.endTime ? new Date(body.endTime) : new Date();
+ const duration = toNumber(body.duration, Math.max(1, (endTime - startTime) / 1000));
+ const totalKeystrokes = toNumber(body.totalKeystrokes);
+ const telemetry = normalizeTelemetry(body.telemetry);
+ const pasteCount = telemetry.pastedSegments.length || toNumber(body.pasteCount);
+ const pastedTextLength = telemetry.pastedSegments.length
+ ? telemetry.pastedSegments.reduce((sum, segment) => sum + toNumber(segment.length), 0)
+ : toNumber(body.pastedTextLength);
+ const deviceSnapshot = buildDeviceSnapshot(telemetry.deviceInfo);
+ const linguisticMetrics = buildLinguisticMetrics(content);
+ const sessionMetrics = buildSessionMetrics(
+ content,
+ duration,
+ totalKeystrokes,
+ pasteCount,
+ telemetry,
+ linguisticMetrics
+ );
+
+ return {
+ content,
+ startTime,
+ endTime,
+ duration,
+ pasteCount,
+ pastedTextLength,
+ totalKeystrokes,
+ telemetry,
+ deviceSnapshot,
+ linguisticMetrics,
+ sessionMetrics
+ };
+}
+
+async function runSessionAnalysis(userId, textSession, sessionData) {
+ const { content, telemetry, sessionMetrics, linguisticMetrics, deviceSnapshot, pasteCount } = sessionData;
+ let stage = 'paste-analysis';
+
+ try {
+ const pasteAnalysis = buildPasteAnalysis(content, telemetry, sessionMetrics, linguisticMetrics);
+
+ stage = 'device-profile';
+ const deviceSummary = await upsertDeviceProfile(userId, deviceSnapshot, sessionMetrics, pasteCount);
+
+ stage = 'biometric-profile';
+ const biometricSummary = await upsertBiometricProfile(userId, deviceSnapshot, sessionMetrics);
+
+ stage = 'authentication-event';
+ const authEvent = await createAuthenticationEvent(userId, textSession._id, deviceSnapshot, sessionMetrics, biometricSummary);
+
+ stage = 'paste-log';
+ const pasteLog = await createPasteDetectionLog(userId, textSession._id, pasteAnalysis);
+
+ stage = 'writing-version';
+ const writingVersion = await createWritingVersion(userId, textSession._id, content, telemetry);
+
+ stage = 'encrypted-telemetry';
+ const encryptedTelemetry = await createEncryptedTelemetry(userId, textSession._id, telemetry, sessionMetrics);
+
+ stage = 'trust-score';
+ const trustSummary = buildTrustSummary(sessionMetrics, pasteAnalysis, deviceSummary, biometricSummary);
+ const trustScoreDoc = await createTrustScore(userId, textSession._id, trustSummary);
+
+ stage = 'report';
+ const report = await createReport(
+ userId,
+ textSession._id,
+ content,
+ sessionMetrics,
+ linguisticMetrics,
+ trustSummary,
+ deviceSummary,
+ biometricSummary
+ );
+
+ const artifactRefs = {
+ authenticationEventId: String(authEvent._id),
+ deviceProfileId: deviceSummary.profileId,
+ biometricProfileId: biometricSummary.profileId,
+ pasteLogId: String(pasteLog._id),
+ writingVersionId: String(writingVersion._id),
+ encryptedTelemetryId: String(encryptedTelemetry._id),
+ trustScoreId: String(trustScoreDoc._id),
+ reportId: String(report._id)
+ };
+
+ return {
+ pasteAnalysis,
+ deviceSummary,
+ biometricSummary,
+ trustSummary,
+ report,
+ artifactRefs,
+ records: {
+ textId: String(textSession._id),
+ authEventId: artifactRefs.authenticationEventId,
+ pasteLogId: artifactRefs.pasteLogId,
+ writingVersionId: artifactRefs.writingVersionId,
+ encryptedTelemetryId: artifactRefs.encryptedTelemetryId,
+ trustScoreId: artifactRefs.trustScoreId,
+ reportId: artifactRefs.reportId
+ },
+ textSummary: {
+ pasteVerdict: trustSummary.pasteVerdict,
+ aiVerdict: trustSummary.aiVerdict,
+ trustScore: trustSummary.trustScore,
+ riskLevel: trustSummary.riskLevel,
+ biometricStatus: biometricSummary.profileStatus,
+ deviceTrustScore: deviceSummary.trustScore,
+ reportToken: report.sharingToken
+ }
+ };
+ } catch (err) {
+ err.message = `Analysis failed at ${stage}: ${err.message}`;
+ throw err;
+ }
+}
+
+async function fetchSessionArtifacts(userId, session) {
+ const artifactRefs = session?.artifactRefs || {};
+ const sessionId = session?._id;
+ const deviceId = session?.deviceSnapshot?.deviceId;
+
+ const authEventsPromise = artifactRefs.authenticationEventId
+ ? AuthenticationEvent.find({ _id: artifactRefs.authenticationEventId, userId }).lean()
+ : AuthenticationEvent.find({ userId, sessionId: String(sessionId) }).lean();
+
+ const deviceProfilesPromise = artifactRefs.deviceProfileId
+ ? DeviceProfile.find({ _id: artifactRefs.deviceProfileId, userId }).lean()
+ : deviceId
+ ? DeviceProfile.find({ userId, deviceId }).lean()
+ : DeviceProfile.find({ userId }).lean();
+
+ const [authEvents, biometricProfile, deviceProfiles, encryptedTelemetry, pasteLog, report, trustScore, writingVersion] = await Promise.all([
+ authEventsPromise,
+ artifactRefs.biometricProfileId
+ ? BiometricProfile.findOne({ _id: artifactRefs.biometricProfileId, userId }).lean()
+ : BiometricProfile.findOne({ userId }).lean(),
+ deviceProfilesPromise,
+ artifactRefs.encryptedTelemetryId
+ ? EncryptedKeystrokeData.findOne({ _id: artifactRefs.encryptedTelemetryId, userId }).lean()
+ : EncryptedKeystrokeData.findOne({ sessionId }).lean(),
+ artifactRefs.pasteLogId
+ ? PasteDetectionLog.findOne({ _id: artifactRefs.pasteLogId, userId }).lean()
+ : PasteDetectionLog.findOne({ textSessionId: sessionId }).lean(),
+ artifactRefs.reportId
+ ? Report.findOne({ _id: artifactRefs.reportId, userId }).lean()
+ : Report.findOne({ textSessionId: sessionId }).lean(),
+ artifactRefs.trustScoreId
+ ? SessionTrustScore.findOne({ _id: artifactRefs.trustScoreId, userId }).lean()
+ : SessionTrustScore.findOne({ textSessionId: sessionId }).lean(),
+ artifactRefs.writingVersionId
+ ? WritingVersion.findOne({ _id: artifactRefs.writingVersionId, userId }).lean()
+ : WritingVersion.findOne({ textSessionId: sessionId }).lean()
+ ]);
+
+ return {
+ authEvents,
+ biometricProfile,
+ deviceProfiles,
+ encryptedTelemetry,
+ pasteLog,
+ report,
+ trustScore,
+ writingVersion
+ };
+}
+
+module.exports = {
+ buildSessionContext,
+ buildTextSessionPayload,
+ fetchSessionArtifacts,
+ runSessionAnalysis
+};