import * as plugins from '../plugins.js'; import { PassportChallenge } from './classes.passportchallenge.js'; import { PassportDevice } from './classes.passportdevice.js'; import { PassportNonce } from './classes.passportnonce.js'; import { logger } from './logging.js'; import { Reception } from './classes.reception.js'; export class PassportManager { private readonly enrollmentChallengeMillis = plugins.smarttime.getMilliSecondsFromUnits({ minutes: 10, }); private readonly assertionChallengeMillis = plugins.smarttime.getMilliSecondsFromUnits({ minutes: 5, }); private readonly deviceRequestWindowMillis = plugins.smarttime.getMilliSecondsFromUnits({ minutes: 5, }); public receptionRef: Reception; public get db() { return this.receptionRef.db.smartdataDb; } public typedRouter = new plugins.typedrequest.TypedRouter(); public CPassportDevice = plugins.smartdata.setDefaultManagerForDoc(this, PassportDevice); public CPassportChallenge = plugins.smartdata.setDefaultManagerForDoc(this, PassportChallenge); public CPassportNonce = plugins.smartdata.setDefaultManagerForDoc(this, PassportNonce); constructor(receptionRefArg: Reception) { this.receptionRef = receptionRefArg; this.receptionRef.typedrouter.addTypedRouter(this.typedRouter); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createPassportEnrollmentChallenge', async (requestArg) => { const userId = await this.getAuthenticatedUserId(requestArg.jwt); const enrollmentChallenge = await this.createEnrollmentChallengeForUser(userId, { deviceLabel: requestArg.deviceLabel, platform: requestArg.platform, appVersion: requestArg.appVersion, capabilities: requestArg.capabilities, }); return { challengeId: enrollmentChallenge.challenge.id, pairingToken: enrollmentChallenge.pairingToken, pairingPayload: enrollmentChallenge.pairingPayload, signingPayload: enrollmentChallenge.signingPayload, expiresAt: enrollmentChallenge.challenge.data.expiresAt, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'completePassportEnrollment', async (requestArg) => { const passportDevice = await this.completeEnrollment({ pairingToken: requestArg.pairingToken, deviceLabel: requestArg.deviceLabel, platform: requestArg.platform, publicKeyX963Base64: requestArg.publicKeyX963Base64, signatureBase64: requestArg.signatureBase64, signatureFormat: requestArg.signatureFormat, appVersion: requestArg.appVersion, capabilities: requestArg.capabilities, }); return { device: { id: passportDevice.id, data: passportDevice.data, }, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getPassportDevices', async (requestArg) => { const userId = await this.getAuthenticatedUserId(requestArg.jwt); const devices = await this.getPassportDevicesForUser(userId); return { devices: devices.map((deviceArg) => ({ id: deviceArg.id, data: deviceArg.data, })), }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'revokePassportDevice', async (requestArg) => { const userId = await this.getAuthenticatedUserId(requestArg.jwt); await this.revokePassportDeviceForUser(userId, requestArg.deviceId); return { success: true, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createPassportChallenge', async (requestArg) => { const userId = await this.getAuthenticatedUserId(requestArg.jwt); const challengeResult = await this.createPassportChallengeForUser(userId, { type: requestArg.type, preferredDeviceId: requestArg.preferredDeviceId, audience: requestArg.audience, notificationTitle: requestArg.notificationTitle, requireLocation: requestArg.requireLocation, requireNfc: requestArg.requireNfc, }); return { challengeId: challengeResult.challenge.id, challenge: challengeResult.challenge.data.challenge, signingPayload: challengeResult.signingPayload, deviceId: challengeResult.challenge.data.deviceId!, expiresAt: challengeResult.challenge.data.expiresAt, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getPassportDashboard', async (requestArg) => { const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, { action: 'getPassportDashboard', }); const user = await this.receptionRef.userManager.CUser.getInstance({ id: passportDevice.data.userId, }); const organizations = user ? await this.receptionRef.organizationmanager.getAllOrganizationsForUser(user) : []; const devices = await this.getPassportDevicesForUser(passportDevice.data.userId); const challenges = await this.listPendingChallengesForDevice(passportDevice.id); const alerts = await this.receptionRef.alertManager.listAlertsForUser(passportDevice.data.userId); return { profile: { userId: passportDevice.data.userId, name: user?.data?.name || user?.data?.email || 'Passport User', handle: user?.data?.username || user?.data?.email || passportDevice.data.userId, organizations: organizations.map((organizationArg) => ({ id: organizationArg.id, name: organizationArg.data.name, })), deviceCount: devices.length, recoverySummary: 'Recovery workflows are not configured yet for this passport.', }, devices: devices.map((deviceArg) => ({ id: deviceArg.id, data: deviceArg.data })), challenges: challenges.map((challengeArg) => ({ challenge: { id: challengeArg.id, data: challengeArg.data }, signingPayload: this.buildChallengeSigningPayload(challengeArg), })), alerts: alerts.map((alertArg) => ({ id: alertArg.id, data: alertArg.data })), }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'approvePassportChallenge', async (requestArg) => { const passportChallenge = await this.approvePassportChallenge({ challengeId: requestArg.challengeId, deviceId: requestArg.deviceId, signatureBase64: requestArg.signatureBase64, signatureFormat: requestArg.signatureFormat, location: requestArg.location, nfc: requestArg.nfc, }); return { success: true, challenge: { id: passportChallenge.id, data: passportChallenge.data, }, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'rejectPassportChallenge', async (requestArg) => { const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, { action: 'rejectPassportChallenge', signedFields: [`challenge_id=${requestArg.challengeId}`], }); const challenge = await this.rejectPassportChallenge(passportDevice.id, requestArg.challengeId); return { success: true, challenge: { id: challenge.id, data: challenge.data, }, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'registerPassportPushToken', async (requestArg) => { const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, { action: 'registerPassportPushToken', signedFields: [ `provider=${requestArg.provider}`, `token=${requestArg.token}`, `topic=${requestArg.topic}`, `environment=${requestArg.environment}`, ], }); passportDevice.data.pushRegistration = { provider: requestArg.provider, token: requestArg.token, topic: requestArg.topic, environment: requestArg.environment, registeredAt: Date.now(), lastDeliveredAt: passportDevice.data.pushRegistration?.lastDeliveredAt, lastError: undefined, }; passportDevice.data.lastSeenAt = Date.now(); await passportDevice.save(); return { success: true, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'listPendingPassportChallenges', async (requestArg) => { const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, { action: 'listPendingPassportChallenges', }); const challenges = await this.listPendingChallengesForDevice(passportDevice.id); return { challenges: challenges.map((challengeArg) => ({ id: challengeArg.id, data: challengeArg.data, })), }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getPassportChallengeByHint', async (requestArg) => { const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, { action: 'getPassportChallengeByHint', signedFields: [`hint_id=${requestArg.hintId}`], }); const passportChallenge = await this.getPassportChallengeByHint(passportDevice.id, requestArg.hintId); return { challenge: passportChallenge ? { challenge: { id: passportChallenge.id, data: passportChallenge.data, }, signingPayload: this.buildChallengeSigningPayload(passportChallenge), } : undefined, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'markPassportChallengeSeen', async (requestArg) => { const passportDevice = await this.authenticatePassportDeviceRequest(requestArg, { action: 'markPassportChallengeSeen', signedFields: [`hint_id=${requestArg.hintId}`], }); await this.markPassportChallengeSeen(passportDevice.id, requestArg.hintId); return { success: true, }; } ) ); } private async getAuthenticatedUserId(jwtArg: string) { const jwt = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtArg); if (!jwt) { throw new plugins.typedrequest.TypedResponseError('Invalid JWT'); } return jwt.data.userId; } private getOriginHost() { return new URL(this.receptionRef.options.baseUrl).host; } private createOpaqueToken(prefixArg: string) { return `${prefixArg}${plugins.crypto.randomBytes(32).toString('base64url')}`; } private buildDeviceRequestSigningPayload( requestArg: plugins.idpInterfaces.request.IPassportDeviceSignedRequest, actionArg: string, signedFieldsArg: string[] = [] ) { return [ 'purpose=passport-device-request', `origin=${this.getOriginHost()}`, `action=${actionArg}`, `device_id=${requestArg.deviceId}`, `timestamp=${requestArg.timestamp}`, `nonce=${requestArg.nonce}`, ...signedFieldsArg, ].join('\n'); } private async consumePassportNonce(deviceIdArg: string, nonceArg: string, timestampArg: number) { const now = Date.now(); if (Math.abs(now - timestampArg) > this.deviceRequestWindowMillis) { throw new plugins.typedrequest.TypedResponseError('Passport device request timestamp expired'); } const existingNonce = await this.CPassportNonce.getInstance({ id: PassportNonce.hashNonce(`${deviceIdArg}:${nonceArg}`), }); if (existingNonce && !existingNonce.isExpired(now)) { throw new plugins.typedrequest.TypedResponseError('Passport device request replay detected'); } const passportNonce = existingNonce || new PassportNonce(); passportNonce.id = PassportNonce.hashNonce(`${deviceIdArg}:${nonceArg}`); passportNonce.data = { deviceId: deviceIdArg, nonceHash: PassportNonce.hashNonce(nonceArg), createdAt: now, expiresAt: now + this.deviceRequestWindowMillis, }; await passportNonce.save(); } public async authenticatePassportDeviceRequest( requestArg: plugins.idpInterfaces.request.IPassportDeviceSignedRequest, optionsArg: { action: string; signedFields?: string[]; } ) { const passportDevice = await this.CPassportDevice.getInstance({ id: requestArg.deviceId, 'data.status': 'active', }); if (!passportDevice) { throw new plugins.typedrequest.TypedResponseError('Passport device not found'); } const verified = this.verifyPassportSignature( passportDevice.data.publicKeyX963Base64, requestArg.signatureBase64, requestArg.signatureFormat || 'raw', this.buildDeviceRequestSigningPayload( requestArg, optionsArg.action, optionsArg.signedFields || [] ) ); if (!verified) { throw new plugins.typedrequest.TypedResponseError('Passport device signature invalid'); } await this.consumePassportNonce(requestArg.deviceId, requestArg.nonce, requestArg.timestamp); passportDevice.data.lastSeenAt = Date.now(); await passportDevice.save(); return passportDevice; } private normalizeCapabilities( capabilitiesArg?: Partial ): plugins.idpInterfaces.data.IPassportCapabilities { return { gps: !!capabilitiesArg?.gps, nfc: !!capabilitiesArg?.nfc, push: !!capabilitiesArg?.push, }; } private buildEnrollmentSigningPayload(pairingTokenArg: string, challengeArg: PassportChallenge) { return [ 'purpose=passport-enrollment', `origin=${this.getOriginHost()}`, `token=${pairingTokenArg}`, `challenge=${challengeArg.data.challenge}`, `challenge_id=${challengeArg.id}`, ].join('\n'); } private buildChallengeSigningPayload(challengeArg: PassportChallenge) { return [ 'purpose=passport-challenge', `origin=${this.getOriginHost()}`, `challenge=${challengeArg.data.challenge}`, `challenge_id=${challengeArg.id}`, `type=${challengeArg.data.type}`, `device_id=${challengeArg.data.deviceId || ''}`, `audience=${challengeArg.data.metadata.audience || ''}`, `require_location=${challengeArg.data.metadata.requireLocation}`, `require_nfc=${challengeArg.data.metadata.requireNfc}`, `location_policy=${challengeArg.data.metadata.locationPolicy ? JSON.stringify(challengeArg.data.metadata.locationPolicy) : ''}`, ].join('\n'); } private evaluateLocationPolicy( locationPolicyArg: plugins.idpInterfaces.data.IPassportLocationPolicy, locationEvidenceArg: plugins.idpInterfaces.data.IPassportLocationEvidence ) { const earthRadiusMeters = 6371000; const latitude1 = (locationPolicyArg.latitude * Math.PI) / 180; const latitude2 = (locationEvidenceArg.latitude * Math.PI) / 180; const deltaLatitude = ((locationEvidenceArg.latitude - locationPolicyArg.latitude) * Math.PI) / 180; const deltaLongitude = ((locationEvidenceArg.longitude - locationPolicyArg.longitude) * Math.PI) / 180; const haversine = Math.sin(deltaLatitude / 2) * Math.sin(deltaLatitude / 2) + Math.cos(latitude1) * Math.cos(latitude2) * Math.sin(deltaLongitude / 2) * Math.sin(deltaLongitude / 2); const distanceMeters = 2 * earthRadiusMeters * Math.atan2(Math.sqrt(haversine), Math.sqrt(1 - haversine)); const accuracyAccepted = !locationPolicyArg.maxAccuracyMeters || locationEvidenceArg.accuracyMeters <= locationPolicyArg.maxAccuracyMeters; const withinGeofence = distanceMeters <= locationPolicyArg.radiusMeters; return { matched: accuracyAccepted && withinGeofence, distanceMeters, accuracyAccepted, evaluatedAt: Date.now(), reason: !accuracyAccepted ? `Accuracy ${locationEvidenceArg.accuracyMeters}m exceeds allowed ${locationPolicyArg.maxAccuracyMeters}m` : !withinGeofence ? `Location is ${Math.round(distanceMeters)}m away from ${locationPolicyArg.label || 'required area'}` : undefined, }; } private createPairingPayload( pairingTokenArg: string, challengeArg: PassportChallenge, deviceLabelArg: string ) { const searchParams = new URLSearchParams({ token: pairingTokenArg, challenge: challengeArg.data.challenge, challenge_id: challengeArg.id, origin: this.getOriginHost(), device: deviceLabelArg, }); return `idp.global://pair?${searchParams.toString()}`; } private createP256JwkFromX963(publicKeyX963Base64Arg: string) { const rawPublicKey = Buffer.from(publicKeyX963Base64Arg, 'base64'); if (rawPublicKey.length !== 65 || rawPublicKey[0] !== 4) { throw new plugins.typedrequest.TypedResponseError('Invalid passport public key'); } return { kty: 'EC', crv: 'P-256', x: rawPublicKey.subarray(1, 33).toString('base64url'), y: rawPublicKey.subarray(33, 65).toString('base64url'), ext: true, } as JsonWebKey; } private verifyPassportSignature( publicKeyX963Base64Arg: string, signatureBase64Arg: string, signatureFormatArg: plugins.idpInterfaces.data.TPassportSignatureFormat, payloadArg: string ) { const publicKey = plugins.crypto.createPublicKey({ key: this.createP256JwkFromX963(publicKeyX963Base64Arg), format: 'jwk', }); const signature = Buffer.from(signatureBase64Arg, 'base64'); const payload = Buffer.from(payloadArg, 'utf8'); return signatureFormatArg === 'raw' ? plugins.crypto.verify('sha256', payload, { key: publicKey, dsaEncoding: 'ieee-p1363' }, signature) : plugins.crypto.verify('sha256', payload, publicKey, signature); } public async createEnrollmentChallengeForUser( userIdArg: string, optionsArg: { deviceLabel: string; platform: plugins.idpInterfaces.data.TPassportDevicePlatform; appVersion?: string; capabilities?: Partial; } ) { const pairingToken = this.createOpaqueToken('passport_pair_'); const passportChallenge = new PassportChallenge(); passportChallenge.id = plugins.smartunique.shortId(); passportChallenge.data = { userId: userIdArg, deviceId: null, type: 'device_enrollment', status: 'pending', tokenHash: PassportChallenge.hashToken(pairingToken), challenge: this.createOpaqueToken('challenge_'), metadata: { originHost: this.getOriginHost(), deviceLabel: optionsArg.deviceLabel, requireLocation: false, requireNfc: false, locationPolicy: undefined, requestedCapabilities: this.normalizeCapabilities(optionsArg.capabilities), }, evidence: undefined, notification: undefined, createdAt: Date.now(), expiresAt: Date.now() + this.enrollmentChallengeMillis, completedAt: null, }; await passportChallenge.save(); return { challenge: passportChallenge, pairingToken, pairingPayload: this.createPairingPayload( pairingToken, passportChallenge, optionsArg.deviceLabel ), signingPayload: this.buildEnrollmentSigningPayload(pairingToken, passportChallenge), }; } public async completeEnrollment(optionsArg: { pairingToken: string; deviceLabel: string; platform: plugins.idpInterfaces.data.TPassportDevicePlatform; publicKeyX963Base64: string; signatureBase64: string; signatureFormat?: plugins.idpInterfaces.data.TPassportSignatureFormat; appVersion?: string; capabilities?: Partial; }) { const passportChallenge = await this.CPassportChallenge.getInstance({ 'data.tokenHash': PassportChallenge.hashToken(optionsArg.pairingToken), 'data.type': 'device_enrollment', 'data.status': 'pending', }); if (!passportChallenge) { throw new plugins.typedrequest.TypedResponseError('Pairing token not found'); } if (passportChallenge.isExpired()) { await passportChallenge.markExpired(); throw new plugins.typedrequest.TypedResponseError('Pairing token expired'); } const existingPassportDevice = await this.CPassportDevice.getInstance({ 'data.publicKeyX963Base64': optionsArg.publicKeyX963Base64, 'data.status': 'active', }); if (existingPassportDevice) { throw new plugins.typedrequest.TypedResponseError('Passport device already enrolled'); } const verified = this.verifyPassportSignature( optionsArg.publicKeyX963Base64, optionsArg.signatureBase64, optionsArg.signatureFormat || 'raw', this.buildEnrollmentSigningPayload(optionsArg.pairingToken, passportChallenge) ); if (!verified) { throw new plugins.typedrequest.TypedResponseError('Passport signature invalid'); } const passportDevice = new PassportDevice(); passportDevice.id = plugins.smartunique.shortId(); passportDevice.data = { userId: passportChallenge.data.userId, label: optionsArg.deviceLabel, platform: optionsArg.platform, status: 'active', publicKeyAlgorithm: 'p256', publicKeyX963Base64: optionsArg.publicKeyX963Base64, capabilities: this.normalizeCapabilities( optionsArg.capabilities || passportChallenge.data.metadata.requestedCapabilities ), pushRegistration: undefined, appVersion: optionsArg.appVersion, createdAt: Date.now(), lastSeenAt: Date.now(), lastChallengeAt: undefined, }; await passportDevice.save(); passportChallenge.data.deviceId = passportDevice.id; passportChallenge.data.tokenHash = null; await passportChallenge.markApproved({ signatureFormat: optionsArg.signatureFormat || 'raw', }); await this.receptionRef.activityLogManager.logActivity( passportChallenge.data.userId, 'passport_device_enrolled', `Enrolled passport device ${passportDevice.data.label}`, { targetId: passportDevice.id, targetType: 'passport-device', } ); return passportDevice; } public async getPassportDevicesForUser(userIdArg: string) { const devices = await this.CPassportDevice.getInstances({ 'data.userId': userIdArg, 'data.status': 'active', }); return devices.sort( (leftArg, rightArg) => (rightArg.data.lastSeenAt || rightArg.data.createdAt) - (leftArg.data.lastSeenAt || leftArg.data.createdAt) ); } public async revokePassportDeviceForUser(userIdArg: string, deviceIdArg: string) { const passportDevice = await this.CPassportDevice.getInstance({ id: deviceIdArg, 'data.userId': userIdArg, 'data.status': 'active', }); if (!passportDevice) { throw new plugins.typedrequest.TypedResponseError('Passport device not found'); } passportDevice.data.status = 'revoked'; await passportDevice.save(); await this.receptionRef.activityLogManager.logActivity( userIdArg, 'passport_device_revoked', `Revoked passport device ${passportDevice.data.label}`, { targetId: passportDevice.id, targetType: 'passport-device', } ); } public async createPassportChallengeForUser( userIdArg: string, optionsArg: { type?: Exclude; preferredDeviceId?: string; audience?: string; notificationTitle?: string; requireLocation?: boolean; requireNfc?: boolean; locationPolicy?: plugins.idpInterfaces.data.IPassportLocationPolicy; } ) { const passportDevices = await this.getPassportDevicesForUser(userIdArg); if (passportDevices.length === 0) { throw new plugins.typedrequest.TypedResponseError('No passport device enrolled'); } const targetDevice = optionsArg.preferredDeviceId ? passportDevices.find((deviceArg) => deviceArg.id === optionsArg.preferredDeviceId) : passportDevices[0]; if (!targetDevice) { throw new plugins.typedrequest.TypedResponseError('Target passport device not found'); } const passportChallenge = new PassportChallenge(); passportChallenge.id = plugins.smartunique.shortId(); passportChallenge.data = { userId: userIdArg, deviceId: targetDevice.id, type: optionsArg.type || 'step_up', status: 'pending', tokenHash: null, challenge: this.createOpaqueToken('passport_challenge_'), metadata: { originHost: this.getOriginHost(), audience: optionsArg.audience, notificationTitle: optionsArg.notificationTitle, deviceLabel: targetDevice.data.label, requireLocation: !!optionsArg.requireLocation || !!optionsArg.locationPolicy, requireNfc: !!optionsArg.requireNfc, locationPolicy: optionsArg.locationPolicy, }, evidence: undefined, notification: { hintId: plugins.crypto.randomUUID(), status: 'pending', attemptCount: 0, createdAt: Date.now(), deliveredAt: null, seenAt: null, lastError: null, }, createdAt: Date.now(), expiresAt: Date.now() + this.assertionChallengeMillis, completedAt: null, }; await passportChallenge.save(); targetDevice.data.lastChallengeAt = Date.now(); await targetDevice.save(); await this.receptionRef.passportPushManager.deliverChallengeHint(targetDevice, passportChallenge); return { challenge: passportChallenge, signingPayload: this.buildChallengeSigningPayload(passportChallenge), }; } public async approvePassportChallenge(optionsArg: { challengeId: string; deviceId: string; signatureBase64: string; signatureFormat?: plugins.idpInterfaces.data.TPassportSignatureFormat; location?: plugins.idpInterfaces.data.IPassportLocationEvidence; nfc?: plugins.idpInterfaces.data.IPassportNfcEvidence; }) { const passportChallenge = await this.CPassportChallenge.getInstance({ id: optionsArg.challengeId, 'data.status': 'pending', }); if (!passportChallenge) { throw new plugins.typedrequest.TypedResponseError('Passport challenge not found'); } if (passportChallenge.isExpired()) { await passportChallenge.markExpired(); throw new plugins.typedrequest.TypedResponseError('Passport challenge expired'); } if (passportChallenge.data.deviceId && passportChallenge.data.deviceId !== optionsArg.deviceId) { throw new plugins.typedrequest.TypedResponseError('Passport challenge not assigned to this device'); } const passportDevice = await this.CPassportDevice.getInstance({ id: optionsArg.deviceId, 'data.status': 'active', }); if (!passportDevice) { throw new plugins.typedrequest.TypedResponseError('Passport device not found'); } if (passportDevice.data.userId !== passportChallenge.data.userId) { throw new plugins.typedrequest.TypedResponseError('Passport device user mismatch'); } if (passportChallenge.data.metadata.requireLocation && !optionsArg.location) { throw new plugins.typedrequest.TypedResponseError('Location evidence required'); } if (passportChallenge.data.metadata.requireNfc && !optionsArg.nfc) { throw new plugins.typedrequest.TypedResponseError('NFC evidence required'); } const verified = this.verifyPassportSignature( passportDevice.data.publicKeyX963Base64, optionsArg.signatureBase64, optionsArg.signatureFormat || 'raw', this.buildChallengeSigningPayload(passportChallenge) ); if (!verified) { throw new plugins.typedrequest.TypedResponseError('Passport signature invalid'); } const locationEvaluation = passportChallenge.data.metadata.locationPolicy && optionsArg.location ? this.evaluateLocationPolicy(passportChallenge.data.metadata.locationPolicy, optionsArg.location) : undefined; if (passportChallenge.data.metadata.locationPolicy && !locationEvaluation?.matched) { throw new plugins.typedrequest.TypedResponseError( locationEvaluation?.reason || 'Location evidence did not satisfy the office policy' ); } await passportChallenge.markApproved({ signatureFormat: optionsArg.signatureFormat || 'raw', location: optionsArg.location, locationEvaluation, nfc: optionsArg.nfc, }); passportDevice.data.lastSeenAt = Date.now(); await passportDevice.save(); await this.receptionRef.activityLogManager.logActivity( passportChallenge.data.userId, 'passport_challenge_approved', `Approved passport challenge ${passportChallenge.data.type}`, { targetId: passportChallenge.id, targetType: 'passport-challenge', } ); return passportChallenge; } public async rejectPassportChallenge(deviceIdArg: string, challengeIdArg: string) { const passportChallenge = await this.CPassportChallenge.getInstance({ id: challengeIdArg, 'data.deviceId': deviceIdArg, 'data.status': 'pending', }); if (!passportChallenge) { throw new plugins.typedrequest.TypedResponseError('Passport challenge not found'); } if (passportChallenge.isExpired()) { await passportChallenge.markExpired(); throw new plugins.typedrequest.TypedResponseError('Passport challenge expired'); } await passportChallenge.markRejected(); await this.receptionRef.activityLogManager.logActivity( passportChallenge.data.userId, 'passport_challenge_rejected', `Rejected passport challenge ${passportChallenge.data.type}`, { targetId: passportChallenge.id, targetType: 'passport-challenge', } ); return passportChallenge; } public async listPendingChallengesForDevice(deviceIdArg: string) { const passportChallenges = await this.CPassportChallenge.getInstances({ 'data.deviceId': deviceIdArg, 'data.status': 'pending', }); return passportChallenges.sort((leftArg, rightArg) => rightArg.data.createdAt - leftArg.data.createdAt); } public async getPassportChallengeByHint(deviceIdArg: string, hintIdArg: string) { return this.CPassportChallenge.getInstance({ 'data.deviceId': deviceIdArg, 'data.status': 'pending', 'data.notification.hintId': hintIdArg, }); } public async markPassportChallengeSeen(deviceIdArg: string, hintIdArg: string) { const passportChallenge = await this.getPassportChallengeByHint(deviceIdArg, hintIdArg); if (!passportChallenge) { throw new plugins.typedrequest.TypedResponseError('Passport challenge not found'); } passportChallenge.data.notification = { ...passportChallenge.data.notification!, status: 'seen', seenAt: Date.now(), }; await passportChallenge.save(); return passportChallenge; } public async cleanupExpiredChallenges() { const passportChallenges = await this.CPassportChallenge.getInstances({}); for (const passportChallenge of passportChallenges) { if (passportChallenge.data.status === 'pending' && passportChallenge.isExpired()) { await passportChallenge.markExpired(); } } const passportNonces = await this.CPassportNonce.getInstances({}); for (const passportNonce of passportNonces) { if (passportNonce.isExpired()) { await passportNonce.delete(); } } } public async reDeliverPendingChallengeHints() { const passportChallenges = await this.CPassportChallenge.getInstances({ 'data.status': 'pending', }); for (const passportChallenge of passportChallenges) { if (!passportChallenge.data.notification || passportChallenge.data.notification.status === 'sent') { continue; } if (!passportChallenge.data.deviceId) { continue; } const passportDevice = await this.CPassportDevice.getInstance({ id: passportChallenge.data.deviceId, 'data.status': 'active', }); if (!passportDevice) { continue; } try { await this.receptionRef.passportPushManager.deliverChallengeHint(passportDevice, passportChallenge); } catch (errorArg) { logger.log('warn', `passport hint redelivery failed: ${(errorArg as Error).message}`); } } } }