@learncard/network-brain-service 3.19.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +47 -82
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +147 -0
- package/README.md +51 -2
- package/bootstrapLambda.test.ts +72 -0
- package/config/config.dev.json +29 -0
- package/config/config.example.json +29 -0
- package/config/config.json +1 -0
- package/config/config.production.json +29 -0
- package/config/config.scouts.dev.json +24 -0
- package/config/config.scouts.production.json +24 -0
- package/contractEventsLambda.ts +18 -0
- package/contractEventsLambdaApp.ts +19 -0
- package/didWebLambda.ts +13 -18
- package/didWebLambdaApp.ts +22 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
- package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
- package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
- package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/accesslayer/presentation/read.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
- package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts +3 -0
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
- package/dist/accesslayer/share-link/attachment.d.ts +37 -0
- package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
- package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
- package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
- package/dist/accesslayer/share-link/errors.d.ts +8 -0
- package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts +3 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
- package/dist/accesslayer/share-link/helpers.d.ts +59 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
- package/dist/accesslayer/share-link/index.d.ts +12 -0
- package/dist/accesslayer/share-link/index.d.ts.map +1 -0
- package/dist/accesslayer/share-link/list.d.ts +5 -0
- package/dist/accesslayer/share-link/list.d.ts.map +1 -0
- package/dist/accesslayer/share-link/read.d.ts +5 -0
- package/dist/accesslayer/share-link/read.d.ts.map +1 -0
- package/dist/accesslayer/share-link/receipt.d.ts +11 -0
- package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
- package/dist/accesslayer/share-link/recovery.d.ts +6 -0
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
- package/dist/accesslayer/share-link/reserve.d.ts +4 -0
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
- package/dist/accesslayer/share-link/revoke.d.ts +3 -0
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction.d.ts +15 -0
- package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
- package/dist/accesslayer/share-link/types.d.ts +375 -0
- package/dist/accesslayer/share-link/types.d.ts.map +1 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +49 -0
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/environment.d.ts +14 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +112 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/constants/auth-grant.d.ts +2 -0
- package/dist/constants/auth-grant.d.ts.map +1 -1
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/helpers/app-manifest.helpers.d.ts +5 -0
- package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
- package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
- package/dist/helpers/consent-referral.helpers.d.ts +2 -0
- package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
- package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
- package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts +25 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-requests.helpers.d.ts +19 -0
- package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
- package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
- package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inline-template.helpers.d.ts +45 -0
- package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts +6 -0
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/presentation-metadata.d.ts +4 -0
- package/dist/helpers/presentation-metadata.d.ts.map +1 -0
- package/dist/helpers/presentation.helpers.d.ts +1 -1
- package/dist/helpers/presentation.helpers.d.ts.map +1 -1
- package/dist/helpers/profile.helpers.d.ts +2 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/share-content-client/adapters.d.ts +10 -0
- package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
- package/dist/helpers/share-content-client/canonical.d.ts +7 -0
- package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
- package/dist/helpers/share-content-client/client.d.ts +5 -0
- package/dist/helpers/share-content-client/client.d.ts.map +1 -0
- package/dist/helpers/share-content-client/config.d.ts +11 -0
- package/dist/helpers/share-content-client/config.d.ts.map +1 -0
- package/dist/helpers/share-content-client/index.d.ts +6 -0
- package/dist/helpers/share-content-client/index.d.ts.map +1 -0
- package/dist/helpers/share-content-client/responses.d.ts +16 -0
- package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
- package/dist/helpers/share-content-client/types.d.ts +145 -0
- package/dist/helpers/share-content-client/types.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment-access.d.ts +4 -0
- package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment.d.ts +18 -0
- package/dist/helpers/share-link-attachment.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
- package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
- package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
- package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts +20 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts +38 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/types.d.ts +56 -0
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/config.d.ts +16 -0
- package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
- package/dist/helpers/share-link-owner-projection.d.ts +4 -0
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode.d.ts +6 -0
- package/dist/helpers/share-link-passcode.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/index.d.ts +4 -0
- package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/production.d.ts +11 -0
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/types.d.ts +22 -0
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
- package/dist/helpers/share-link-public/config.d.ts +13 -0
- package/dist/helpers/share-link-public/config.d.ts.map +1 -0
- package/dist/helpers/share-link-view-notification.d.ts +2 -0
- package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/smartResume.helpers.d.ts +3 -0
- package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AppManifestVersion.d.ts +20 -0
- package/dist/models/AppManifestVersion.d.ts.map +1 -0
- package/dist/models/AppStoreListing.d.ts +13 -0
- package/dist/models/AppStoreListing.d.ts.map +1 -1
- package/dist/models/ConsentFlowContract.d.ts +13 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +2 -0
- package/dist/models/Integration.d.ts.map +1 -1
- package/dist/models/Presentation.d.ts +2 -0
- package/dist/models/Presentation.d.ts.map +1 -1
- package/dist/models/Profile.d.ts +2 -0
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +46 -0
- package/dist/models/ShareLink.d.ts.map +1 -0
- package/dist/models/ShareViewReceipt.d.ts +22 -0
- package/dist/models/ShareViewReceipt.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/share-link-constraints.d.ts +3 -0
- package/dist/models/share-link-constraints.d.ts.map +1 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/public-share-link-http.d.ts +4 -0
- package/dist/public-share-link-http.d.ts.map +1 -0
- package/dist/routes/app-store.d.ts +619 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +2 -2
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +148 -5
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +35 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +52 -2
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +153 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +1 -0
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +285 -0
- package/dist/routes/public-share-links.d.ts.map +1 -0
- package/dist/routes/share-links.d.ts +1005 -0
- package/dist/routes/share-links.d.ts.map +1 -0
- package/dist/routes/test.d.ts +5 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/types/app-manifest.d.ts +339 -0
- package/dist/types/app-manifest.d.ts.map +1 -0
- package/dist/types/app-store-listing.d.ts +34 -0
- package/dist/types/app-store-listing.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +31 -4
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +1 -1
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +49 -192
- package/lambdaApp.ts +203 -0
- package/package.json +25 -17
- package/scripts/export-openapi.ts +20 -0
- package/scripts/offline-schema-environment.ts +18 -0
- package/scripts/seed-demo-persona.ts +425 -0
- package/scripts/verify-python-contracts.ts +72 -0
- package/serverless-local.yml +3 -6
- package/serverless.function-env.cjs +50 -0
- package/serverless.local.yml +0 -1
- package/serverless.yml +44 -55
- package/shareLinkMaintenanceLambda.ts +18 -0
- package/shareLinkMaintenanceLambdaApp.ts +50 -0
- package/src/accesslayer/app-manifest-version/create.ts +57 -0
- package/src/accesslayer/app-manifest-version/read.ts +129 -0
- package/src/accesslayer/app-manifest-version/update.ts +61 -0
- package/src/accesslayer/app-store-listing/read.ts +6 -0
- package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
- package/src/accesslayer/app-store-listing/update.ts +15 -0
- package/src/accesslayer/boost/relationships/create.ts +8 -2
- package/src/accesslayer/consentflowcontract/create.ts +81 -8
- package/src/accesslayer/consentflowcontract/read.ts +84 -10
- package/src/accesslayer/consentflowcontract/relationships/create.ts +125 -109
- package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
- package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
- package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/update.ts +255 -217
- package/src/accesslayer/credential/create.test.ts +2 -115
- package/src/accesslayer/credential/create.ts +11 -6
- package/src/accesslayer/credential/relationships/create.ts +17 -10
- package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
- package/src/accesslayer/inbox-credential/update.ts +4 -2
- package/src/accesslayer/presentation/read.ts +57 -25
- package/src/accesslayer/presentation/relationships/create.ts +54 -11
- package/src/accesslayer/presentation/relationships/read.ts +21 -6
- package/src/accesslayer/share-link/abandon.ts +120 -0
- package/src/accesslayer/share-link/attachment.ts +360 -0
- package/src/accesslayer/share-link/cleanup.ts +271 -0
- package/src/accesslayer/share-link/errors.ts +32 -0
- package/src/accesslayer/share-link/finalize.ts +313 -0
- package/src/accesslayer/share-link/helpers.ts +450 -0
- package/src/accesslayer/share-link/index.ts +11 -0
- package/src/accesslayer/share-link/list.ts +162 -0
- package/src/accesslayer/share-link/read.ts +107 -0
- package/src/accesslayer/share-link/receipt.ts +548 -0
- package/src/accesslayer/share-link/recovery.ts +565 -0
- package/src/accesslayer/share-link/reserve.ts +763 -0
- package/src/accesslayer/share-link/revoke.ts +232 -0
- package/src/accesslayer/share-link/transaction-execution.ts +72 -0
- package/src/accesslayer/share-link/transaction.ts +126 -0
- package/src/accesslayer/share-link/types.ts +606 -0
- package/src/app.ts +6 -0
- package/src/cache/app-store.caches.ts +35 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/environment.test.ts +17 -12
- package/src/config/environment.ts +15 -0
- package/src/config/serverlessFunctionEnv.test.ts +105 -0
- package/src/config/stageConfig.test.ts +25 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/constants/auth-grant.ts +5 -0
- package/src/docker-entry.ts +81 -0
- package/src/helpers/app-manifest.helpers.ts +185 -0
- package/src/helpers/boost.helpers.ts +4 -31
- package/src/helpers/claim-hooks.helpers.ts +20 -7
- package/src/helpers/consent-referral.helpers.ts +6 -0
- package/src/helpers/consent-scopes.helpers.ts +58 -0
- package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
- package/src/helpers/contract-events.helpers.ts +540 -0
- package/src/helpers/contract-requests.helpers.ts +211 -0
- package/src/helpers/credentialSubject.helpers.test.ts +114 -0
- package/src/helpers/credentialSubject.helpers.ts +40 -0
- package/src/helpers/finalize-inbox.helpers.ts +163 -44
- package/src/helpers/inbox.helpers.ts +58 -70
- package/src/helpers/inline-template.helpers.ts +560 -0
- package/src/helpers/notificationMessages.ts +77 -1
- package/src/helpers/notificationQueue.helpers.ts +23 -5
- package/src/helpers/notifications.helpers.ts +25 -1
- package/src/helpers/presentation-metadata.ts +41 -0
- package/src/helpers/presentation.helpers.ts +27 -20
- package/src/helpers/profile.helpers.ts +13 -0
- package/src/helpers/share-content-client/adapters.ts +55 -0
- package/src/helpers/share-content-client/canonical.ts +25 -0
- package/src/helpers/share-content-client/client.ts +440 -0
- package/src/helpers/share-content-client/config.ts +189 -0
- package/src/helpers/share-content-client/index.ts +27 -0
- package/src/helpers/share-content-client/responses.ts +273 -0
- package/src/helpers/share-content-client/types.ts +253 -0
- package/src/helpers/share-link-attachment-access.ts +87 -0
- package/src/helpers/share-link-attachment.ts +115 -0
- package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
- package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
- package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +727 -0
- package/src/helpers/share-link-coordinator/index.ts +8 -0
- package/src/helpers/share-link-coordinator/recovery-runner.ts +443 -0
- package/src/helpers/share-link-coordinator/runtime.ts +88 -0
- package/src/helpers/share-link-coordinator/stat.ts +30 -0
- package/src/helpers/share-link-coordinator/types.ts +353 -0
- package/src/helpers/share-link-lifecycle/index.ts +4 -0
- package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
- package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
- package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
- package/src/helpers/share-link-lifecycle/types.ts +58 -0
- package/src/helpers/share-link-maintenance/budget.ts +63 -0
- package/src/helpers/share-link-maintenance/config.ts +312 -0
- package/src/helpers/share-link-maintenance/index.ts +24 -0
- package/src/helpers/share-link-maintenance/runner.ts +251 -0
- package/src/helpers/share-link-maintenance/runtime.ts +340 -0
- package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +73 -0
- package/src/helpers/share-link-maintenance/types.ts +91 -0
- package/src/helpers/share-link-owner/config.ts +48 -0
- package/src/helpers/share-link-owner/diagnostics.ts +89 -0
- package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
- package/src/helpers/share-link-owner-projection.ts +54 -0
- package/src/helpers/share-link-passcode-abuse.ts +36 -0
- package/src/helpers/share-link-passcode.ts +47 -0
- package/src/helpers/share-link-policy/index.ts +7 -0
- package/src/helpers/share-link-policy/production.ts +108 -0
- package/src/helpers/share-link-policy/resolver.ts +113 -0
- package/src/helpers/share-link-policy/types.ts +56 -0
- package/src/helpers/share-link-public/config.ts +41 -0
- package/src/helpers/share-link-view-notification.ts +18 -0
- package/src/helpers/signingAuthority.helpers.ts +23 -14
- package/src/helpers/smartResume.helpers.ts +123 -0
- package/src/models/AppManifestVersion.ts +43 -0
- package/src/models/AppStoreListing.ts +32 -0
- package/src/models/ConsentFlowContract.ts +34 -3
- package/src/models/ConsentFlowTerms.ts +10 -0
- package/src/models/ConsentFlowTransaction.ts +3 -0
- package/src/models/Integration.ts +2 -0
- package/src/models/Presentation.ts +2 -2
- package/src/models/Profile.ts +3 -2
- package/src/models/ShareLink.ts +134 -0
- package/src/models/ShareViewReceipt.ts +69 -0
- package/src/models/index.ts +25 -0
- package/src/models/share-link-constraints.ts +98 -0
- package/src/openapi.ts +44 -8
- package/src/public-share-link-http.ts +49 -0
- package/src/routes/app-store.ts +1056 -38
- package/src/routes/boosts.ts +14 -25
- package/src/routes/contracts.ts +384 -273
- package/src/routes/inbox.ts +4 -2
- package/src/routes/index.ts +142 -44
- package/src/routes/presentations.ts +19 -3
- package/src/routes/profile-manager.ts +29 -2
- package/src/routes/profiles.ts +29 -6
- package/src/routes/public-share-links.ts +983 -0
- package/src/routes/share-links.ts +709 -0
- package/src/routes/workflows.ts +3 -2
- package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
- package/src/types/app-manifest.ts +28 -0
- package/src/types/app-store-listing.ts +13 -0
- package/src/types/consentflowcontract.ts +25 -4
- package/test/app-manifest.helpers.spec.ts +170 -0
- package/test/app-manifest.routes.spec.ts +299 -0
- package/test/app-store-listing-updates.spec.ts +236 -0
- package/test/app-store-listing.spec.ts +65 -3
- package/test/consent-data-boundaries.spec.ts +352 -0
- package/test/consent-recipients.spec.ts +513 -0
- package/test/consent-scopes.helpers.spec.ts +67 -0
- package/test/consentflow.spec.ts +63 -2
- package/test/contract-requests.spec.ts +1588 -0
- package/test/credential-storage.neo4j.spec.ts +122 -0
- package/test/dids.spec.ts +10 -1
- package/test/guardian-gated-routes.spec.ts +49 -0
- package/test/helpers/share-link-fixtures.ts +94 -0
- package/test/inbox-security.e2e.ts +26 -10
- package/test/inbox.spec.ts +184 -8
- package/test/inline-template.helpers.spec.ts +49 -0
- package/test/managed-recipient-inbox.spec.ts +152 -0
- package/test/notificationMessages.spec.ts +13 -0
- package/test/openapi.spec.ts +14 -53
- package/test/presentations.spec.ts +54 -6
- package/test/profiles.spec.ts +97 -1
- package/test/share-content-client.contract.spec.ts +365 -0
- package/test/share-content-client.unit.spec.ts +462 -0
- package/test/share-link-abuse.unit.spec.ts +65 -0
- package/test/share-link-attachment.neo4j.spec.ts +262 -0
- package/test/share-link-attachment.unit.spec.ts +244 -0
- package/test/share-link-coordinator.neo4j.spec.ts +684 -0
- package/test/share-link-coordinator.unit.spec.ts +990 -0
- package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +1411 -0
- package/test/share-link-lifecycle.unit.spec.ts +205 -0
- package/test/share-link-list-cursor.unit.spec.ts +86 -0
- package/test/share-link-list.neo4j.spec.ts +324 -0
- package/test/share-link-maintenance.neo4j.spec.ts +355 -0
- package/test/share-link-maintenance.unit.spec.ts +1140 -0
- package/test/share-link-owner-policy.unit.spec.ts +210 -0
- package/test/share-link-owner-route.unit.spec.ts +820 -0
- package/test/share-link-owner-runtime.unit.spec.ts +102 -0
- package/test/share-link-passcode.unit.spec.ts +29 -0
- package/test/share-link-policy.unit.spec.ts +149 -0
- package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
- package/test/share-link-production-policy.unit.spec.ts +126 -0
- package/test/share-link-public-config.unit.spec.ts +64 -0
- package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
- package/test/share-link-public-route.unit.spec.ts +1079 -0
- package/test/share-link-receipt.neo4j.spec.ts +943 -0
- package/test/share-link-recovery.neo4j.spec.ts +935 -0
- package/test/share-link-recovery.unit.spec.ts +726 -0
- package/test/share-link-service-policy.neo4j.spec.ts +403 -0
- package/test/signing-authority-encryption.spec.ts +75 -0
- package/test/smart-resume-consent.spec.ts +233 -0
- package/test/smart-resume-upload.spec.ts +65 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +14 -0
- package/vitest.inbox-e2e.config.ts +2 -0
- package/vitest.share-links-neo4j.config.ts +19 -0
- package/vitest.share-links-unit.config.ts +28 -0
- package/workerLambda.test.ts +100 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { VCValidator, type BitstringStatusListEntry } from '@learncard/types';
|
|
3
|
+
import { Credential, Profile, StatusList } from '@models';
|
|
4
|
+
import { storeCredential } from '../src/accesslayer/credential/create';
|
|
5
|
+
import { getCredentialById } from '../src/accesslayer/credential/read';
|
|
6
|
+
import { setCredentialBitstringStatus, decodeBitstring } from '../src/helpers/status-list.helpers';
|
|
7
|
+
import { getUser } from './helpers/getClient';
|
|
8
|
+
import { app as statusListsApp } from '../src/status-lists';
|
|
9
|
+
|
|
10
|
+
let issuer: Awaited<ReturnType<typeof getUser>>;
|
|
11
|
+
const credentialIds: string[] = [];
|
|
12
|
+
|
|
13
|
+
const isStatusBitSet = async (entry: BitstringStatusListEntry): Promise<boolean> => {
|
|
14
|
+
const id = entry.statusListCredential.split('/').pop();
|
|
15
|
+
if (!id) throw new Error('Missing status list id');
|
|
16
|
+
const response = await statusListsApp.inject({ method: 'GET', url: `/status-lists/${id}` });
|
|
17
|
+
expect(response.statusCode).toBe(200);
|
|
18
|
+
const list = JSON.parse(response.body);
|
|
19
|
+
const bytes = decodeBitstring(list.credentialSubject.encodedList, 131_072);
|
|
20
|
+
const index = Number(entry.statusListIndex);
|
|
21
|
+
return ((bytes[Math.floor(index / 8)] ?? 0) & (1 << (index % 8))) !== 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('credential status storage in Neo4j', () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
issuer = await getUser();
|
|
27
|
+
await issuer.clients.fullAuth.profile.createProfile({ profileId: 'storage-issuer' });
|
|
28
|
+
await statusListsApp.ready();
|
|
29
|
+
});
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
for (const id of credentialIds) await Credential.delete({ where: { id } });
|
|
32
|
+
await Profile.delete({ detach: true, where: { profileId: 'storage-issuer' } });
|
|
33
|
+
await StatusList.delete({ detach: true, where: { ownerProfileId: 'storage-issuer' } });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('persists trusted public status descriptors for revocation consumers without decrypting the payload', async () => {
|
|
37
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
38
|
+
statusPurposes: ['revocation'],
|
|
39
|
+
});
|
|
40
|
+
if (!entry) throw new Error('Expected allocated revocation entry');
|
|
41
|
+
const signed = await issuer.learnCard.invoke.issueCredential({
|
|
42
|
+
...(await issuer.learnCard.invoke.getTestVc()),
|
|
43
|
+
credentialStatus: entry,
|
|
44
|
+
});
|
|
45
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
46
|
+
const stored = await storeCredential({
|
|
47
|
+
kind: 'issued-credential',
|
|
48
|
+
credential: jwe,
|
|
49
|
+
statusEntries: [entry],
|
|
50
|
+
});
|
|
51
|
+
credentialIds.push(stored.id);
|
|
52
|
+
|
|
53
|
+
const row = await getCredentialById(stored.id);
|
|
54
|
+
expect(row?.credential).toBe(JSON.stringify(jwe));
|
|
55
|
+
expect(JSON.parse(row?.statusEntries ?? 'null')).toEqual([entry]);
|
|
56
|
+
expect(await isStatusBitSet(entry)).toBe(false);
|
|
57
|
+
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(true);
|
|
58
|
+
expect(await isStatusBitSet(entry)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each(['embedded', 'absent'] as const)(
|
|
62
|
+
'preserves a valid signed wire VC and proof with %s status',
|
|
63
|
+
async statusCase => {
|
|
64
|
+
const unsigned = await issuer.learnCard.invoke.getTestVc();
|
|
65
|
+
if (statusCase === 'embedded') {
|
|
66
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
67
|
+
statusPurposes: ['revocation'],
|
|
68
|
+
});
|
|
69
|
+
unsigned.credentialStatus = entry;
|
|
70
|
+
}
|
|
71
|
+
const signed = await issuer.learnCard.invoke.issueCredential(unsigned);
|
|
72
|
+
const stored = await storeCredential(signed);
|
|
73
|
+
credentialIds.push(stored.id);
|
|
74
|
+
const row = await getCredentialById(stored.id);
|
|
75
|
+
const recovered = VCValidator.parse(JSON.parse(row?.credential ?? 'null'));
|
|
76
|
+
expect(recovered).toEqual(signed);
|
|
77
|
+
expect(
|
|
78
|
+
await issuer.learnCard.invoke.verifyCredential(recovered, { checks: ['proof'] })
|
|
79
|
+
).toMatchObject({
|
|
80
|
+
checks: expect.arrayContaining(['proof']),
|
|
81
|
+
warnings: [],
|
|
82
|
+
errors: [],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
it('does not promote wire JWE metadata to trusted status descriptors', async () => {
|
|
88
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
89
|
+
statusPurposes: ['revocation'],
|
|
90
|
+
});
|
|
91
|
+
if (!entry) throw new Error('Expected allocated revocation entry');
|
|
92
|
+
const signed = await issuer.learnCard.invoke.issueCredential({
|
|
93
|
+
...(await issuer.learnCard.invoke.getTestVc()),
|
|
94
|
+
credentialStatus: entry,
|
|
95
|
+
});
|
|
96
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
97
|
+
const wire = { ...jwe, kind: 'issued-credential', statusEntries: [entry] };
|
|
98
|
+
const stored = await storeCredential(wire);
|
|
99
|
+
credentialIds.push(stored.id);
|
|
100
|
+
const row = await getCredentialById(stored.id);
|
|
101
|
+
expect(row?.credential).toBe(JSON.stringify(wire));
|
|
102
|
+
expect(row?.statusEntries).toBeUndefined();
|
|
103
|
+
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
|
|
104
|
+
expect(await isStatusBitSet(entry)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does not persist or apply an empty trusted status descriptor set', async () => {
|
|
108
|
+
const signed = await issuer.learnCard.invoke.issueCredential(
|
|
109
|
+
await issuer.learnCard.invoke.getTestVc()
|
|
110
|
+
);
|
|
111
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
112
|
+
const stored = await storeCredential({
|
|
113
|
+
kind: 'issued-credential',
|
|
114
|
+
credential: jwe,
|
|
115
|
+
statusEntries: [],
|
|
116
|
+
});
|
|
117
|
+
credentialIds.push(stored.id);
|
|
118
|
+
const row = await getCredentialById(stored.id);
|
|
119
|
+
expect(row?.statusEntries).toBeUndefined();
|
|
120
|
+
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
package/test/dids.spec.ts
CHANGED
|
@@ -168,7 +168,7 @@ describe('did:web profile document generation with signing authorities', () => {
|
|
|
168
168
|
await deleteDidDocForProfile(profileId);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
it('
|
|
171
|
+
it('renders controller-owned verification and key-agreement methods', async () => {
|
|
172
172
|
const profile = await getProfileByProfileId(profileId);
|
|
173
173
|
const sa = await createSigningAuthority(SA_ENDPOINT);
|
|
174
174
|
await createUseSigningAuthorityRelationship(profile!, sa, 'lca-sa', SA_DID, true);
|
|
@@ -195,6 +195,15 @@ describe('did:web profile document generation with signing authorities', () => {
|
|
|
195
195
|
);
|
|
196
196
|
expect(saVm).toBeDefined();
|
|
197
197
|
expect((saVm as { controller: string }).controller).toBe(did);
|
|
198
|
+
|
|
199
|
+
const ownerKeyAgreement = (doc.keyAgreement ?? []).find(
|
|
200
|
+
method => typeof method === 'object' && method.controller === did
|
|
201
|
+
);
|
|
202
|
+
expect(ownerKeyAgreement).toMatchObject({
|
|
203
|
+
type: 'X25519KeyAgreementKey2019',
|
|
204
|
+
controller: did,
|
|
205
|
+
publicKeyBase58: expect.any(String),
|
|
206
|
+
});
|
|
198
207
|
});
|
|
199
208
|
|
|
200
209
|
it('de-duplicates repeated signing-authority relationships in the rendered document', async () => {
|
|
@@ -216,6 +216,21 @@ describe('Guardian-approved consent mutations', () => {
|
|
|
216
216
|
expect(record?.terms.guardianApproval).toBeUndefined();
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
it('preserves the manager-based gate for explicitly typed profiles without managers', async () => {
|
|
220
|
+
await Profile.update(
|
|
221
|
+
{ type: 'child', isServiceProfile: true },
|
|
222
|
+
{ where: { profileId: 'child-user' } }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const { termsUri } = await childCaller().contracts.consentToContract({
|
|
226
|
+
contractUri,
|
|
227
|
+
terms: minimalTerms,
|
|
228
|
+
});
|
|
229
|
+
const record = await getContractTermsByUri(termsUri);
|
|
230
|
+
expect(record?.terms.status).toBe('live');
|
|
231
|
+
expect(record?.terms.guardianApproval).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
219
234
|
it('does not create consent or an audit transaction without guardian approval', async () => {
|
|
220
235
|
await manageChild();
|
|
221
236
|
await expect(
|
|
@@ -225,6 +240,40 @@ describe('Guardian-approved consent mutations', () => {
|
|
|
225
240
|
expect(await ConsentFlowTransaction.findMany({ where: {} })).toEqual([]);
|
|
226
241
|
});
|
|
227
242
|
|
|
243
|
+
it('does not guardian-gate a managed service profile', async () => {
|
|
244
|
+
await Profile.update({ isServiceProfile: true }, { where: { profileId: 'child-user' } });
|
|
245
|
+
await manageChild();
|
|
246
|
+
|
|
247
|
+
const { termsUri } = await childCaller().contracts.consentToContract({
|
|
248
|
+
contractUri,
|
|
249
|
+
terms: minimalTerms,
|
|
250
|
+
});
|
|
251
|
+
const record = await getContractTermsByUri(termsUri);
|
|
252
|
+
expect(record?.terms.status).toBe('live');
|
|
253
|
+
expect(record?.terms.guardianApproval).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('requires guardian approval for legacy service-flagged children', async () => {
|
|
257
|
+
await Profile.update(
|
|
258
|
+
{ type: 'child', isServiceProfile: true },
|
|
259
|
+
{ where: { profileId: 'child-user' } }
|
|
260
|
+
);
|
|
261
|
+
await manageChild();
|
|
262
|
+
|
|
263
|
+
await expect(
|
|
264
|
+
childCaller().contracts.consentToContract({ contractUri, terms: minimalTerms })
|
|
265
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
266
|
+
|
|
267
|
+
const { termsUri } = await childCaller(await approve()).contracts.consentToContract({
|
|
268
|
+
contractUri,
|
|
269
|
+
terms: minimalTerms,
|
|
270
|
+
});
|
|
271
|
+
expect((await getContractTermsByUri(termsUri))?.terms.guardianApproval).toMatchObject({
|
|
272
|
+
guardianProfileId: 'guardian-user',
|
|
273
|
+
guardianDid: guardian.id.did(),
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
228
277
|
it('records the verified guardian and contract version on consent and permission updates', async () => {
|
|
229
278
|
await manageChild();
|
|
230
279
|
const beforeApproval = Date.now();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import { neogma } from '@instance';
|
|
4
|
+
import { computeShareLinkRequestHash } from '@helpers/share-link-lifecycle';
|
|
5
|
+
import { reserveCreate } from '../../src/accesslayer/share-link';
|
|
6
|
+
|
|
7
|
+
export const nextShareId = (): string => randomBytes(16).toString('base64url');
|
|
8
|
+
|
|
9
|
+
export const contentBinding = (seed: string) => ({
|
|
10
|
+
contentHash: createHash('sha256').update(`content:${seed}`).digest('hex'),
|
|
11
|
+
contentBytes: 128,
|
|
12
|
+
recoveryHash: createHash('sha256').update(`recovery:${seed}`).digest('hex'),
|
|
13
|
+
recoveryBytes: 256,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const clearLifecycleGraph = async (): Promise<void> => {
|
|
17
|
+
await neogma.queryRunner.run(
|
|
18
|
+
`MATCH (n)
|
|
19
|
+
WHERE n:ShareLink OR n:ShareLinkReservation OR n:ShareLinkOperation OR n:ShareContentCleanupJob
|
|
20
|
+
DETACH DELETE n`
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ReservedCreate = {
|
|
25
|
+
namespace: string;
|
|
26
|
+
ownerProfileId: string;
|
|
27
|
+
shareId: string;
|
|
28
|
+
clientRequestId: string;
|
|
29
|
+
requestHash: string;
|
|
30
|
+
operationId: string;
|
|
31
|
+
objectRef: string;
|
|
32
|
+
generation: number;
|
|
33
|
+
leaseOwner: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const reserveShareFixture = async (options: {
|
|
37
|
+
ownerProfileId: string;
|
|
38
|
+
shareId?: string;
|
|
39
|
+
expiresAt?: string | null;
|
|
40
|
+
selectedCount?: number;
|
|
41
|
+
title?: string;
|
|
42
|
+
leaseMs?: number;
|
|
43
|
+
now: Date;
|
|
44
|
+
clientRequestId?: string;
|
|
45
|
+
requestHash?: string;
|
|
46
|
+
namespace: string;
|
|
47
|
+
}): Promise<ReservedCreate> => {
|
|
48
|
+
const namespace = options.namespace;
|
|
49
|
+
const ownerProfileId = options.ownerProfileId;
|
|
50
|
+
const shareId = options.shareId ?? nextShareId();
|
|
51
|
+
const clientRequestId = options.clientRequestId ?? uuid();
|
|
52
|
+
const selectedCount = options.selectedCount ?? 1;
|
|
53
|
+
const content = contentBinding(shareId);
|
|
54
|
+
const requestHash =
|
|
55
|
+
options.requestHash ??
|
|
56
|
+
computeShareLinkRequestHash('create', {
|
|
57
|
+
id: shareId,
|
|
58
|
+
title: options.title ?? 'Shared credentials',
|
|
59
|
+
selectedCount,
|
|
60
|
+
...content,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await reserveCreate({
|
|
64
|
+
namespace,
|
|
65
|
+
ownerProfileId,
|
|
66
|
+
clientRequestId,
|
|
67
|
+
shareId,
|
|
68
|
+
title: options.title ?? 'Shared credentials',
|
|
69
|
+
note: null,
|
|
70
|
+
expiresAt: options.expiresAt ?? null,
|
|
71
|
+
selectedCount,
|
|
72
|
+
content,
|
|
73
|
+
requestHash,
|
|
74
|
+
leaseOwner: 'worker-1',
|
|
75
|
+
leaseMs: options.leaseMs,
|
|
76
|
+
now: options.now,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (result.outcome !== 'reserved' || !result.reservation.objectRef) {
|
|
80
|
+
throw new Error(`expected reserved create, got ${result.outcome}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
namespace,
|
|
85
|
+
ownerProfileId,
|
|
86
|
+
shareId,
|
|
87
|
+
clientRequestId,
|
|
88
|
+
requestHash,
|
|
89
|
+
operationId: result.reservation.operationId,
|
|
90
|
+
objectRef: result.reservation.objectRef,
|
|
91
|
+
generation: result.reservation.generation,
|
|
92
|
+
leaseOwner: result.reservation.leaseOwner,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -3,7 +3,12 @@ import Fastify, { type FastifyInstance } from 'fastify';
|
|
|
3
3
|
import { fastifyTRPCOpenApiPlugin } from 'trpc-to-openapi';
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
JWEValidator,
|
|
8
|
+
type IssueInboxCredentialType,
|
|
9
|
+
type UnsignedVC,
|
|
10
|
+
type VC,
|
|
11
|
+
} from '@learncard/types';
|
|
7
12
|
import { getBitstringStatusListEntries } from '@learncard/helpers';
|
|
8
13
|
|
|
9
14
|
import { appRouter, createContext } from '../src/app';
|
|
@@ -380,7 +385,8 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
380
385
|
expect(result.deliveries).toEqual([
|
|
381
386
|
{ id: issued.issuanceId, credential: result.verifiableCredentials[0] },
|
|
382
387
|
]);
|
|
383
|
-
|
|
388
|
+
const encrypted = JWEValidator.parse(result.verifiableCredentials[0]);
|
|
389
|
+
expect(await recipient.learnCard.invoke.decryptDagJwe(encrypted)).toMatchObject({
|
|
384
390
|
type: ['VerifiableCredential', 'ClrCredential'],
|
|
385
391
|
proof: expect.anything(),
|
|
386
392
|
credentialSubject: {
|
|
@@ -397,7 +403,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
397
403
|
});
|
|
398
404
|
|
|
399
405
|
it.each(['finalize', 'claim link', 'claim without profile'] as const)(
|
|
400
|
-
'recovers exactly one
|
|
406
|
+
'recovers exactly one authorized delivery after discarding the %s response',
|
|
401
407
|
async path => {
|
|
402
408
|
const credential = await signedCredential();
|
|
403
409
|
const issued = await issue({
|
|
@@ -439,11 +445,11 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
439
445
|
expect(
|
|
440
446
|
(await service.invoke.decryptDagJwe(recovery.credential).catch(() => null)) || null
|
|
441
447
|
).toBeNull();
|
|
442
|
-
|
|
448
|
+
const issuerRead =
|
|
443
449
|
(await issuer.learnCard.invoke
|
|
444
450
|
.decryptDagJwe(recovery.credential)
|
|
445
|
-
.catch(() => null)) || null
|
|
446
|
-
).
|
|
451
|
+
.catch(() => null)) || null;
|
|
452
|
+
expect(issuerRead).toEqual(path === 'finalize' ? credential : null);
|
|
447
453
|
expect((await post('/api/inbox/deliveries', {})).status).toBe(401);
|
|
448
454
|
expect(
|
|
449
455
|
(await (await post('/api/inbox/deliveries', {}, issuer)).json()).records
|
|
@@ -510,9 +516,14 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
510
516
|
await verifyRecipientContact('idless@example.test');
|
|
511
517
|
const response = await post('/api/inbox/finalize', {}, recipient);
|
|
512
518
|
expect(response.status).toBe(200);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
]);
|
|
519
|
+
const { deliveries } = await response.json();
|
|
520
|
+
expect(deliveries).toHaveLength(1);
|
|
521
|
+
expect(deliveries[0].id).toBe(issued.issuanceId);
|
|
522
|
+
expect(
|
|
523
|
+
await recipient.learnCard.invoke.decryptDagJwe(
|
|
524
|
+
JWEValidator.parse(deliveries[0].credential)
|
|
525
|
+
)
|
|
526
|
+
).toEqual(credential);
|
|
516
527
|
} else {
|
|
517
528
|
expect(await claim(issued.claimUrl!)).toEqual([credential]);
|
|
518
529
|
}
|
|
@@ -893,7 +904,12 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
893
904
|
expect(config).toMatch(
|
|
894
905
|
/inboxMaintenance:\s+handler: lambda\.inboxMaintenanceHandler[\s\S]*?rate: rate\(1 day\)\s+enabled: true/
|
|
895
906
|
);
|
|
896
|
-
|
|
907
|
+
// lambda.ts is a thin bootstrap entry that delegates to the lazily imported lambdaApp.ts.
|
|
908
|
+
const entry = await readFile(new URL('../lambda.ts', import.meta.url), 'utf8');
|
|
909
|
+
expect(entry).toMatch(
|
|
910
|
+
/export const inboxMaintenanceHandler[\s\S]*?\(await getApplication\(\)\)\.inboxMaintenanceHandler\(/
|
|
911
|
+
);
|
|
912
|
+
const handler = await readFile(new URL('../lambdaApp.ts', import.meta.url), 'utf8');
|
|
897
913
|
expect(handler).toMatch(/inboxMaintenanceHandler[\s\S]*?await runInboxMaintenance\(\)/);
|
|
898
914
|
});
|
|
899
915
|
});
|
package/test/inbox.spec.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
VC,
|
|
9
9
|
VP,
|
|
10
10
|
JWE,
|
|
11
|
+
JWEValidator,
|
|
11
12
|
} from '@learncard/types';
|
|
12
13
|
import { sendSpy, addNotificationToQueueSpy } from './helpers/spies';
|
|
13
14
|
import * as notifications from '@helpers/notifications.helpers';
|
|
@@ -16,6 +17,9 @@ import { getProfileByProfileId } from '@accesslayer/profile/read';
|
|
|
16
17
|
import { areProfilesConnected } from '@helpers/connection.helpers';
|
|
17
18
|
import { updateInboxCredential } from '@accesslayer/inbox-credential/update';
|
|
18
19
|
import { neogma } from '@instance';
|
|
20
|
+
import { Credential } from '@models';
|
|
21
|
+
import { getInboxCredentialById } from '@accesslayer/inbox-credential/read';
|
|
22
|
+
import * as credentialRelationships from '@accesslayer/credential/relationships/create';
|
|
19
23
|
|
|
20
24
|
const noAuthClient = getClient();
|
|
21
25
|
let userA: Awaited<ReturnType<typeof getUser>>;
|
|
@@ -1832,6 +1836,174 @@ describe('Universal Inbox', () => {
|
|
|
1832
1836
|
await SigningAuthority.delete({ detach: true, where: {} });
|
|
1833
1837
|
});
|
|
1834
1838
|
|
|
1839
|
+
const preparePendingBoost = async (email: string) => {
|
|
1840
|
+
await Profile.relateTo({
|
|
1841
|
+
alias: 'usesSigningAuthority',
|
|
1842
|
+
where: {
|
|
1843
|
+
source: { profileId: 'usera' },
|
|
1844
|
+
target: { endpoint: 'https://example.com' },
|
|
1845
|
+
},
|
|
1846
|
+
properties: { name: 'example', did: userA.learnCard.id.did() },
|
|
1847
|
+
});
|
|
1848
|
+
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
1849
|
+
credential: testUnsignedBoost,
|
|
1850
|
+
claimPermissions: { canRevoke: true },
|
|
1851
|
+
});
|
|
1852
|
+
const issued = await userA.clients.fullAuth.inbox.issue({
|
|
1853
|
+
templateUri: boostUri,
|
|
1854
|
+
recipient: { type: 'email', value: email },
|
|
1855
|
+
configuration: {
|
|
1856
|
+
signingAuthority: { endpoint: 'https://example.com', name: 'example' },
|
|
1857
|
+
webhookUrl: 'https://example.com/finalized',
|
|
1858
|
+
},
|
|
1859
|
+
});
|
|
1860
|
+
await updateInboxCredential(issued.issuanceId, { isAccepted: true });
|
|
1861
|
+
await userB.clients.fullAuth.contactMethods.addContactMethod({
|
|
1862
|
+
type: 'email',
|
|
1863
|
+
value: email,
|
|
1864
|
+
});
|
|
1865
|
+
await userB.clients.fullAuth.contactMethods.verifyContactMethod({
|
|
1866
|
+
token: sendSpy.mock.calls.at(-1)?.[0].templateModel.verificationToken,
|
|
1867
|
+
});
|
|
1868
|
+
addNotificationToQueueSpy.mockClear();
|
|
1869
|
+
return { boostUri, inboxId: issued.issuanceId };
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
it('rolls back a failed received write and completes all delivery tracking on retry', async () => {
|
|
1873
|
+
const { boostUri, inboxId } = await preparePendingBoost('atomic-receipt@test.com');
|
|
1874
|
+
const escrow = (await getInboxCredentialById(inboxId))!.credential;
|
|
1875
|
+
const originalCredentialIds = (await Credential.findMany()).map(record => record.id);
|
|
1876
|
+
const failedWrite = vi
|
|
1877
|
+
.spyOn(credentialRelationships, 'createReceivedCredentialRelationship')
|
|
1878
|
+
.mockRejectedValueOnce(new Error('injected received write failure'));
|
|
1879
|
+
try {
|
|
1880
|
+
await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
|
|
1881
|
+
claimed: 0,
|
|
1882
|
+
errors: 1,
|
|
1883
|
+
deliveries: [],
|
|
1884
|
+
});
|
|
1885
|
+
} finally {
|
|
1886
|
+
failedWrite.mockRestore();
|
|
1887
|
+
}
|
|
1888
|
+
expect(await getInboxCredentialById(inboxId)).toMatchObject({
|
|
1889
|
+
currentStatus: 'PENDING',
|
|
1890
|
+
credential: escrow,
|
|
1891
|
+
});
|
|
1892
|
+
expect((await Credential.findMany()).map(record => record.id).sort()).toEqual(
|
|
1893
|
+
originalCredentialIds.sort()
|
|
1894
|
+
);
|
|
1895
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toEqual([]);
|
|
1896
|
+
const auditBeforeRetry = await neogma.queryRunner.run(
|
|
1897
|
+
'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
|
|
1898
|
+
{ id: inboxId }
|
|
1899
|
+
);
|
|
1900
|
+
expect(auditBeforeRetry.records[0].get('count').toNumber()).toBe(0);
|
|
1901
|
+
expect(
|
|
1902
|
+
addNotificationToQueueSpy.mock.calls.some(
|
|
1903
|
+
([notification]) =>
|
|
1904
|
+
notification.type ===
|
|
1905
|
+
LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR &&
|
|
1906
|
+
notification.data?.inbox?.status === 'PENDING'
|
|
1907
|
+
)
|
|
1908
|
+
).toBe(true);
|
|
1909
|
+
addNotificationToQueueSpy.mockClear();
|
|
1910
|
+
|
|
1911
|
+
const retry = await userB.clients.fullAuth.inbox.finalize({});
|
|
1912
|
+
expect(retry).toMatchObject({ claimed: 1, errors: 0, deliveries: [{ id: inboxId }] });
|
|
1913
|
+
const finalized = await getInboxCredentialById(inboxId);
|
|
1914
|
+
expect(finalized?.currentStatus).toBe('ISSUED');
|
|
1915
|
+
expect(finalized?.credential).toBeUndefined();
|
|
1916
|
+
expect(
|
|
1917
|
+
await userA.clients.fullAuth.boost.getBoostRecipients({
|
|
1918
|
+
uri: boostUri,
|
|
1919
|
+
includeUnacceptedBoosts: false,
|
|
1920
|
+
})
|
|
1921
|
+
).toHaveLength(1);
|
|
1922
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toHaveLength(1);
|
|
1923
|
+
const auditAfterRetry = await neogma.queryRunner.run(
|
|
1924
|
+
'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
|
|
1925
|
+
{ id: inboxId }
|
|
1926
|
+
);
|
|
1927
|
+
expect(auditAfterRetry.records[0].get('count').toNumber()).toBe(1);
|
|
1928
|
+
expect(
|
|
1929
|
+
addNotificationToQueueSpy.mock.calls.some(
|
|
1930
|
+
([notification]) =>
|
|
1931
|
+
notification.type === LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR
|
|
1932
|
+
)
|
|
1933
|
+
).toBe(false);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
it('rolls back partial claim permissions and grants hooks and the default role on retry', async () => {
|
|
1937
|
+
const { boostUri, inboxId } = await preparePendingBoost('atomic-permissions@test.com');
|
|
1938
|
+
const targetUri = await userA.clients.fullAuth.boost.createBoost({
|
|
1939
|
+
credential: testUnsignedBoost,
|
|
1940
|
+
});
|
|
1941
|
+
await userA.clients.fullAuth.claimHook.createClaimHook({
|
|
1942
|
+
hook: {
|
|
1943
|
+
type: 'GRANT_PERMISSIONS',
|
|
1944
|
+
data: { claimUri: boostUri, targetUri, permissions: { canIssue: true } },
|
|
1945
|
+
},
|
|
1946
|
+
});
|
|
1947
|
+
const failedRole = vi
|
|
1948
|
+
.spyOn(credentialRelationships, 'setDefaultClaimedRole')
|
|
1949
|
+
.mockRejectedValueOnce(new Error('injected claimed-role failure'));
|
|
1950
|
+
try {
|
|
1951
|
+
await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
|
|
1952
|
+
claimed: 0,
|
|
1953
|
+
errors: 1,
|
|
1954
|
+
});
|
|
1955
|
+
} finally {
|
|
1956
|
+
failedRole.mockRestore();
|
|
1957
|
+
}
|
|
1958
|
+
expect(await getInboxCredentialById(inboxId)).toMatchObject({
|
|
1959
|
+
currentStatus: 'PENDING',
|
|
1960
|
+
});
|
|
1961
|
+
expect(
|
|
1962
|
+
(await userB.clients.fullAuth.boost.getBoostPermissions({ uri: targetUri }))
|
|
1963
|
+
.canIssue
|
|
1964
|
+
).toBe(false);
|
|
1965
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toEqual([]);
|
|
1966
|
+
|
|
1967
|
+
await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
|
|
1968
|
+
claimed: 1,
|
|
1969
|
+
errors: 0,
|
|
1970
|
+
});
|
|
1971
|
+
expect(
|
|
1972
|
+
(await userB.clients.fullAuth.boost.getBoostPermissions({ uri: targetUri }))
|
|
1973
|
+
.canIssue
|
|
1974
|
+
).toBe(true);
|
|
1975
|
+
expect(
|
|
1976
|
+
(await userB.clients.fullAuth.boost.getBoostPermissions({ uri: boostUri }))
|
|
1977
|
+
.canRevoke
|
|
1978
|
+
).toBe(true);
|
|
1979
|
+
const [received] = await userB.clients.fullAuth.credential.receivedCredentials({});
|
|
1980
|
+
expect(
|
|
1981
|
+
await userB.clients.fullAuth.credential.acceptCredential({ uri: received.uri })
|
|
1982
|
+
).toBe(true);
|
|
1983
|
+
const roleCounts = await neogma.queryRunner.run(
|
|
1984
|
+
'MATCH (:Profile {profileId: "userb"})-[r:HAS_ROLE]->(b:Boost) WHERE b.id IN $ids RETURN b.id AS id, count(r) AS count',
|
|
1985
|
+
{ ids: [boostUri, targetUri].map(uri => uri.split(':').at(-1)) }
|
|
1986
|
+
);
|
|
1987
|
+
expect(roleCounts.records.map(record => record.get('count').toNumber())).toEqual([
|
|
1988
|
+
1, 1,
|
|
1989
|
+
]);
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
it('commits only one accepted delivery and claim audit under concurrent finalization', async () => {
|
|
1993
|
+
const { inboxId } = await preparePendingBoost('atomic-concurrent@test.com');
|
|
1994
|
+
const results = await Promise.all(
|
|
1995
|
+
Array.from({ length: 3 }, () => userB.clients.fullAuth.inbox.finalize({}))
|
|
1996
|
+
);
|
|
1997
|
+
expect(results.reduce((count, result) => count + result.claimed, 0)).toBe(1);
|
|
1998
|
+
expect(results.reduce((count, result) => count + result.errors, 0)).toBe(0);
|
|
1999
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toHaveLength(1);
|
|
2000
|
+
const audit = await neogma.queryRunner.run(
|
|
2001
|
+
'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
|
|
2002
|
+
{ id: inboxId }
|
|
2003
|
+
);
|
|
2004
|
+
expect(audit.records[0].get('count').toNumber()).toBe(1);
|
|
2005
|
+
});
|
|
2006
|
+
|
|
1835
2007
|
it('should not allow finalize without full auth', async () => {
|
|
1836
2008
|
await expect(noAuthClient.inbox.finalize({})).rejects.toMatchObject({
|
|
1837
2009
|
code: 'UNAUTHORIZED',
|
|
@@ -1912,8 +2084,11 @@ describe('Universal Inbox', () => {
|
|
|
1912
2084
|
expect(res.claimed).toBe(2);
|
|
1913
2085
|
expect(res.errors).toBe(0);
|
|
1914
2086
|
expect(res.verifiableCredentials).toHaveLength(2);
|
|
1915
|
-
//
|
|
1916
|
-
for (const
|
|
2087
|
+
// The HTTP contract is encrypted; only the holder decrypts the signed VCs.
|
|
2088
|
+
for (const encrypted of res.verifiableCredentials) {
|
|
2089
|
+
const vc = await userB.learnCard.invoke.decryptDagJwe<VC>(
|
|
2090
|
+
JWEValidator.parse(encrypted)
|
|
2091
|
+
);
|
|
1917
2092
|
expect(vc.proof).toBeDefined();
|
|
1918
2093
|
}
|
|
1919
2094
|
|
|
@@ -2089,12 +2264,13 @@ describe('Universal Inbox', () => {
|
|
|
2089
2264
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
2090
2265
|
|
|
2091
2266
|
try {
|
|
2092
|
-
await
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2267
|
+
const result = await userB.clients.fullAuth.inbox.finalize({});
|
|
2268
|
+
expect(result).toMatchObject({ processed: 1, claimed: 1, errors: 0 });
|
|
2269
|
+
expect(
|
|
2270
|
+
await userB.learnCard.invoke.decryptDagJwe<VC>(
|
|
2271
|
+
JWEValidator.parse(result.verifiableCredentials[0])
|
|
2272
|
+
)
|
|
2273
|
+
).toMatchObject({ proof: expect.anything() });
|
|
2098
2274
|
} finally {
|
|
2099
2275
|
queryRunnerSpy.mockRestore();
|
|
2100
2276
|
consoleErrorSpy.mockRestore();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeInlineTemplateHash,
|
|
5
|
+
validateInlineTemplateDataOrThrow,
|
|
6
|
+
validateInlineTemplateOrThrow,
|
|
7
|
+
} from '../src/helpers/inline-template.helpers';
|
|
8
|
+
|
|
9
|
+
describe('inline-template.helpers', () => {
|
|
10
|
+
it('throws developer-readable template validation errors', () => {
|
|
11
|
+
expect(() =>
|
|
12
|
+
validateInlineTemplateOrThrow({
|
|
13
|
+
name: '',
|
|
14
|
+
image: 'http://not-https.example.com/badge.png',
|
|
15
|
+
})
|
|
16
|
+
).toThrow(/TEMPLATE_INVALID:/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('throws strict templateData validation errors with suggestions', () => {
|
|
20
|
+
const compiled = computeInlineTemplateHash({
|
|
21
|
+
name: 'Course Completion: {{courseName}}',
|
|
22
|
+
description: 'Awarded to {{learnerName}}',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(() =>
|
|
26
|
+
validateInlineTemplateDataOrThrow(compiled.variableManifest, {
|
|
27
|
+
courseNmae: 'Broken key',
|
|
28
|
+
learnerName: 'Ada',
|
|
29
|
+
})
|
|
30
|
+
).toThrow(
|
|
31
|
+
/TEMPLATE_DATA_INVALID: .*templateData.courseName is required.*did you mean "courseName"/
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('hashes template content deterministically', () => {
|
|
36
|
+
const first = computeInlineTemplateHash({
|
|
37
|
+
name: 'Course Completion: {{courseName}}',
|
|
38
|
+
description: 'Awarded to {{learnerName}}',
|
|
39
|
+
walletSkills: [{ frameworkId: 'framework-1', id: 'skill-1' }],
|
|
40
|
+
});
|
|
41
|
+
const second = computeInlineTemplateHash({
|
|
42
|
+
name: 'Course Completion: {{courseName}}',
|
|
43
|
+
description: 'Awarded to {{learnerName}}',
|
|
44
|
+
walletSkills: [{ frameworkId: 'framework-1', id: 'skill-1' }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(first.contentHash).toBe(second.contentHash);
|
|
48
|
+
});
|
|
49
|
+
});
|