@learncard/network-brain-service 3.18.0 → 3.19.1
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 +38 -2
- package/CHANGELOG.md +118 -0
- package/README.md +42 -0
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/update.d.ts +1 -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/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 +56 -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 +370 -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/atomic-counter.test-helpers.d.ts +3 -0
- package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
- package/dist/cache/atomic-counter.test.d.ts +2 -0
- package/dist/cache/atomic-counter.test.d.ts.map +1 -0
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/config/environment.d.ts +55 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/constants/auth-grant.d.ts +2 -0
- package/dist/constants/auth-grant.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
- package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential.helpers.d.ts +2 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
- package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
- package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
- package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox.helpers.d.ts +9 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- 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 +1 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
- 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-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 +19 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -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 +53 -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 +13 -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 +16 -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/shim.d.ts +1 -1
- package/dist/helpers/shim.d.ts.map +1 -1
- package/dist/inbox-queue-local.d.ts +2 -0
- package/dist/inbox-queue-local.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AuthGrant.d.ts.map +1 -1
- package/dist/models/CredentialActivity.d.ts +1 -0
- package/dist/models/CredentialActivity.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -2
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/InboxCredential.d.ts +1 -0
- package/dist/models/InboxCredential.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 +44 -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/credential-refresh-constraints.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -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.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/act-as.test.d.ts +2 -0
- package/dist/routes/act-as.test.d.ts.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +10 -0
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +494 -0
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +2 -2
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +1 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +953 -11
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +154 -85
- 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 +230 -0
- package/dist/routes/public-share-links.d.ts.map +1 -0
- package/dist/routes/share-links.d.ts +844 -0
- package/dist/routes/share-links.d.ts.map +1 -0
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
- package/dist/services/registry/registry.factory.d.ts.map +1 -1
- package/dist/types/activity.d.ts +4 -0
- package/dist/types/activity.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +1 -1
- package/dist/types/credential-refresh.d.ts +10 -1
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/inbox-batch.d.ts +38 -0
- package/dist/types/inbox-batch.d.ts.map +1 -0
- package/inbox-e2e-setup.ts +53 -1
- package/inbox-elasticmq.conf +10 -0
- package/lambda.ts +64 -16
- package/package.json +24 -16
- package/scripts/seed-demo-persona.ts +425 -0
- package/scripts/setup-inbox-queues.ts +60 -0
- package/serverless-local.yml +2 -0
- package/serverless.yml +111 -0
- package/shareLinkMaintenanceLambda.ts +49 -0
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
- package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
- package/src/accesslayer/credential-activity/create.ts +1 -0
- package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
- package/src/accesslayer/credential-refresh/index.ts +1 -0
- package/src/accesslayer/credential-refresh/read.ts +8 -2
- package/src/accesslayer/credential-refresh/update.ts +4 -0
- package/src/accesslayer/inbox-batch/store.ts +469 -0
- package/src/accesslayer/inbox-credential/create.ts +51 -19
- package/src/accesslayer/inbox-credential/refresh.ts +221 -0
- package/src/accesslayer/inbox-credential/update.ts +1 -1
- 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 +100 -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 +265 -0
- package/src/accesslayer/share-link/helpers.ts +419 -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 +545 -0
- package/src/accesslayer/share-link/reserve.ts +684 -0
- package/src/accesslayer/share-link/revoke.ts +197 -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 +601 -0
- package/src/app.ts +6 -0
- package/src/cache/atomic-counter.test-helpers.ts +27 -0
- package/src/cache/atomic-counter.test.ts +6 -0
- package/src/cache/index.ts +25 -13
- package/src/config/environment.ts +40 -0
- package/src/constants/auth-grant.ts +5 -0
- package/src/credential-refresh.ts +5 -1
- package/src/docker-entry.ts +65 -0
- package/src/helpers/activity.helpers.ts +4 -0
- package/src/helpers/app-counter.helpers.ts +1 -1
- package/src/helpers/boost.helpers.ts +4 -31
- package/src/helpers/contact-method.helpers.test.ts +43 -0
- package/src/helpers/contact-method.helpers.ts +3 -4
- package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
- package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
- package/src/helpers/credential-refresh-email.helpers.ts +201 -0
- package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
- package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
- package/src/helpers/credential-refresh.helpers.ts +375 -99
- package/src/helpers/credential.helpers.ts +4 -1
- package/src/helpers/credentialSubject.helpers.test.ts +114 -0
- package/src/helpers/credentialSubject.helpers.ts +40 -0
- package/src/helpers/finalize-inbox.helpers.ts +29 -14
- package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
- package/src/helpers/inbox-batch-http.helpers.ts +150 -0
- package/src/helpers/inbox-batch-status.helpers.ts +11 -0
- package/src/helpers/inbox-batch.helpers.test.ts +668 -0
- package/src/helpers/inbox-batch.helpers.ts +374 -0
- package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
- package/src/helpers/inbox-queue.helpers.test.ts +253 -0
- package/src/helpers/inbox-queue.helpers.ts +372 -0
- package/src/helpers/inbox-refresh.helpers.ts +432 -0
- package/src/helpers/inbox.helpers.ts +275 -36
- package/src/helpers/notificationMessages.ts +18 -1
- package/src/helpers/notifications.helpers.ts +4 -2
- package/src/helpers/presentation-metadata.ts +41 -0
- package/src/helpers/presentation.helpers.ts +27 -20
- package/src/helpers/profile.helpers.ts +7 -0
- package/src/helpers/rateLimit.helpers.ts +2 -5
- package/src/helpers/refresh-send-intent.helpers.ts +273 -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-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 +714 -0
- package/src/helpers/share-link-coordinator/index.ts +8 -0
- package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -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 +223 -0
- package/src/helpers/share-link-maintenance/runtime.ts +334 -0
- package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
- package/src/helpers/share-link-maintenance/types.ts +89 -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 +52 -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 +88 -0
- package/src/helpers/share-link-policy/resolver.ts +108 -0
- package/src/helpers/share-link-policy/types.ts +48 -0
- package/src/helpers/share-link-public/config.ts +41 -0
- package/src/helpers/share-link-view-notification.ts +18 -0
- package/src/helpers/shim.ts +32 -31
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/CredentialActivity.ts +3 -5
- package/src/models/CredentialRefresh.ts +4 -2
- package/src/models/InboxCredential.ts +2 -0
- package/src/models/Presentation.ts +2 -2
- package/src/models/Profile.ts +3 -2
- package/src/models/ShareLink.ts +130 -0
- package/src/models/ShareViewReceipt.ts +69 -0
- package/src/models/credential-refresh-constraints.test.ts +8 -3
- package/src/models/credential-refresh-constraints.ts +5 -0
- package/src/models/index.ts +2 -0
- package/src/models/share-link-constraints.ts +93 -0
- package/src/openapi.ts +8 -0
- package/src/public-share-link-http.ts +49 -0
- package/src/routes/act-as.test.ts +239 -0
- package/src/routes/app-store.ts +4 -13
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +916 -103
- package/src/routes/contracts.ts +2 -11
- package/src/routes/credential-refreshes.test.ts +8 -2
- package/src/routes/credential-refreshes.ts +4 -1
- package/src/routes/credentials.ts +2 -1
- package/src/routes/inbox.ts +95 -49
- package/src/routes/index.ts +216 -46
- package/src/routes/presentations.ts +19 -3
- package/src/routes/profile-manager.ts +10 -2
- package/src/routes/profiles.ts +17 -6
- package/src/routes/public-share-links.ts +814 -0
- package/src/routes/share-links.ts +640 -0
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +43 -24
- package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
- package/src/services/delivery/adapters/postmark.adapter.ts +16 -1
- package/src/services/registry/registry.factory.ts +2 -1
- package/src/types/activity.ts +2 -0
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/inbox-batch.ts +46 -0
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +2 -0
- package/test/dids.spec.ts +10 -1
- package/test/guardian-gated-routes.spec.ts +13 -0
- package/test/helpers/inbox-e2e-environment.ts +6 -0
- package/test/helpers/share-link-fixtures.ts +94 -0
- package/test/inbox-batch.spec.ts +1338 -0
- package/test/inbox-refresh.spec.ts +534 -0
- package/test/managed-recipient-inbox.spec.ts +130 -0
- package/test/notificationMessages.spec.ts +13 -0
- package/test/openapi.spec.ts +61 -1
- package/test/presentations.spec.ts +54 -6
- package/test/profiles.spec.ts +67 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- 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-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 +1268 -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 +1113 -0
- package/test/share-link-owner-policy.unit.spec.ts +210 -0
- package/test/share-link-owner-route.unit.spec.ts +752 -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 +135 -0
- package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
- package/test/share-link-production-policy.unit.spec.ts +77 -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 +800 -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/vitest.config.ts +10 -0
- package/vitest.inbox-e2e.config.ts +5 -1
- package/vitest.integration.config.ts +2 -0
- package/vitest.share-links-neo4j.config.ts +19 -0
- package/vitest.share-links-unit.config.ts +28 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { TRPCError } from '@trpc/server';
|
|
3
|
+
import type { ManagedTransaction } from 'neo4j-driver';
|
|
4
|
+
import type { InboxBatchReceipt, IssueInboxCredentialBatchItemResult } from '@learncard/types';
|
|
5
|
+
import { neogma } from '@instance';
|
|
6
|
+
import { encryptInboxCredential, decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
|
|
7
|
+
import type { BatchItem, BatchJob, BatchReplayStore, InboxDispatchLease } from 'types/inbox-batch';
|
|
8
|
+
import { getInboxBatchState } from '@helpers/inbox-batch-status.helpers';
|
|
9
|
+
|
|
10
|
+
export const DAY = 86_400_000;
|
|
11
|
+
export const LEASE_MS = 360_000;
|
|
12
|
+
const CLEANUP_MARGIN_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
let readiness: Promise<void> | undefined;
|
|
15
|
+
export const ensureInboxBatchConstraints = (): Promise<void> => {
|
|
16
|
+
if (!readiness) {
|
|
17
|
+
readiness = (async () => {
|
|
18
|
+
for (const label of [
|
|
19
|
+
'InboxBatch',
|
|
20
|
+
'InboxBatchItem',
|
|
21
|
+
'InboxBatchIssuer',
|
|
22
|
+
'InboxBatchReplay',
|
|
23
|
+
]) {
|
|
24
|
+
try {
|
|
25
|
+
await neogma.queryRunner.run(
|
|
26
|
+
`CREATE CONSTRAINT ${label.toLowerCase()}_id IF NOT EXISTS FOR (n:${label}) REQUIRE n.id IS UNIQUE`
|
|
27
|
+
);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (
|
|
30
|
+
(error as { code?: string }).code !==
|
|
31
|
+
'Neo.ClientError.Schema.EquivalentSchemaRuleAlreadyExists'
|
|
32
|
+
)
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const query of [
|
|
37
|
+
'CREATE INDEX inbox_batch_request IF NOT EXISTS FOR (b:InboxBatch) ON (b.issuer, b.requestId)',
|
|
38
|
+
'CREATE INDEX inbox_batch_dispatch IF NOT EXISTS FOR (i:InboxBatchItem) ON (i.state, i.dispatchAt)',
|
|
39
|
+
'CREATE INDEX inbox_batch_recovery IF NOT EXISTS FOR (i:InboxBatchItem) ON (i.state, i.leaseUntil)',
|
|
40
|
+
'CREATE INDEX inbox_batch_replay_owner IF NOT EXISTS FOR (r:InboxBatchReplay) ON (r.itemId)',
|
|
41
|
+
'CREATE INDEX inbox_batch_completed IF NOT EXISTS FOR (b:InboxBatch) ON (b.completedAt)',
|
|
42
|
+
'CREATE INDEX inbox_batch_replay_expiry IF NOT EXISTS FOR (r:InboxBatchReplay) ON (r.expiresAt)',
|
|
43
|
+
])
|
|
44
|
+
await neogma.queryRunner.run(query);
|
|
45
|
+
})();
|
|
46
|
+
void readiness.catch(() => {
|
|
47
|
+
readiness = undefined;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return readiness;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const transaction = async <T>(fn: (tx: ManagedTransaction) => Promise<T>): Promise<T> => {
|
|
54
|
+
await ensureInboxBatchConstraints();
|
|
55
|
+
const session = neogma.driver.session();
|
|
56
|
+
try {
|
|
57
|
+
return await session.executeWrite(fn);
|
|
58
|
+
} finally {
|
|
59
|
+
await session.close();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** The issuer node serializes submission replay and quota admission in the same transaction. */
|
|
64
|
+
export const createBatchJob = async (input: {
|
|
65
|
+
issuer: string;
|
|
66
|
+
requestId?: string;
|
|
67
|
+
requestHash: string;
|
|
68
|
+
payload: string;
|
|
69
|
+
items: { replayKey: string; duplicate: boolean; correlation?: string }[];
|
|
70
|
+
limit: number;
|
|
71
|
+
}): Promise<InboxBatchReceipt> => {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const id = randomUUID();
|
|
74
|
+
return transaction(async tx => {
|
|
75
|
+
// The property write acquires Neo4j's exclusive node lock until commit. Concurrent
|
|
76
|
+
// admissions therefore cannot read quota between this statement and the update below.
|
|
77
|
+
const locked = await tx.run(
|
|
78
|
+
`MERGE (q:InboxBatchIssuer {id: $issuer})
|
|
79
|
+
ON CREATE SET q.used = 0, q.window = $now
|
|
80
|
+
SET q.lock = coalesce(q.lock, 0) + 1 RETURN q`,
|
|
81
|
+
{ issuer: input.issuer, now }
|
|
82
|
+
);
|
|
83
|
+
if (input.requestId) {
|
|
84
|
+
// Creation atomically creates at least one HAS_ITEM relationship. Requiring it here
|
|
85
|
+
// prevents a structurally incomplete batch from being returned as a valid replay.
|
|
86
|
+
const prior = await tx.run(
|
|
87
|
+
`MATCH (b:InboxBatch {issuer: $issuer, requestId: $requestId})
|
|
88
|
+
WHERE b.createdAt > $cutoff
|
|
89
|
+
MATCH (b)-[:HAS_ITEM]->(i:InboxBatchItem) RETURN b, collect(i.state) AS states`,
|
|
90
|
+
{ issuer: input.issuer, requestId: input.requestId, cutoff: now - DAY }
|
|
91
|
+
);
|
|
92
|
+
const batch = prior.records[0]?.get('b').properties;
|
|
93
|
+
if (batch) {
|
|
94
|
+
if (batch.requestHash !== input.requestHash)
|
|
95
|
+
throw new TRPCError({
|
|
96
|
+
code: 'CONFLICT',
|
|
97
|
+
message: 'requestId was used for a different batch.',
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
batchId: batch.id,
|
|
101
|
+
status: getInboxBatchState(prior.records[0]!.get('states')),
|
|
102
|
+
createdAt: new Date(Number(batch.createdAt)).toISOString(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const quota = locked.records[0]!.get('q').properties;
|
|
107
|
+
const expired = now - Number(quota.window) >= 3_600_000;
|
|
108
|
+
const used = expired ? 0 : Number(quota.used);
|
|
109
|
+
if (used + input.items.length > input.limit)
|
|
110
|
+
throw new TRPCError({
|
|
111
|
+
code: 'TOO_MANY_REQUESTS',
|
|
112
|
+
message: `${input.limit} inbox items per hour; retry after the current window expires (at most 3600 seconds).`,
|
|
113
|
+
});
|
|
114
|
+
await tx.run(
|
|
115
|
+
`MATCH (q:InboxBatchIssuer {id: $issuer}) SET q.used = $used, q.window = $window
|
|
116
|
+
CREATE (b:InboxBatch {id: $id, issuer: $issuer, createdAt: $now, requestHash: $hash, payload: $payload})
|
|
117
|
+
SET b.requestId = $requestId
|
|
118
|
+
WITH b UNWIND $items AS item
|
|
119
|
+
CREATE (b)-[:HAS_ITEM]->(:InboxBatchItem {id: item.id, batchId: $id,
|
|
120
|
+
index: item.index, replayKey: item.replayKey, duplicate: item.duplicate,
|
|
121
|
+
correlation: item.correlation,
|
|
122
|
+
state: 'QUEUED', attempts: 0, dispatchAt: 0})`,
|
|
123
|
+
{
|
|
124
|
+
issuer: input.issuer,
|
|
125
|
+
id,
|
|
126
|
+
now,
|
|
127
|
+
used: used + input.items.length,
|
|
128
|
+
window: expired ? now : Number(quota.window),
|
|
129
|
+
hash: input.requestHash,
|
|
130
|
+
payload: input.payload,
|
|
131
|
+
requestId: input.requestId ?? null,
|
|
132
|
+
items: input.items.map((item, index) => ({ ...item, index, id: `${id}:${index}` })),
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
return { batchId: id, status: 'QUEUED', createdAt: new Date(now).toISOString() };
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const readBatchJob = async (
|
|
140
|
+
id: string,
|
|
141
|
+
issuer: string
|
|
142
|
+
): Promise<{ job: BatchJob; items: BatchItem[] }> => {
|
|
143
|
+
const result = await neogma.queryRunner.run(
|
|
144
|
+
`MATCH (b:InboxBatch {id: $id, issuer: $issuer})-[:HAS_ITEM]->(i)
|
|
145
|
+
RETURN b, i ORDER BY i.index`,
|
|
146
|
+
{ id, issuer }
|
|
147
|
+
);
|
|
148
|
+
if (!result.records.length) throw new TRPCError({ code: 'NOT_FOUND' });
|
|
149
|
+
return {
|
|
150
|
+
job: result.records[0]!.get('b').properties,
|
|
151
|
+
items: result.records.map(r => r.get('i').properties),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Lock before testing ownership: concurrent SQS deliveries must not both enter issuance. */
|
|
156
|
+
export const claimBatchItem = async (
|
|
157
|
+
id: string,
|
|
158
|
+
owner: string
|
|
159
|
+
): Promise<{ job: BatchJob; item: BatchItem } | undefined> =>
|
|
160
|
+
transaction(async tx => {
|
|
161
|
+
const result = await tx.run(
|
|
162
|
+
`MATCH (b:InboxBatch)-[:HAS_ITEM]->(i:InboxBatchItem {id: $id})
|
|
163
|
+
SET i.lock = coalesce(i.lock, 0) + 1 RETURN b, i`,
|
|
164
|
+
{ id }
|
|
165
|
+
);
|
|
166
|
+
if (!result.records.length) return;
|
|
167
|
+
const item = result.records[0]!.get('i').properties as BatchItem;
|
|
168
|
+
if (item.state !== 'QUEUED') return;
|
|
169
|
+
await tx.run(
|
|
170
|
+
`MATCH (i:InboxBatchItem {id: $id}) SET i.state = 'PROCESSING', i.phase = 'PREPARING',
|
|
171
|
+
i.owner = $owner, i.leaseUntil = $lease, i.attempts = i.attempts + 1`,
|
|
172
|
+
{ id, owner, lease: Date.now() + LEASE_MS }
|
|
173
|
+
);
|
|
174
|
+
return { job: result.records[0]!.get('b').properties, item };
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const lockOwnedItem = async (
|
|
178
|
+
tx: ManagedTransaction,
|
|
179
|
+
id: string,
|
|
180
|
+
owner: string
|
|
181
|
+
): Promise<BatchItem> => {
|
|
182
|
+
const result = await tx.run(
|
|
183
|
+
`MATCH (i:InboxBatchItem {id: $id}) SET i.lock = coalesce(i.lock, 0) + 1 RETURN i`,
|
|
184
|
+
{ id }
|
|
185
|
+
);
|
|
186
|
+
const item = result.records[0]?.get('i').properties as BatchItem | undefined;
|
|
187
|
+
if (
|
|
188
|
+
!item ||
|
|
189
|
+
item.owner !== owner ||
|
|
190
|
+
item.state !== 'PROCESSING' ||
|
|
191
|
+
Number(item.leaseUntil) <= Date.now()
|
|
192
|
+
)
|
|
193
|
+
throw new Error('Inbox item lease lost');
|
|
194
|
+
return item;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const markBatchIssuanceStarted = async (id: string, owner: string): Promise<void> =>
|
|
198
|
+
transaction(async tx => {
|
|
199
|
+
await lockOwnedItem(tx, id, owner);
|
|
200
|
+
await tx.run(`MATCH (i:InboxBatchItem {id: $id}) SET i.phase = 'ISSUING'`, { id });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/** Adapter retains the existing request fingerprint/CAS protocol with durable, encrypted records.
|
|
204
|
+
* Processing markers have no expiry. Orphaned internal markers are removed after job retention;
|
|
205
|
+
* client-keyed markers remain blocked. Only confirmed successes get the 24-hour replay expiry.
|
|
206
|
+
*/
|
|
207
|
+
export const batchReplayStore = (
|
|
208
|
+
itemId: string,
|
|
209
|
+
owner: string,
|
|
210
|
+
replayKey: string
|
|
211
|
+
): BatchReplayStore => ({
|
|
212
|
+
get: async (_key: string): Promise<string | null> => {
|
|
213
|
+
const result = await neogma.queryRunner.run(
|
|
214
|
+
`MATCH (r:InboxBatchReplay {id: $id})
|
|
215
|
+
WHERE r.expiresAt IS NULL OR r.expiresAt > $now RETURN r.value AS value`,
|
|
216
|
+
{ id: replayKey, now: Date.now() }
|
|
217
|
+
);
|
|
218
|
+
const value = result.records[0]?.get('value');
|
|
219
|
+
return value ? decryptInboxCredential(value) : null;
|
|
220
|
+
},
|
|
221
|
+
setIfAbsent: async (_key: string, value: string, _ttl: number): Promise<'OK' | null> => {
|
|
222
|
+
const encrypted = await encryptInboxCredential(value);
|
|
223
|
+
return transaction(async tx => {
|
|
224
|
+
await lockOwnedItem(tx, itemId, owner);
|
|
225
|
+
const rows = await tx.run(
|
|
226
|
+
`MERGE (r:InboxBatchReplay {id: $id}) SET r.lock = coalesce(r.lock, 0) + 1 RETURN r`,
|
|
227
|
+
{ id: replayKey }
|
|
228
|
+
);
|
|
229
|
+
const replay = rows.records[0]!.get('r').properties;
|
|
230
|
+
if (replay.value && (replay.expiresAt == null || Number(replay.expiresAt) > Date.now()))
|
|
231
|
+
return null;
|
|
232
|
+
await tx.run(
|
|
233
|
+
`MATCH (r:InboxBatchReplay {id: $id}) SET r.value = $value, r.marker = $marker,
|
|
234
|
+
r.itemId = $itemId, r.expiresAt = null`,
|
|
235
|
+
{ id: replayKey, value: encrypted, marker: value, itemId }
|
|
236
|
+
);
|
|
237
|
+
return 'OK';
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
compareAndSet: async (
|
|
241
|
+
_key: string,
|
|
242
|
+
expected: string,
|
|
243
|
+
value: string | null,
|
|
244
|
+
ttl: number
|
|
245
|
+
): Promise<boolean> => {
|
|
246
|
+
const encrypted = value === null ? null : await encryptInboxCredential(value);
|
|
247
|
+
return transaction(async tx => {
|
|
248
|
+
await lockOwnedItem(tx, itemId, owner);
|
|
249
|
+
const rows = await tx.run(
|
|
250
|
+
`MATCH (r:InboxBatchReplay {id: $id}) SET r.lock = coalesce(r.lock, 0) + 1 RETURN r.marker AS marker`,
|
|
251
|
+
{ id: replayKey }
|
|
252
|
+
);
|
|
253
|
+
if (rows.records[0]?.get('marker') !== expected) return false;
|
|
254
|
+
if (value === null) {
|
|
255
|
+
await tx.run(`MATCH (r:InboxBatchReplay {id: $id}) DELETE r`, { id: replayKey });
|
|
256
|
+
// The shared helper only releases after an explicitly safe preflight failure.
|
|
257
|
+
await tx.run(`MATCH (i:InboxBatchItem {id: $itemId}) SET i.phase = 'PREPARING'`, {
|
|
258
|
+
itemId,
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
await tx.run(
|
|
262
|
+
`MATCH (r:InboxBatchReplay {id: $id}) SET r.value = $value,
|
|
263
|
+
r.marker = null, r.expiresAt = $expiry`,
|
|
264
|
+
{ id: replayKey, value: encrypted, expiry: Date.now() + ttl * 1000 }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/** Terminal items no longer need the original credential payload, including uncertain outcomes.
|
|
273
|
+
* Client-keyed replay reservations are separate nodes and survive the 30-day job retention period.
|
|
274
|
+
*/
|
|
275
|
+
const finalizeSettledBatch = async (tx: ManagedTransaction, id: string): Promise<void> => {
|
|
276
|
+
await tx.run(
|
|
277
|
+
`MATCH (b:InboxBatch {id: $id}) SET b.lock = coalesce(b.lock, 0) + 1
|
|
278
|
+
WITH b WHERE NOT EXISTS {
|
|
279
|
+
MATCH (b)-[:HAS_ITEM]->(i) WHERE i.state IN ['QUEUED', 'PROCESSING']
|
|
280
|
+
}
|
|
281
|
+
SET b.completedAt = coalesce(b.completedAt, $now) REMOVE b.payload`,
|
|
282
|
+
{ id, now: Date.now() }
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const finishBatchItem = async (
|
|
287
|
+
id: string,
|
|
288
|
+
owner: string,
|
|
289
|
+
result: IssueInboxCredentialBatchItemResult
|
|
290
|
+
): Promise<void> => {
|
|
291
|
+
const encrypted = await encryptInboxCredential(JSON.stringify(result));
|
|
292
|
+
await transaction(async tx => {
|
|
293
|
+
const item = await lockOwnedItem(tx, id, owner);
|
|
294
|
+
const unconfirmed = !result.success && item.phase === 'ISSUING';
|
|
295
|
+
const retry =
|
|
296
|
+
!result.success &&
|
|
297
|
+
(result.error.code === 'INTERNAL_SERVER_ERROR' ||
|
|
298
|
+
result.error.reason === 'IN_PROGRESS') &&
|
|
299
|
+
!unconfirmed &&
|
|
300
|
+
Number(item.attempts) < 5;
|
|
301
|
+
if (retry)
|
|
302
|
+
await tx.run(
|
|
303
|
+
`MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NOT NULL DELETE r`,
|
|
304
|
+
{ id }
|
|
305
|
+
);
|
|
306
|
+
await tx.run(
|
|
307
|
+
`MATCH (i:InboxBatchItem {id: $id}) SET i.state = $state, i.result = $result,
|
|
308
|
+
i.owner = null, i.leaseUntil = null, i.dispatchAt = $dispatchAt`,
|
|
309
|
+
{
|
|
310
|
+
id,
|
|
311
|
+
state: retry ? 'QUEUED' : unconfirmed ? 'NEEDS_RECONCILIATION' : 'COMPLETED',
|
|
312
|
+
result: retry ? null : encrypted,
|
|
313
|
+
dispatchAt:
|
|
314
|
+
retry && !result.success && result.error.reason === 'IN_PROGRESS'
|
|
315
|
+
? Date.now() + Math.min(60_000, 2_000 * 2 ** (Number(item.attempts) - 1))
|
|
316
|
+
: 0,
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
await finalizeSettledBatch(tx, item.batchId);
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/** Claim dispatch leases before publishing. A lost publish acknowledgement causes a safe duplicate. */
|
|
324
|
+
export const takeInboxDispatches = async (): Promise<InboxDispatchLease[]> =>
|
|
325
|
+
transaction(async tx => {
|
|
326
|
+
const result = await tx.run(
|
|
327
|
+
`MATCH (i:InboxBatchItem) WHERE i.state = 'QUEUED' AND i.dispatchAt < $now
|
|
328
|
+
WITH i LIMIT 1000 SET i.lock = coalesce(i.lock, 0) + 1
|
|
329
|
+
WITH i WHERE i.state = 'QUEUED' AND i.dispatchAt < $now
|
|
330
|
+
SET i.dispatchAt = $next RETURN i.id AS id, i.dispatchAt AS dispatchAt`,
|
|
331
|
+
{ now: Date.now(), next: Date.now() + 60_000 }
|
|
332
|
+
);
|
|
333
|
+
return result.records.map(r => ({
|
|
334
|
+
id: r.get('id'),
|
|
335
|
+
dispatchAt: Number(r.get('dispatchAt')),
|
|
336
|
+
}));
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
export const acknowledgeInboxDispatches = async (leases: InboxDispatchLease[]): Promise<void> => {
|
|
340
|
+
await neogma.queryRunner.run(
|
|
341
|
+
`UNWIND $leases AS lease MATCH (i:InboxBatchItem {id: lease.id})
|
|
342
|
+
SET i.lock = coalesce(i.lock, 0) + 1
|
|
343
|
+
WITH i, lease WHERE i.state = 'QUEUED' AND i.dispatchAt = lease.dispatchAt
|
|
344
|
+
SET i.dispatchAt = $next`,
|
|
345
|
+
{ leases, next: Date.now() + 1_800_000 }
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** Poison messages become visible terminal outcomes instead of disappearing into the DLQ. */
|
|
350
|
+
export const deadLetterBatchItem = async (id: string): Promise<void> =>
|
|
351
|
+
transaction(async tx => {
|
|
352
|
+
const rows = await tx.run(
|
|
353
|
+
`MATCH (i:InboxBatchItem {id: $id}) SET i.lock = coalesce(i.lock, 0) + 1 RETURN i`,
|
|
354
|
+
{ id }
|
|
355
|
+
);
|
|
356
|
+
const item = rows.records[0]?.get('i').properties as BatchItem | undefined;
|
|
357
|
+
if (!item || ['COMPLETED', 'NEEDS_RECONCILIATION'].includes(item.state)) return;
|
|
358
|
+
if (item.state === 'PROCESSING' && Number(item.leaseUntil) > Date.now())
|
|
359
|
+
throw new Error('Inbox item still has an active worker');
|
|
360
|
+
const uncertain = item.phase === 'ISSUING';
|
|
361
|
+
const replay = await tx.run(
|
|
362
|
+
`MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NULL RETURN r.value AS value`,
|
|
363
|
+
{ id }
|
|
364
|
+
);
|
|
365
|
+
const saved = replay.records[0]?.get('value');
|
|
366
|
+
if (!uncertain)
|
|
367
|
+
await tx.run(
|
|
368
|
+
`MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NOT NULL DELETE r`,
|
|
369
|
+
{ id }
|
|
370
|
+
);
|
|
371
|
+
await tx.run(
|
|
372
|
+
`MATCH (i:InboxBatchItem {id: $id}) SET i.state = $state, i.owner = null, i.leaseUntil = null, i.result = $result`,
|
|
373
|
+
{
|
|
374
|
+
id,
|
|
375
|
+
state: uncertain && !saved ? 'NEEDS_RECONCILIATION' : 'COMPLETED',
|
|
376
|
+
result: saved ?? null,
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
await finalizeSettledBatch(tx, item.batchId);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/** Recover bounded groups in short transactions without holding locks across the whole maintenance run. */
|
|
383
|
+
export const recoverInboxJobs = async (deadline = Date.now() + 20_000): Promise<void> => {
|
|
384
|
+
await ensureInboxBatchConstraints();
|
|
385
|
+
if (Date.now() >= deadline) return;
|
|
386
|
+
const expired = await neogma.queryRunner.run(
|
|
387
|
+
`MATCH (i:InboxBatchItem) WHERE i.state = 'PROCESSING' AND i.leaseUntil < $now
|
|
388
|
+
RETURN i.id AS id LIMIT 100`,
|
|
389
|
+
{ now: Date.now() }
|
|
390
|
+
);
|
|
391
|
+
for (const row of expired.records) {
|
|
392
|
+
if (Date.now() >= deadline) break;
|
|
393
|
+
await transaction(async tx => {
|
|
394
|
+
const rows = await tx.run(
|
|
395
|
+
`MATCH (i:InboxBatchItem {id: $id})
|
|
396
|
+
SET i.lock = coalesce(i.lock, 0) + 1
|
|
397
|
+
WITH i WHERE i.state = 'PROCESSING' AND i.leaseUntil < $now RETURN i`,
|
|
398
|
+
{ id: row.get('id'), now: Date.now() }
|
|
399
|
+
);
|
|
400
|
+
const item = rows.records[0]?.get('i').properties as BatchItem | undefined;
|
|
401
|
+
if (!item) return;
|
|
402
|
+
// A persisted success is authoritative even if the worker lost its completion reply.
|
|
403
|
+
const replay = await tx.run(
|
|
404
|
+
`MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NULL RETURN r.value AS value`,
|
|
405
|
+
{ id: item.id }
|
|
406
|
+
);
|
|
407
|
+
const saved = replay.records[0]?.get('value');
|
|
408
|
+
const state = saved
|
|
409
|
+
? 'COMPLETED'
|
|
410
|
+
: item.phase === 'ISSUING'
|
|
411
|
+
? 'NEEDS_RECONCILIATION'
|
|
412
|
+
: Number(item.attempts) >= 5
|
|
413
|
+
? 'COMPLETED'
|
|
414
|
+
: 'QUEUED';
|
|
415
|
+
if (item.phase !== 'ISSUING')
|
|
416
|
+
await tx.run(
|
|
417
|
+
`MATCH (r:InboxBatchReplay {itemId: $id}) WHERE r.marker IS NOT NULL DELETE r`,
|
|
418
|
+
{ id: item.id }
|
|
419
|
+
);
|
|
420
|
+
await tx.run(
|
|
421
|
+
`MATCH (i:InboxBatchItem {id: $id}) SET i.state = $state, i.owner = null,
|
|
422
|
+
i.leaseUntil = null, i.dispatchAt = 0, i.result = $result`,
|
|
423
|
+
{ id: item.id, state, result: saved ?? null }
|
|
424
|
+
);
|
|
425
|
+
await finalizeSettledBatch(tx, item.batchId);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (Date.now() < deadline) {
|
|
429
|
+
// Also finalize older terminal batches that predate immediate terminal cleanup.
|
|
430
|
+
const settled = await neogma.queryRunner.run(
|
|
431
|
+
`MATCH (b:InboxBatch) WHERE b.completedAt IS NULL
|
|
432
|
+
AND NOT EXISTS { MATCH (b)-[:HAS_ITEM]->(i) WHERE i.state IN ['QUEUED', 'PROCESSING'] }
|
|
433
|
+
RETURN b.id AS id LIMIT 100`
|
|
434
|
+
);
|
|
435
|
+
for (const row of settled.records) {
|
|
436
|
+
if (Date.now() >= deadline) break;
|
|
437
|
+
await transaction(tx => finalizeSettledBatch(tx, row.get('id')));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Leave room for each transaction to finish; checking only for a future deadline can
|
|
442
|
+
// start a large delete immediately before Lambda shutdown. Recheck between steps.
|
|
443
|
+
if (deadline - Date.now() > CLEANUP_MARGIN_MS)
|
|
444
|
+
await transaction(async tx => {
|
|
445
|
+
await tx.run(
|
|
446
|
+
`MATCH (b:InboxBatch) WHERE b.completedAt < $cutoff WITH b LIMIT 25
|
|
447
|
+
MATCH (b)-[:HAS_ITEM]->(i) DETACH DELETE i, b`,
|
|
448
|
+
{ cutoff: Date.now() - 30 * DAY }
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
if (deadline - Date.now() > CLEANUP_MARGIN_MS)
|
|
452
|
+
await transaction(async tx => {
|
|
453
|
+
await tx.run(
|
|
454
|
+
`MATCH (r:InboxBatchReplay) WHERE r.expiresAt < $now WITH r LIMIT 1000 DELETE r`,
|
|
455
|
+
{ now: Date.now() }
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
if (deadline - Date.now() > CLEANUP_MARGIN_MS)
|
|
459
|
+
await transaction(async tx => {
|
|
460
|
+
// Internal keys cannot be supplied by clients, and messages cannot claim a deleted
|
|
461
|
+
// item. Client-keyed reservations must continue blocking uncertain reissuance.
|
|
462
|
+
await tx.run(
|
|
463
|
+
`MATCH (r:InboxBatchReplay)
|
|
464
|
+
WHERE r.id STARTS WITH 'internal:' AND r.expiresAt IS NULL
|
|
465
|
+
AND NOT EXISTS { MATCH (i:InboxBatchItem) WHERE i.id = r.itemId }
|
|
466
|
+
WITH r LIMIT 1000 DELETE r`
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
};
|
|
@@ -9,12 +9,14 @@ import { getContactMethodByValue } from '@accesslayer/contact-method/read';
|
|
|
9
9
|
import { createContactMethod } from '@accesslayer/contact-method/create';
|
|
10
10
|
import { ProfileType } from 'types/profile';
|
|
11
11
|
import { encryptInboxCredential } from '@helpers/inbox-encryption.helpers';
|
|
12
|
+
import { createPendingInboxRefresh } from './refresh';
|
|
12
13
|
import { parseCredentialMeta } from '@helpers/credential-meta.helpers';
|
|
13
14
|
|
|
14
15
|
export const DEFAULT_INBOX_EXPIRY_DAYS = 30;
|
|
15
16
|
|
|
16
17
|
export const createInboxCredential = async (input: {
|
|
17
18
|
credential: string;
|
|
19
|
+
refresh?: { aggregate: Record<string, unknown>; issueKey: string; requestDigest: string };
|
|
18
20
|
/** Only set after the normal delivery helper has durably stored the credential. */
|
|
19
21
|
delivered?: boolean;
|
|
20
22
|
isSigned: boolean;
|
|
@@ -39,7 +41,7 @@ export const createInboxCredential = async (input: {
|
|
|
39
41
|
throw new Error('expiresInDays must be an integer between 1 and 720');
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
let id = uuid();
|
|
43
45
|
const expiresAt = new Date();
|
|
44
46
|
expiresAt.setDate(expiresAt.getDate() + (input.expiresInDays ?? DEFAULT_INBOX_EXPIRY_DAYS));
|
|
45
47
|
// Encrypt before persisting so the credential itself is not stored in plaintext in the inbox.
|
|
@@ -50,6 +52,7 @@ export const createInboxCredential = async (input: {
|
|
|
50
52
|
|
|
51
53
|
const inboxCredentialData = {
|
|
52
54
|
id,
|
|
55
|
+
...(input.refresh ? { refreshId: input.refresh.aggregate.refreshId } : {}),
|
|
53
56
|
credential: encryptedCredential,
|
|
54
57
|
...credentialMeta,
|
|
55
58
|
isSigned: input.isSigned,
|
|
@@ -76,28 +79,55 @@ export const createInboxCredential = async (input: {
|
|
|
76
79
|
...(input.guardianStatus ? { guardianStatus: input.guardianStatus } : {}),
|
|
77
80
|
};
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
if (input.refresh) {
|
|
83
|
+
const created = await createPendingInboxRefresh({
|
|
84
|
+
inbox: Object.fromEntries(
|
|
85
|
+
Object.entries(flattenObject(inboxCredentialData)).filter(
|
|
86
|
+
([, value]) => value !== undefined
|
|
87
|
+
)
|
|
88
|
+
),
|
|
89
|
+
aggregate: Object.fromEntries(
|
|
90
|
+
Object.entries({ ...input.refresh.aggregate, inboxCredentialId: id }).filter(
|
|
91
|
+
([, value]) => value !== undefined
|
|
92
|
+
)
|
|
93
|
+
),
|
|
94
|
+
issueKey: input.refresh.issueKey,
|
|
95
|
+
requestDigest: input.refresh.requestDigest,
|
|
96
|
+
});
|
|
97
|
+
id = created.id;
|
|
98
|
+
} else {
|
|
99
|
+
await new QueryBuilder(
|
|
100
|
+
new BindParam({
|
|
101
|
+
params: flattenObject(inboxCredentialData),
|
|
102
|
+
issuerProfileId: input.issuerProfile.profileId,
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
})
|
|
105
|
+
)
|
|
106
|
+
.create({ model: InboxCredential, identifier: 'inboxCredential' })
|
|
107
|
+
.set('inboxCredential += $params')
|
|
108
|
+
.run();
|
|
109
|
+
}
|
|
89
110
|
|
|
90
111
|
const contactMethod = await getContactMethodByValue(
|
|
91
112
|
input.recipient.type,
|
|
92
113
|
input.recipient.value
|
|
93
114
|
);
|
|
94
115
|
if (!contactMethod) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
try {
|
|
117
|
+
await createContactMethod({
|
|
118
|
+
type: input.recipient.type,
|
|
119
|
+
value: input.recipient.value,
|
|
120
|
+
isVerified: false,
|
|
121
|
+
isPrimary: false,
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// A concurrent idempotent issue may have created the same contact first.
|
|
125
|
+
if (
|
|
126
|
+
!input.refresh ||
|
|
127
|
+
!(await getContactMethodByValue(input.recipient.type, input.recipient.value))
|
|
128
|
+
)
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
// Create relationships SEQUENTIALLY to prevent deadlocks
|
|
@@ -112,7 +142,8 @@ export const createInboxCredential = async (input: {
|
|
|
112
142
|
.where('ic.id = $inboxId')
|
|
113
143
|
.match('(profile:Profile)')
|
|
114
144
|
.where('profile.profileId = $profileId')
|
|
115
|
-
.
|
|
145
|
+
.merge('(profile)-[created:CREATED_INBOX_CREDENTIAL]->(ic)')
|
|
146
|
+
.set('created.timestamp = coalesce(created.timestamp, $timestamp)')
|
|
116
147
|
.run();
|
|
117
148
|
|
|
118
149
|
await new QueryBuilder(
|
|
@@ -127,7 +158,8 @@ export const createInboxCredential = async (input: {
|
|
|
127
158
|
.where('ic.id = $inboxId')
|
|
128
159
|
.match('(contactMethod:ContactMethod)')
|
|
129
160
|
.where('contactMethod.type = $type AND contactMethod.value = $value')
|
|
130
|
-
.
|
|
161
|
+
.merge('(ic)-[addressed:ADDRESSED_TO]->(contactMethod)')
|
|
162
|
+
.set('addressed.timestamp = coalesce(addressed.timestamp, $timestamp)')
|
|
131
163
|
.run();
|
|
132
164
|
|
|
133
165
|
return (await getInboxCredentialById(id))!;
|