@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,271 @@
|
|
|
1
|
+
import { int } from 'neo4j-driver';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
SHARE_LINK_CLEANUP_DEFAULT_BATCH,
|
|
5
|
+
SHARE_LINK_CLEANUP_DEFAULT_CLAIM_MS,
|
|
6
|
+
SHARE_LINK_CLEANUP_MAX_BATCH,
|
|
7
|
+
SHARE_LINK_CLEANUP_MAX_CLAIM_MS,
|
|
8
|
+
SHARE_LINK_CLEANUP_MIN_CLAIM_MS,
|
|
9
|
+
computeCleanupBackoffMs,
|
|
10
|
+
computeLeaseExpiry,
|
|
11
|
+
} from '@helpers/share-link-lifecycle';
|
|
12
|
+
import { neogma } from '@instance';
|
|
13
|
+
|
|
14
|
+
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
15
|
+
import { failShareLink } from './errors';
|
|
16
|
+
import { generateClaimToken, readNodeProperties, toShareContentCleanupJobRecord } from './helpers';
|
|
17
|
+
import { withShareLinkTransaction, type ShareLinkTransaction } from './transaction';
|
|
18
|
+
import type {
|
|
19
|
+
ClaimCleanupJobsInput,
|
|
20
|
+
ClaimCleanupJobsResult,
|
|
21
|
+
CompleteCleanupJobInput,
|
|
22
|
+
CompleteCleanupJobResult,
|
|
23
|
+
ShareContentCleanupJobRecord,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
const clampLimit = (limit: number | undefined): number => {
|
|
27
|
+
if (!Number.isSafeInteger(limit) || (limit as number) < 1)
|
|
28
|
+
return SHARE_LINK_CLEANUP_DEFAULT_BATCH;
|
|
29
|
+
|
|
30
|
+
return Math.min(limit as number, SHARE_LINK_CLEANUP_MAX_BATCH);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const clampClaimMs = (claimMs: number | undefined): number => {
|
|
34
|
+
if (!Number.isSafeInteger(claimMs)) return SHARE_LINK_CLEANUP_DEFAULT_CLAIM_MS;
|
|
35
|
+
|
|
36
|
+
return Math.min(
|
|
37
|
+
Math.max(claimMs as number, SHARE_LINK_CLEANUP_MIN_CLAIM_MS),
|
|
38
|
+
SHARE_LINK_CLEANUP_MAX_CLAIM_MS
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Require an explicit namespace before any graph access. This is intentionally
|
|
44
|
+
* only an exact, non-empty-string check: the scheduler additionally enforces the
|
|
45
|
+
* C1 opaque-identifier rule, while the reviewed lifecycle tests keep their
|
|
46
|
+
* legacy `localhost%3A3000` namespace at the repository layer.
|
|
47
|
+
*/
|
|
48
|
+
const assertExplicitNamespace = (namespace: unknown): string => {
|
|
49
|
+
if (typeof namespace !== 'string' || namespace.length === 0) {
|
|
50
|
+
failShareLink('INVALID_INPUT', 'an explicit cleanup namespace is required');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return namespace;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const readCleanupJob = async (
|
|
57
|
+
tx: ShareLinkTransaction,
|
|
58
|
+
objectRef: string
|
|
59
|
+
): Promise<ShareContentCleanupJobRecord | null> => {
|
|
60
|
+
const result = await tx.run(
|
|
61
|
+
'MATCH (c:ShareContentCleanupJob {objectRef: $objectRef}) RETURN c LIMIT 1',
|
|
62
|
+
{ objectRef }
|
|
63
|
+
);
|
|
64
|
+
const props = readNodeProperties(result, 'c');
|
|
65
|
+
|
|
66
|
+
return props ? toShareContentCleanupJobRecord(props) : null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Claims a bounded batch of due cleanup jobs for one explicit namespace.
|
|
71
|
+
*
|
|
72
|
+
* The candidate scan and the post-lock reread both filter on the exact
|
|
73
|
+
* namespace, so a worker never claims a foreign namespace's job. Every clock read
|
|
74
|
+
* used for eligibility and for the new claim expiry happens *inside* the managed
|
|
75
|
+
* transaction after the job write lock is taken (and therefore afresh on each
|
|
76
|
+
* driver retry); production callers omit `now`, tests inject it.
|
|
77
|
+
*
|
|
78
|
+
* The claim is fenced: `completeCleanupJob` only accepts the unique `claimToken`
|
|
79
|
+
* returned here, so a claim whose bounded lease expired cannot complete a job that
|
|
80
|
+
* another worker has since claimed. Jobs are returned in due order; the caller
|
|
81
|
+
* performs the actual exact-object LearnCloud deletion and then completes the job.
|
|
82
|
+
*/
|
|
83
|
+
export const claimCleanupJobs = async (
|
|
84
|
+
input: ClaimCleanupJobsInput
|
|
85
|
+
): Promise<ClaimCleanupJobsResult> => {
|
|
86
|
+
const namespace = assertExplicitNamespace(input.namespace);
|
|
87
|
+
|
|
88
|
+
await ensureShareLinkConstraints();
|
|
89
|
+
|
|
90
|
+
const limit = clampLimit(input.limit);
|
|
91
|
+
const claimMs = clampClaimMs(input.claimMs);
|
|
92
|
+
|
|
93
|
+
return withShareLinkTransaction(
|
|
94
|
+
async tx => {
|
|
95
|
+
// Discovery prefilter only. Eligibility is re-checked against the
|
|
96
|
+
// post-lock clock below, so this timestamp cannot authorise a claim.
|
|
97
|
+
const discoveryNow = (input.now ?? new Date()).toISOString();
|
|
98
|
+
const candidates = await tx.run(
|
|
99
|
+
`MATCH (c:ShareContentCleanupJob)
|
|
100
|
+
WHERE c.namespace = $namespace
|
|
101
|
+
AND ((c.status = 'queued' AND c.nextAttemptAt <= $discoveryNow)
|
|
102
|
+
OR (c.status = 'claimed' AND c.claimExpiresAt <= $discoveryNow))
|
|
103
|
+
RETURN c.objectRef AS objectRef ORDER BY c.objectRef LIMIT $limit`,
|
|
104
|
+
{ namespace, discoveryNow, limit: int(limit) }
|
|
105
|
+
);
|
|
106
|
+
const claimToken = generateClaimToken();
|
|
107
|
+
const jobs: ShareContentCleanupJobRecord[] = [];
|
|
108
|
+
for (const candidate of candidates.records) {
|
|
109
|
+
const objectRef = candidate.get('objectRef');
|
|
110
|
+
const locked = await tx.run(
|
|
111
|
+
`MATCH (c:ShareContentCleanupJob {objectRef: $objectRef})
|
|
112
|
+
SET c.lockTick = coalesce(c.lockTick, 0) + 1 RETURN c`,
|
|
113
|
+
{ objectRef }
|
|
114
|
+
);
|
|
115
|
+
const props = readNodeProperties(locked, 'c');
|
|
116
|
+
if (!props) continue;
|
|
117
|
+
|
|
118
|
+
// Fresh clock after the lock (and on every managed retry).
|
|
119
|
+
const now = input.now ?? new Date();
|
|
120
|
+
const nowIso = now.toISOString();
|
|
121
|
+
const job = toShareContentCleanupJobRecord(props);
|
|
122
|
+
if (job.namespace !== namespace) continue;
|
|
123
|
+
if (!(
|
|
124
|
+
(job.status === 'queued' && job.nextAttemptAt <= nowIso) ||
|
|
125
|
+
(job.status === 'claimed' && job.claimExpiresAt && job.claimExpiresAt <= nowIso)
|
|
126
|
+
))
|
|
127
|
+
continue;
|
|
128
|
+
|
|
129
|
+
const claimExpiresAt = computeLeaseExpiry(now, claimMs);
|
|
130
|
+
const updated = await tx.run(
|
|
131
|
+
`MATCH (c:ShareContentCleanupJob {objectRef: $objectRef, namespace: $namespace})
|
|
132
|
+
SET c.status = 'claimed', c.claimedBy = $claimant, c.claimToken = $claimToken,
|
|
133
|
+
c.claimExpiresAt = $claimExpiresAt, c.updatedAt = $now RETURN c`,
|
|
134
|
+
{
|
|
135
|
+
objectRef,
|
|
136
|
+
namespace,
|
|
137
|
+
claimant: input.claimant,
|
|
138
|
+
claimToken,
|
|
139
|
+
claimExpiresAt,
|
|
140
|
+
now: nowIso,
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
const updatedProps = readNodeProperties(updated, 'c');
|
|
144
|
+
if (updatedProps) jobs.push(toShareContentCleanupJobRecord(updatedProps));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { claimToken, jobs };
|
|
148
|
+
},
|
|
149
|
+
{ timeoutMs: input.transactionTimeoutMs, noInlineRetry: input.noInlineRetry }
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Completes a claimed cleanup job (`completed`) or returns it to the queue with
|
|
155
|
+
* exponential backoff (`retry`). A missing/mismatched claim token yields
|
|
156
|
+
* `claim_lost`; this repository never deletes LearnCloud content itself.
|
|
157
|
+
*/
|
|
158
|
+
export const completeCleanupJob = async (
|
|
159
|
+
input: CompleteCleanupJobInput
|
|
160
|
+
): Promise<CompleteCleanupJobResult> => {
|
|
161
|
+
await ensureShareLinkConstraints();
|
|
162
|
+
|
|
163
|
+
return withShareLinkTransaction(
|
|
164
|
+
async tx => {
|
|
165
|
+
const jobProps = await (async () => {
|
|
166
|
+
const result = await tx.run(
|
|
167
|
+
`MATCH (c:ShareContentCleanupJob {objectRef: $objectRef})
|
|
168
|
+
SET c.lockTick = coalesce(c.lockTick, 0) + 1
|
|
169
|
+
RETURN c LIMIT 1`,
|
|
170
|
+
{ objectRef: input.objectRef, claimToken: input.claimToken }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return readNodeProperties(result, 'c');
|
|
174
|
+
})();
|
|
175
|
+
|
|
176
|
+
if (!jobProps) return { outcome: 'claim_lost' };
|
|
177
|
+
|
|
178
|
+
// Sample the clock only after the job write lock is held (and afresh on
|
|
179
|
+
// each managed retry). A worker that was blocked on the lock past its
|
|
180
|
+
// claim expiry therefore observes the lapsed lease and fails completion
|
|
181
|
+
// instead of committing with a stale pre-lock timestamp.
|
|
182
|
+
const now = input.now ?? new Date();
|
|
183
|
+
const nowIso = now.toISOString();
|
|
184
|
+
const job = toShareContentCleanupJobRecord(jobProps);
|
|
185
|
+
if (
|
|
186
|
+
job.status !== 'claimed' ||
|
|
187
|
+
job.claimToken !== input.claimToken ||
|
|
188
|
+
!job.claimExpiresAt ||
|
|
189
|
+
job.claimExpiresAt <= nowIso
|
|
190
|
+
)
|
|
191
|
+
return { outcome: 'claim_lost' };
|
|
192
|
+
|
|
193
|
+
if (input.outcome === 'completed') {
|
|
194
|
+
await tx.run(
|
|
195
|
+
`MATCH (c:ShareContentCleanupJob {objectRef: $objectRef})
|
|
196
|
+
SET c.status = 'completed',
|
|
197
|
+
c.completedAt = $now,
|
|
198
|
+
c.claimToken = null,
|
|
199
|
+
c.claimedBy = null,
|
|
200
|
+
c.claimExpiresAt = null,
|
|
201
|
+
c.updatedAt = $now`,
|
|
202
|
+
{ objectRef: input.objectRef, now: nowIso }
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const updated = await readCleanupJob(tx, input.objectRef);
|
|
206
|
+
|
|
207
|
+
return { outcome: 'completed', job: updated ?? job };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const attempts = job.attempts + 1;
|
|
211
|
+
const nextAttemptAt = new Date(
|
|
212
|
+
now.getTime() + computeCleanupBackoffMs(job.attempts)
|
|
213
|
+
).toISOString();
|
|
214
|
+
|
|
215
|
+
await tx.run(
|
|
216
|
+
`MATCH (c:ShareContentCleanupJob {objectRef: $objectRef})
|
|
217
|
+
SET c.status = 'queued',
|
|
218
|
+
c.attempts = $attempts,
|
|
219
|
+
c.nextAttemptAt = $nextAttemptAt,
|
|
220
|
+
c.claimToken = null,
|
|
221
|
+
c.claimedBy = null,
|
|
222
|
+
c.claimExpiresAt = null,
|
|
223
|
+
c.lastError = $lastError,
|
|
224
|
+
c.updatedAt = $now`,
|
|
225
|
+
{
|
|
226
|
+
objectRef: input.objectRef,
|
|
227
|
+
attempts,
|
|
228
|
+
nextAttemptAt,
|
|
229
|
+
lastError: input.errorMessage ?? null,
|
|
230
|
+
now: nowIso,
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const updated = await readCleanupJob(tx, input.objectRef);
|
|
235
|
+
|
|
236
|
+
return { outcome: 'retry', job: updated ?? job };
|
|
237
|
+
},
|
|
238
|
+
{ timeoutMs: input.transactionTimeoutMs, noInlineRetry: input.noInlineRetry }
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/** Observability/test read: the durable cleanup job for one exact object. */
|
|
243
|
+
export const getCleanupJob = async (
|
|
244
|
+
objectRef: string
|
|
245
|
+
): Promise<ShareContentCleanupJobRecord | null> => {
|
|
246
|
+
const result = await neogma.queryRunner.run(
|
|
247
|
+
'MATCH (c:ShareContentCleanupJob {objectRef: $objectRef}) RETURN c LIMIT 1',
|
|
248
|
+
{ objectRef }
|
|
249
|
+
);
|
|
250
|
+
const props = readNodeProperties(result, 'c');
|
|
251
|
+
|
|
252
|
+
return props ? toShareContentCleanupJobRecord(props) : null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/** Observability/test read: all durable cleanup jobs queued for one share. */
|
|
256
|
+
export const listCleanupJobsForShare = async (
|
|
257
|
+
shareId: string
|
|
258
|
+
): Promise<ShareContentCleanupJobRecord[]> => {
|
|
259
|
+
const result = await neogma.queryRunner.run(
|
|
260
|
+
'MATCH (c:ShareContentCleanupJob {shareId: $shareId}) RETURN c ORDER BY c.createdAt ASC',
|
|
261
|
+
{ shareId }
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return result.records
|
|
265
|
+
.map(record => {
|
|
266
|
+
const props = (record.get('c') as { properties?: Record<string, unknown> })?.properties;
|
|
267
|
+
|
|
268
|
+
return props ? toShareContentCleanupJobRecord(props) : null;
|
|
269
|
+
})
|
|
270
|
+
.filter((job): job is ShareContentCleanupJobRecord => job !== null);
|
|
271
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failures raised by the share-link lifecycle repository.
|
|
3
|
+
*
|
|
4
|
+
* Expected races are surfaced as codes rather than free-form strings so routes and
|
|
5
|
+
* the reconciliation worker can map them to tRPC/HTTP responses without parsing
|
|
6
|
+
* messages, and so tests can assert the exact failure class.
|
|
7
|
+
*/
|
|
8
|
+
export type ShareLinkRepositoryErrorCode =
|
|
9
|
+
| 'NOT_FOUND'
|
|
10
|
+
| 'CONFLICT'
|
|
11
|
+
| 'OPERATION_IN_FLIGHT'
|
|
12
|
+
| 'LEASE_EXPIRED'
|
|
13
|
+
| 'STALE_GENERATION'
|
|
14
|
+
| 'PRECONDITION_FAILED'
|
|
15
|
+
| 'INVALID_INPUT';
|
|
16
|
+
|
|
17
|
+
export class ShareLinkRepositoryError extends Error {
|
|
18
|
+
readonly code: ShareLinkRepositoryErrorCode;
|
|
19
|
+
|
|
20
|
+
constructor(code: ShareLinkRepositoryErrorCode, message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ShareLinkRepositoryError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const isShareLinkRepositoryError = (error: unknown): error is ShareLinkRepositoryError =>
|
|
28
|
+
error instanceof ShareLinkRepositoryError;
|
|
29
|
+
|
|
30
|
+
export function failShareLink(code: ShareLinkRepositoryErrorCode, message: string): never {
|
|
31
|
+
throw new ShareLinkRepositoryError(code, message);
|
|
32
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { SHARE_LINK_OPERATION_RETENTION_MS, isLeaseActive } from '@helpers/share-link-lifecycle';
|
|
2
|
+
import { mergeShareLinkPolicyConservatively } from '@helpers/share-link-policy/resolver';
|
|
3
|
+
import { DEFAULT_SHARE_LINK_POLICY } from '@helpers/share-link-policy/types';
|
|
4
|
+
import type { ShareLinkPolicySnapshot } from '@helpers/share-link-policy/types';
|
|
5
|
+
|
|
6
|
+
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
7
|
+
import { failShareLink } from './errors';
|
|
8
|
+
import {
|
|
9
|
+
deleteReservation,
|
|
10
|
+
enqueueCleanupJob,
|
|
11
|
+
lockShare,
|
|
12
|
+
markOperationCommitted,
|
|
13
|
+
readOperationByOperationId,
|
|
14
|
+
readReservation,
|
|
15
|
+
readShareById,
|
|
16
|
+
toShareLinkOperationRecord,
|
|
17
|
+
toShareLinkRecord,
|
|
18
|
+
toShareLinkReservationRecord,
|
|
19
|
+
} from './helpers';
|
|
20
|
+
import { withShareLinkTransaction } from './transaction';
|
|
21
|
+
import type { FinalizeReservationInput, FinalizeReservationResult } from './types';
|
|
22
|
+
|
|
23
|
+
const pruneAfterFrom = (now: Date): string =>
|
|
24
|
+
new Date(now.getTime() + SHARE_LINK_OPERATION_RETENTION_MS).toISOString();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Commits a reserved revision: consumes the reservation under the share write
|
|
28
|
+
* lock, updates the visible fields/version, records the exact operation result and
|
|
29
|
+
* enqueues cleanup for the superseded immutable object — all in ONE transaction.
|
|
30
|
+
*
|
|
31
|
+
* PRECONDITION (enforced by the caller, never by this repository): the external
|
|
32
|
+
* coordinator has already verified that the immutable LearnCloud object for
|
|
33
|
+
* `objectRef` exists and its payloadHash matches the reserved contentHash. `verifiedContentHash`
|
|
34
|
+
* is re-checked here as defense in depth, but this module performs no network I/O
|
|
35
|
+
* and cannot itself attest remote existence.
|
|
36
|
+
*
|
|
37
|
+
* A stale worker is fenced three ways: the reservation must still match the share,
|
|
38
|
+
* its bounded lease must be active, and its generation must equal the share's
|
|
39
|
+
* current generation. A metadata-only reservation leaves `activeObjectRef`
|
|
40
|
+
* untouched and enqueues no cleanup.
|
|
41
|
+
*/
|
|
42
|
+
export const finalizeReservation = async (
|
|
43
|
+
input: FinalizeReservationInput
|
|
44
|
+
): Promise<FinalizeReservationResult> => {
|
|
45
|
+
await ensureShareLinkConstraints();
|
|
46
|
+
|
|
47
|
+
const now = input.now ?? new Date();
|
|
48
|
+
const nowIso = now.toISOString();
|
|
49
|
+
|
|
50
|
+
return withShareLinkTransaction(
|
|
51
|
+
async tx => {
|
|
52
|
+
const lockedProps = await lockShare(tx, input.shareId);
|
|
53
|
+
|
|
54
|
+
if (!lockedProps) {
|
|
55
|
+
failShareLink('NOT_FOUND', 'share not found');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const share = toShareLinkRecord(lockedProps);
|
|
59
|
+
|
|
60
|
+
if (
|
|
61
|
+
share.namespace !== input.namespace ||
|
|
62
|
+
share.ownerProfileId !== input.ownerProfileId
|
|
63
|
+
) {
|
|
64
|
+
failShareLink('NOT_FOUND', 'share not found');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
share.lastOperationId === input.operationId &&
|
|
69
|
+
(share.activeObjectRef ?? null) === (input.objectRef ?? null) &&
|
|
70
|
+
share.contentState === 'finalized'
|
|
71
|
+
) {
|
|
72
|
+
return { outcome: 'already_finalized', share };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const reservationProps = await readReservation(tx, input.shareId);
|
|
76
|
+
|
|
77
|
+
if (!reservationProps) {
|
|
78
|
+
const operationProps = await readOperationByOperationId(tx, input.operationId);
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
operationProps &&
|
|
82
|
+
toShareLinkOperationRecord(operationProps).status === 'committed' &&
|
|
83
|
+
operationProps.shareId === input.shareId &&
|
|
84
|
+
operationProps.namespace === input.namespace &&
|
|
85
|
+
operationProps.ownerProfileId === input.ownerProfileId
|
|
86
|
+
) {
|
|
87
|
+
return { outcome: 'already_finalized', share };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
failShareLink('CONFLICT', 'reservation is absent or already consumed');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const reservation = toShareLinkReservationRecord(reservationProps);
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
reservation.operationId !== input.operationId ||
|
|
97
|
+
(reservation.objectRef ?? null) !== (input.objectRef ?? null) ||
|
|
98
|
+
reservation.generation !== input.generation ||
|
|
99
|
+
reservation.leaseOwner !== input.leaseOwner ||
|
|
100
|
+
reservation.namespace !== input.namespace ||
|
|
101
|
+
reservation.ownerProfileId !== input.ownerProfileId
|
|
102
|
+
) {
|
|
103
|
+
failShareLink('CONFLICT', 'finalize does not match the in-flight reservation');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (share.status === 'stopped') {
|
|
107
|
+
failShareLink('CONFLICT', 'cannot finalize a stopped share');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!isLeaseActive(reservation.leaseExpiresAt, input.now ?? new Date())) {
|
|
111
|
+
failShareLink('LEASE_EXPIRED', 'the reservation lease has expired');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (share.generation !== reservation.generation) {
|
|
115
|
+
failShareLink('STALE_GENERATION', 'a newer reservation superseded this revision');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (share.version !== reservation.baseVersion) {
|
|
119
|
+
failShareLink('CONFLICT', 'share version changed since the reservation');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (reservation.contentVersion !== null) {
|
|
123
|
+
if (reservation.opKind === 'create') {
|
|
124
|
+
// The first revision is contentVersion 1 and is not a "+1" replacement.
|
|
125
|
+
if (reservation.contentVersion !== 1) {
|
|
126
|
+
failShareLink(
|
|
127
|
+
'PRECONDITION_FAILED',
|
|
128
|
+
'a create reservation must target version 1'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
if (share.contentVersion !== reservation.baseContentVersion) {
|
|
133
|
+
failShareLink(
|
|
134
|
+
'CONFLICT',
|
|
135
|
+
'visible content version changed since the reservation'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (reservation.contentVersion !== share.contentVersion + 1) {
|
|
140
|
+
failShareLink(
|
|
141
|
+
'PRECONDITION_FAILED',
|
|
142
|
+
'the reserved content version is not current + 1'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
input.verifiedContentHash !== undefined &&
|
|
150
|
+
reservation.contentHash !== null &&
|
|
151
|
+
input.verifiedContentHash !== reservation.contentHash
|
|
152
|
+
) {
|
|
153
|
+
failShareLink(
|
|
154
|
+
'PRECONDITION_FAILED',
|
|
155
|
+
'verified content hash does not match the reservation'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const previousObjectRef = share.activeObjectRef;
|
|
160
|
+
const nextObjectRef = reservation.objectRef ?? share.activeObjectRef;
|
|
161
|
+
const nextContentVersion = reservation.contentVersion ?? share.contentVersion;
|
|
162
|
+
|
|
163
|
+
// A fresh graph-local recheck is needed before enabling counting.
|
|
164
|
+
// In particular, shares created while age was unavailable retain
|
|
165
|
+
// an unknown snapshot until the owner explicitly updates them.
|
|
166
|
+
const currentPolicy: ShareLinkPolicySnapshot = {
|
|
167
|
+
isMinor: share.minorPolicyIsMinor,
|
|
168
|
+
policyResolved: share.minorPolicyResolved,
|
|
169
|
+
defaultExpiryDays: share.minorPolicyDefaultExpiryDays === 365 ? 365 : 30,
|
|
170
|
+
viewCountingEnabled: share.minorPolicyViewCountingEnabled,
|
|
171
|
+
};
|
|
172
|
+
const checkedPolicy = input.resolveCurrentPolicy
|
|
173
|
+
? await input
|
|
174
|
+
.resolveCurrentPolicy(tx, share.ownerProfileId, now)
|
|
175
|
+
.catch(() => DEFAULT_SHARE_LINK_POLICY)
|
|
176
|
+
: null;
|
|
177
|
+
const reservationPolicy = checkedPolicy
|
|
178
|
+
? mergeShareLinkPolicyConservatively(reservation.policy, checkedPolicy)
|
|
179
|
+
: mergeShareLinkPolicyConservatively(reservation.policy, currentPolicy);
|
|
180
|
+
const mayReplaceUnknown =
|
|
181
|
+
checkedPolicy !== null &&
|
|
182
|
+
!currentPolicy.policyResolved &&
|
|
183
|
+
(reservation.opKind === 'create' || reservation.opKind === 'update');
|
|
184
|
+
const effectivePolicy = mayReplaceUnknown
|
|
185
|
+
? reservationPolicy
|
|
186
|
+
: mergeShareLinkPolicyConservatively(currentPolicy, reservationPolicy);
|
|
187
|
+
|
|
188
|
+
await tx.run(
|
|
189
|
+
`MATCH (s:ShareLink {id: $shareId})
|
|
190
|
+
SET s += $props`,
|
|
191
|
+
{
|
|
192
|
+
shareId: input.shareId,
|
|
193
|
+
props: {
|
|
194
|
+
version: share.version + 1,
|
|
195
|
+
contentVersion: nextContentVersion,
|
|
196
|
+
status: 'active',
|
|
197
|
+
contentState: 'finalized',
|
|
198
|
+
activeObjectRef: nextObjectRef,
|
|
199
|
+
activeObjectOperationId: reservation.objectRef
|
|
200
|
+
? reservation.operationId
|
|
201
|
+
: share.activeObjectOperationId,
|
|
202
|
+
activeContentHash: reservation.contentHash ?? share.activeContentHash,
|
|
203
|
+
activeContentBytes: reservation.contentBytes ?? share.activeContentBytes,
|
|
204
|
+
activeRecoveryHash: reservation.recoveryHash ?? share.activeRecoveryHash,
|
|
205
|
+
lastOperationId: reservation.operationId,
|
|
206
|
+
title: reservation.title,
|
|
207
|
+
note: reservation.note,
|
|
208
|
+
expiresAt: reservation.expiresAt,
|
|
209
|
+
selectedCount: reservation.selectedCount,
|
|
210
|
+
passcodeHash: reservation.passcodeHash,
|
|
211
|
+
notifyOnView:
|
|
212
|
+
reservation.notifyOnView && effectivePolicy.viewCountingEnabled,
|
|
213
|
+
minorPolicyIsMinor: effectivePolicy.isMinor,
|
|
214
|
+
minorPolicyResolved: effectivePolicy.policyResolved,
|
|
215
|
+
minorPolicyDefaultExpiryDays: effectivePolicy.defaultExpiryDays,
|
|
216
|
+
minorPolicyViewCountingEnabled: effectivePolicy.viewCountingEnabled,
|
|
217
|
+
updatedAt: nowIso,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const updatedProps = await readShareById(tx, input.shareId);
|
|
223
|
+
|
|
224
|
+
if (!updatedProps) {
|
|
225
|
+
failShareLink('CONFLICT', 'share disappeared during finalize');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const updatedShare = toShareLinkRecord(updatedProps);
|
|
229
|
+
|
|
230
|
+
await markOperationCommitted(tx, {
|
|
231
|
+
namespace: reservation.namespace,
|
|
232
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
233
|
+
opKind: reservation.opKind,
|
|
234
|
+
clientRequestId: reservation.clientRequestId,
|
|
235
|
+
operationId: reservation.operationId,
|
|
236
|
+
resultJson: JSON.stringify(updatedShare),
|
|
237
|
+
resultVersion: updatedShare.version,
|
|
238
|
+
now: nowIso,
|
|
239
|
+
pruneAfter: pruneAfterFrom(now),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await deleteReservation(tx, input.shareId);
|
|
243
|
+
|
|
244
|
+
let cleanupQueuedFor: string | null = null;
|
|
245
|
+
|
|
246
|
+
if (previousObjectRef && previousObjectRef !== nextObjectRef) {
|
|
247
|
+
await enqueueCleanupJob(tx, {
|
|
248
|
+
objectRef: previousObjectRef,
|
|
249
|
+
operationId: share.activeObjectOperationId!,
|
|
250
|
+
namespace: reservation.namespace,
|
|
251
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
252
|
+
shareId: input.shareId,
|
|
253
|
+
contentVersion: share.contentVersion,
|
|
254
|
+
reason: 'superseded',
|
|
255
|
+
now: nowIso,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
cleanupQueuedFor = previousObjectRef;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { outcome: 'finalized', share: updatedShare, cleanupQueuedFor };
|
|
262
|
+
},
|
|
263
|
+
{ timeoutMs: input.transactionTimeoutMs, noInlineRetry: input.noInlineRetry }
|
|
264
|
+
);
|
|
265
|
+
};
|