@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,990 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { computeShareLinkPayloadHash } from '@helpers/share-link-lifecycle';
|
|
4
|
+
import {
|
|
5
|
+
createShareLinkCoordinator,
|
|
6
|
+
runShareContentCleanupOnce,
|
|
7
|
+
} from '@helpers/share-link-coordinator';
|
|
8
|
+
import type {
|
|
9
|
+
ShareLinkCoordinatorDependencies,
|
|
10
|
+
ShareLinkLifecycleRepository,
|
|
11
|
+
} from '@helpers/share-link-coordinator';
|
|
12
|
+
|
|
13
|
+
import type { ShareContentClient } from '../src/helpers/share-content-client';
|
|
14
|
+
import { ShareLinkRepositoryError } from '../src/accesslayer/share-link/errors';
|
|
15
|
+
import type { ShareLinkRecord } from '../src/models/ShareLink';
|
|
16
|
+
import type {
|
|
17
|
+
ShareLinkOperationKind,
|
|
18
|
+
ShareLinkReservationRecord,
|
|
19
|
+
} from '../src/accesslayer/share-link';
|
|
20
|
+
|
|
21
|
+
const NAMESPACE = 'test-namespace';
|
|
22
|
+
const OWNER = 'owner-1';
|
|
23
|
+
const SHARE_ID = Buffer.alloc(16, 1).toString('base64url');
|
|
24
|
+
const OBJECT_REF = 'o'.repeat(43);
|
|
25
|
+
const OPERATION_ID = '11111111-1111-4111-8111-111111111111';
|
|
26
|
+
const CLIENT_REQUEST_ID = '22222222-2222-4222-8222-222222222222';
|
|
27
|
+
|
|
28
|
+
const envelope = {
|
|
29
|
+
v: 1 as const,
|
|
30
|
+
alg: 'A256GCM' as const,
|
|
31
|
+
iv: Buffer.alloc(12, 2).toString('base64url'),
|
|
32
|
+
ct: Buffer.alloc(32, 3).toString('base64url'),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const recovery = { protected: 'a', iv: 'b', ciphertext: 'c', tag: 'd' };
|
|
36
|
+
const recoveryBytes = Buffer.byteLength(JSON.stringify(recovery), 'utf8');
|
|
37
|
+
|
|
38
|
+
const payloadHash = computeShareLinkPayloadHash({
|
|
39
|
+
envelope,
|
|
40
|
+
ownerEncryptedRecovery: recovery,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const createRequest = () => ({
|
|
44
|
+
id: SHARE_ID,
|
|
45
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
46
|
+
title: 'Shared credentials',
|
|
47
|
+
selectedCount: 1,
|
|
48
|
+
notifyOnView: false,
|
|
49
|
+
contentVersion: 1,
|
|
50
|
+
envelope,
|
|
51
|
+
ownerEncryptedRecovery: recovery,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const makeShare = (overrides: Partial<ShareLinkRecord> = {}): ShareLinkRecord => ({
|
|
55
|
+
id: SHARE_ID,
|
|
56
|
+
namespace: NAMESPACE,
|
|
57
|
+
ownerProfileId: OWNER,
|
|
58
|
+
version: 1,
|
|
59
|
+
contentVersion: 1,
|
|
60
|
+
generation: 1,
|
|
61
|
+
status: 'pending',
|
|
62
|
+
contentState: 'staging',
|
|
63
|
+
activeObjectRef: null,
|
|
64
|
+
activeObjectOperationId: null,
|
|
65
|
+
activeContentHash: null,
|
|
66
|
+
activeContentBytes: null,
|
|
67
|
+
activeRecoveryHash: null,
|
|
68
|
+
lastOperationId: null,
|
|
69
|
+
createdByClientRequestId: null,
|
|
70
|
+
title: 'Shared credentials',
|
|
71
|
+
note: null,
|
|
72
|
+
selectedCount: 1,
|
|
73
|
+
expiresAt: null,
|
|
74
|
+
stoppedAt: null,
|
|
75
|
+
viewCount: 0,
|
|
76
|
+
lastViewedAt: null,
|
|
77
|
+
minorPolicyIsMinor: null,
|
|
78
|
+
minorPolicyResolved: false,
|
|
79
|
+
minorPolicyDefaultExpiryDays: 30,
|
|
80
|
+
minorPolicyViewCountingEnabled: false,
|
|
81
|
+
createdAt: '2026-09-20T00:00:00.000Z',
|
|
82
|
+
updatedAt: '2026-09-20T00:00:00.000Z',
|
|
83
|
+
...overrides,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const makeReservation = (
|
|
87
|
+
overrides: Partial<ShareLinkReservationRecord> = {}
|
|
88
|
+
): ShareLinkReservationRecord => ({
|
|
89
|
+
shareId: SHARE_ID,
|
|
90
|
+
namespace: NAMESPACE,
|
|
91
|
+
ownerProfileId: OWNER,
|
|
92
|
+
opKind: 'create' as ShareLinkOperationKind,
|
|
93
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
94
|
+
requestHash: 'r'.repeat(64),
|
|
95
|
+
operationId: OPERATION_ID,
|
|
96
|
+
objectRef: OBJECT_REF,
|
|
97
|
+
contentVersion: 1,
|
|
98
|
+
baseVersion: 1,
|
|
99
|
+
baseContentVersion: 1,
|
|
100
|
+
contentHash: payloadHash,
|
|
101
|
+
contentBytes: 32,
|
|
102
|
+
recoveryHash: 'c'.repeat(64),
|
|
103
|
+
recoveryBytes: 12,
|
|
104
|
+
title: 'Shared credentials',
|
|
105
|
+
note: null,
|
|
106
|
+
expiresAt: null,
|
|
107
|
+
selectedCount: 1,
|
|
108
|
+
passcodeHash: null,
|
|
109
|
+
notifyOnView: false,
|
|
110
|
+
generation: 1,
|
|
111
|
+
leaseOwner: 'worker-1',
|
|
112
|
+
leaseExpiresAt: '2099-01-01T00:00:00.000Z',
|
|
113
|
+
createdAt: '2026-09-20T00:00:00.000Z',
|
|
114
|
+
updatedAt: '2026-09-20T00:00:00.000Z',
|
|
115
|
+
...overrides,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const activeStat = (overrides: Record<string, unknown> = {}) => ({
|
|
119
|
+
kind: 'active' as const,
|
|
120
|
+
namespace: NAMESPACE,
|
|
121
|
+
ownerProfileId: OWNER,
|
|
122
|
+
shareId: SHARE_ID,
|
|
123
|
+
contentVersion: 1,
|
|
124
|
+
objectId: OBJECT_REF,
|
|
125
|
+
operationId: OPERATION_ID,
|
|
126
|
+
contentHash: 'd'.repeat(64),
|
|
127
|
+
payloadHash,
|
|
128
|
+
ciphertextBytes: 32,
|
|
129
|
+
recoveryBytes: 12,
|
|
130
|
+
createdAt: '2026-09-20T00:00:00.000Z',
|
|
131
|
+
...overrides,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const makeClient = (overrides: Partial<ShareContentClient> = {}) =>
|
|
135
|
+
({
|
|
136
|
+
put: vi.fn(async () => ({
|
|
137
|
+
ok: true,
|
|
138
|
+
value: { status: 'created', record: activeStat() },
|
|
139
|
+
})),
|
|
140
|
+
stat: vi.fn(async () => ({ ok: true, value: activeStat() })),
|
|
141
|
+
delete: vi.fn(async () => ({
|
|
142
|
+
ok: true,
|
|
143
|
+
value: {
|
|
144
|
+
kind: 'tombstone',
|
|
145
|
+
namespace: NAMESPACE,
|
|
146
|
+
ownerProfileId: OWNER,
|
|
147
|
+
shareId: SHARE_ID,
|
|
148
|
+
contentVersion: 1,
|
|
149
|
+
objectId: OBJECT_REF,
|
|
150
|
+
operationId: OPERATION_ID,
|
|
151
|
+
deletedAt: '2026-09-20T00:00:00.000Z',
|
|
152
|
+
},
|
|
153
|
+
})),
|
|
154
|
+
get: vi.fn(),
|
|
155
|
+
readRecovery: vi.fn(),
|
|
156
|
+
config: {},
|
|
157
|
+
...overrides,
|
|
158
|
+
}) as unknown as ShareContentClient;
|
|
159
|
+
|
|
160
|
+
const makeRepository = (overrides: Partial<ShareLinkLifecycleRepository> = {}) =>
|
|
161
|
+
({
|
|
162
|
+
reserveCreate: vi.fn(),
|
|
163
|
+
reserveReplacement: vi.fn(),
|
|
164
|
+
finalizeReservation: vi.fn(async () => ({
|
|
165
|
+
outcome: 'finalized',
|
|
166
|
+
share: makeShare({ status: 'active', contentState: 'finalized', version: 2 }),
|
|
167
|
+
cleanupQueuedFor: null,
|
|
168
|
+
})),
|
|
169
|
+
abandonReservation: vi.fn(async () => ({
|
|
170
|
+
outcome: 'abandoned',
|
|
171
|
+
cleanupQueuedFor: OBJECT_REF,
|
|
172
|
+
})),
|
|
173
|
+
revokeShareLink: vi.fn(),
|
|
174
|
+
getShareLink: vi.fn(),
|
|
175
|
+
getCurrentShareContent: vi.fn(),
|
|
176
|
+
claimCleanupJobs: vi.fn(),
|
|
177
|
+
completeCleanupJob: vi.fn(),
|
|
178
|
+
...overrides,
|
|
179
|
+
}) as unknown as ShareLinkLifecycleRepository;
|
|
180
|
+
|
|
181
|
+
const makeCoordinator = (
|
|
182
|
+
repository: ShareLinkLifecycleRepository,
|
|
183
|
+
client: ShareContentClient,
|
|
184
|
+
overrides: Partial<ShareLinkCoordinatorDependencies> = {}
|
|
185
|
+
) =>
|
|
186
|
+
createShareLinkCoordinator({
|
|
187
|
+
repository,
|
|
188
|
+
client,
|
|
189
|
+
leaseOwner: 'worker-1',
|
|
190
|
+
leaseMs: 60_000,
|
|
191
|
+
now: () => new Date('2026-09-20T00:00:00.000Z'),
|
|
192
|
+
...overrides,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const context = { namespace: NAMESPACE, ownerProfileId: OWNER };
|
|
196
|
+
|
|
197
|
+
describe('share-link coordinator create', () => {
|
|
198
|
+
let repository: ShareLinkLifecycleRepository;
|
|
199
|
+
let client: ShareContentClient;
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
repository = makeRepository();
|
|
203
|
+
client = makeClient();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('reserves with the payload hash, uploads the reserved ids, verifies stat and finalizes', async () => {
|
|
207
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
208
|
+
outcome: 'reserved',
|
|
209
|
+
state: 'created',
|
|
210
|
+
share: makeShare(),
|
|
211
|
+
reservation: makeReservation(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const coordinator = makeCoordinator(repository, client);
|
|
215
|
+
const result = await coordinator.createShareLink(createRequest(), context);
|
|
216
|
+
|
|
217
|
+
expect(result.status).toBe('committed');
|
|
218
|
+
const reserveArgs = (repository.reserveCreate as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
219
|
+
expect(reserveArgs.content.contentHash).toBe(payloadHash);
|
|
220
|
+
expect(reserveArgs.content.contentBytes).toBe(32);
|
|
221
|
+
expect(reserveArgs.content.recoveryBytes).toBe(recoveryBytes);
|
|
222
|
+
expect(reserveArgs.requestHash).toMatch(/^[0-9a-f]{64}$/);
|
|
223
|
+
|
|
224
|
+
const putArgs = (client.put as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
225
|
+
expect(putArgs.objectId).toBe(OBJECT_REF);
|
|
226
|
+
expect(putArgs.operationId).toBe(OPERATION_ID);
|
|
227
|
+
expect(putArgs.contentVersion).toBe(1);
|
|
228
|
+
|
|
229
|
+
const statArgs = (client.stat as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
230
|
+
expect(statArgs.objectId).toBe(OBJECT_REF);
|
|
231
|
+
|
|
232
|
+
const finalizeArgs = (repository.finalizeReservation as ReturnType<typeof vi.fn>).mock
|
|
233
|
+
.calls[0][0];
|
|
234
|
+
expect(finalizeArgs.verifiedContentHash).toBe(payloadHash);
|
|
235
|
+
expect(finalizeArgs.objectRef).toBe(OBJECT_REF);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('hashes a passcode before persistence and keeps notification opt-in explicit', async () => {
|
|
239
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
240
|
+
outcome: 'reserved',
|
|
241
|
+
state: 'created',
|
|
242
|
+
share: makeShare(),
|
|
243
|
+
reservation: makeReservation(),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const coordinator = makeCoordinator(repository, client, {
|
|
247
|
+
policyResolver: {
|
|
248
|
+
resolve: vi.fn(async () => ({
|
|
249
|
+
isMinor: false,
|
|
250
|
+
policyResolved: true,
|
|
251
|
+
defaultExpiryDays: 365,
|
|
252
|
+
viewCountingEnabled: true,
|
|
253
|
+
})),
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
await coordinator.createShareLink(
|
|
257
|
+
{ ...createRequest(), passcode: '24682468', notifyOnView: true },
|
|
258
|
+
context
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const reserveArgs = (repository.reserveCreate as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
262
|
+
expect(reserveArgs.passcodeHash).toMatch(/^\$argon2id\$/);
|
|
263
|
+
expect(reserveArgs.passcodeHash).not.toContain('24682468');
|
|
264
|
+
expect(reserveArgs).not.toHaveProperty('passcode');
|
|
265
|
+
expect(reserveArgs.notifyOnView).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('suppresses view notifications when the trusted policy disables view counting', async () => {
|
|
269
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
270
|
+
outcome: 'reserved',
|
|
271
|
+
state: 'created',
|
|
272
|
+
share: makeShare(),
|
|
273
|
+
reservation: makeReservation(),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const coordinator = makeCoordinator(repository, client, {
|
|
277
|
+
policyResolver: {
|
|
278
|
+
resolve: vi.fn(async () => ({
|
|
279
|
+
isMinor: true,
|
|
280
|
+
policyResolved: true,
|
|
281
|
+
defaultExpiryDays: 30,
|
|
282
|
+
viewCountingEnabled: false,
|
|
283
|
+
})),
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
await coordinator.createShareLink({ ...createRequest(), notifyOnView: true }, context);
|
|
287
|
+
|
|
288
|
+
const reserveArgs = (repository.reserveCreate as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
289
|
+
expect(reserveArgs.notifyOnView).toBe(false);
|
|
290
|
+
expect(reserveArgs.policy.defaultExpiryDays).toBe(30);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('drives a resumed in-flight create to the same reserved object', async () => {
|
|
294
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
295
|
+
outcome: 'reserved',
|
|
296
|
+
state: 'resumed',
|
|
297
|
+
share: makeShare(),
|
|
298
|
+
reservation: makeReservation(),
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const coordinator = makeCoordinator(repository, client);
|
|
302
|
+
const result = await coordinator.createShareLink(createRequest(), context);
|
|
303
|
+
|
|
304
|
+
expect(result.status).toBe('committed');
|
|
305
|
+
const putArgs = (client.put as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
306
|
+
expect(putArgs.objectId).toBe(OBJECT_REF);
|
|
307
|
+
expect(putArgs.operationId).toBe(OPERATION_ID);
|
|
308
|
+
expect(repository.finalizeReservation).toHaveBeenCalledTimes(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('returns the recorded result for a committed retry without re-uploading', async () => {
|
|
312
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
313
|
+
outcome: 'already_committed',
|
|
314
|
+
recorded: makeShare({ version: 2, status: 'active', contentState: 'finalized' }),
|
|
315
|
+
current: makeShare({ version: 3, status: 'active', contentState: 'finalized' }),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const coordinator = makeCoordinator(repository, client);
|
|
319
|
+
const result = await coordinator.createShareLink(createRequest(), context);
|
|
320
|
+
|
|
321
|
+
expect(result.status).toBe('replayed');
|
|
322
|
+
if (result.status === 'replayed') {
|
|
323
|
+
expect(result.share.version).toBe(3);
|
|
324
|
+
expect(result.recorded.version).toBe(2);
|
|
325
|
+
}
|
|
326
|
+
expect(client.put).not.toHaveBeenCalled();
|
|
327
|
+
expect(client.stat).not.toHaveBeenCalled();
|
|
328
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('keeps a transient upload failure resumable without abandoning the reservation', async () => {
|
|
332
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
333
|
+
outcome: 'reserved',
|
|
334
|
+
state: 'created',
|
|
335
|
+
share: makeShare(),
|
|
336
|
+
reservation: makeReservation(),
|
|
337
|
+
});
|
|
338
|
+
(client.put as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
339
|
+
ok: false,
|
|
340
|
+
error: 'UNAVAILABLE',
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const coordinator = makeCoordinator(repository, client);
|
|
344
|
+
const result = await coordinator.createShareLink(createRequest(), context);
|
|
345
|
+
|
|
346
|
+
expect(result.status).toBe('pending');
|
|
347
|
+
if (result.status === 'pending') expect(result.reason).toBe('upload_unavailable');
|
|
348
|
+
expect(repository.abandonReservation).not.toHaveBeenCalled();
|
|
349
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('abandons and rethrows a deterministic upload rejection', async () => {
|
|
353
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
354
|
+
outcome: 'reserved',
|
|
355
|
+
state: 'created',
|
|
356
|
+
share: makeShare(),
|
|
357
|
+
reservation: makeReservation(),
|
|
358
|
+
});
|
|
359
|
+
(client.put as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
360
|
+
ok: false,
|
|
361
|
+
error: 'CONFLICT',
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const coordinator = makeCoordinator(repository, client);
|
|
365
|
+
|
|
366
|
+
await expect(coordinator.createShareLink(createRequest(), context)).rejects.toMatchObject({
|
|
367
|
+
code: 'CONFLICT',
|
|
368
|
+
});
|
|
369
|
+
expect(repository.abandonReservation).toHaveBeenCalledTimes(1);
|
|
370
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('never finalizes on a wrong stat hash and never destroys the staged object', async () => {
|
|
374
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
375
|
+
outcome: 'reserved',
|
|
376
|
+
state: 'created',
|
|
377
|
+
share: makeShare(),
|
|
378
|
+
reservation: makeReservation(),
|
|
379
|
+
});
|
|
380
|
+
(client.stat as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
381
|
+
ok: true,
|
|
382
|
+
value: activeStat({ payloadHash: 'e'.repeat(64) }),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const coordinator = makeCoordinator(repository, client);
|
|
386
|
+
const result = await coordinator.createShareLink(createRequest(), context);
|
|
387
|
+
|
|
388
|
+
expect(result.status).toBe('pending');
|
|
389
|
+
if (result.status === 'pending') expect(result.reason).toBe('stat_mismatch');
|
|
390
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
391
|
+
expect(repository.abandonReservation).not.toHaveBeenCalled();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('abandons a tombstoned reservation and surfaces a conflict', async () => {
|
|
395
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
396
|
+
outcome: 'reserved',
|
|
397
|
+
state: 'created',
|
|
398
|
+
share: makeShare(),
|
|
399
|
+
reservation: makeReservation(),
|
|
400
|
+
});
|
|
401
|
+
(client.stat as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
402
|
+
ok: true,
|
|
403
|
+
value: {
|
|
404
|
+
kind: 'tombstone',
|
|
405
|
+
namespace: NAMESPACE,
|
|
406
|
+
ownerProfileId: OWNER,
|
|
407
|
+
shareId: SHARE_ID,
|
|
408
|
+
contentVersion: 1,
|
|
409
|
+
objectId: OBJECT_REF,
|
|
410
|
+
operationId: OPERATION_ID,
|
|
411
|
+
deletedAt: '2026-09-20T00:00:00.000Z',
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const coordinator = makeCoordinator(repository, client);
|
|
416
|
+
|
|
417
|
+
await expect(coordinator.createShareLink(createRequest(), context)).rejects.toMatchObject({
|
|
418
|
+
code: 'CONFLICT',
|
|
419
|
+
});
|
|
420
|
+
expect(repository.abandonReservation).toHaveBeenCalledTimes(1);
|
|
421
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('returns a resumable pending result for an ambiguous finalize failure', async () => {
|
|
425
|
+
(repository.reserveCreate as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
426
|
+
outcome: 'reserved',
|
|
427
|
+
state: 'created',
|
|
428
|
+
share: makeShare(),
|
|
429
|
+
reservation: makeReservation(),
|
|
430
|
+
});
|
|
431
|
+
(repository.finalizeReservation as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
432
|
+
new Error('neo4j unavailable')
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const coordinator = makeCoordinator(repository, client);
|
|
436
|
+
const result = await coordinator.createShareLink(createRequest(), context);
|
|
437
|
+
|
|
438
|
+
expect(result.status).toBe('pending');
|
|
439
|
+
if (result.status === 'pending') expect(result.reason).toBe('finalize_unavailable');
|
|
440
|
+
expect(repository.abandonReservation).not.toHaveBeenCalled();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('rejects invalid requests and contexts before any I/O', async () => {
|
|
444
|
+
const coordinator = makeCoordinator(repository, client);
|
|
445
|
+
|
|
446
|
+
await expect(
|
|
447
|
+
coordinator.createShareLink({ ...createRequest(), title: '' }, context)
|
|
448
|
+
).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
|
449
|
+
await expect(
|
|
450
|
+
coordinator.createShareLink(createRequest(), {
|
|
451
|
+
namespace: 'bad:ns',
|
|
452
|
+
ownerProfileId: OWNER,
|
|
453
|
+
})
|
|
454
|
+
).rejects.toMatchObject({ code: 'INVALID_INPUT' });
|
|
455
|
+
expect(repository.reserveCreate).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe('share-link coordinator update', () => {
|
|
460
|
+
let repository: ShareLinkLifecycleRepository;
|
|
461
|
+
let client: ShareContentClient;
|
|
462
|
+
|
|
463
|
+
beforeEach(() => {
|
|
464
|
+
repository = makeRepository();
|
|
465
|
+
client = makeClient();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('metadata-only update uploads nothing and finalizes with a null object ref', async () => {
|
|
469
|
+
(repository.reserveReplacement as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
470
|
+
outcome: 'reserved',
|
|
471
|
+
state: 'created',
|
|
472
|
+
share: makeShare({ status: 'active', contentState: 'finalized', version: 2 }),
|
|
473
|
+
reservation: makeReservation({
|
|
474
|
+
opKind: 'update',
|
|
475
|
+
objectRef: null,
|
|
476
|
+
contentVersion: null,
|
|
477
|
+
contentHash: null,
|
|
478
|
+
contentBytes: null,
|
|
479
|
+
recoveryHash: null,
|
|
480
|
+
recoveryBytes: null,
|
|
481
|
+
title: 'Renamed',
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const coordinator = makeCoordinator(repository, client);
|
|
486
|
+
const result = await coordinator.updateShareLink(
|
|
487
|
+
{
|
|
488
|
+
id: SHARE_ID,
|
|
489
|
+
expectedVersion: 2,
|
|
490
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
491
|
+
title: 'Renamed',
|
|
492
|
+
},
|
|
493
|
+
context
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
expect(result.status).toBe('committed');
|
|
497
|
+
expect(client.put).not.toHaveBeenCalled();
|
|
498
|
+
expect(client.stat).not.toHaveBeenCalled();
|
|
499
|
+
|
|
500
|
+
const finalizeArgs = (repository.finalizeReservation as ReturnType<typeof vi.fn>).mock
|
|
501
|
+
.calls[0][0];
|
|
502
|
+
expect(finalizeArgs.objectRef).toBeNull();
|
|
503
|
+
expect(finalizeArgs.verifiedContentHash).toBeUndefined();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('hashes, replaces, and removes passcode protection without persisting plaintext', async () => {
|
|
507
|
+
(repository.reserveReplacement as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
508
|
+
outcome: 'reserved',
|
|
509
|
+
state: 'created',
|
|
510
|
+
share: makeShare({ status: 'active', contentState: 'finalized', version: 2 }),
|
|
511
|
+
reservation: makeReservation({
|
|
512
|
+
opKind: 'update',
|
|
513
|
+
objectRef: null,
|
|
514
|
+
contentVersion: null,
|
|
515
|
+
contentHash: null,
|
|
516
|
+
contentBytes: null,
|
|
517
|
+
recoveryHash: null,
|
|
518
|
+
recoveryBytes: null,
|
|
519
|
+
}),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const coordinator = makeCoordinator(repository, client, {
|
|
523
|
+
policyResolver: {
|
|
524
|
+
resolve: vi.fn(async () => ({
|
|
525
|
+
isMinor: false,
|
|
526
|
+
policyResolved: true,
|
|
527
|
+
defaultExpiryDays: 365,
|
|
528
|
+
viewCountingEnabled: true,
|
|
529
|
+
})),
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
await coordinator.updateShareLink(
|
|
533
|
+
{
|
|
534
|
+
id: SHARE_ID,
|
|
535
|
+
expectedVersion: 2,
|
|
536
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
537
|
+
passcode: '86428642',
|
|
538
|
+
notifyOnView: true,
|
|
539
|
+
},
|
|
540
|
+
context
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const protectedArgs = (repository.reserveReplacement as ReturnType<typeof vi.fn>).mock
|
|
544
|
+
.calls[0][0];
|
|
545
|
+
expect(protectedArgs.passcodeHash).toMatch(/^\$argon2id\$/);
|
|
546
|
+
expect(protectedArgs.passcodeHash).not.toContain('86428642');
|
|
547
|
+
expect(protectedArgs).not.toHaveProperty('passcode');
|
|
548
|
+
expect(protectedArgs.notifyOnView).toBe(true);
|
|
549
|
+
|
|
550
|
+
await coordinator.updateShareLink(
|
|
551
|
+
{
|
|
552
|
+
id: SHARE_ID,
|
|
553
|
+
expectedVersion: 2,
|
|
554
|
+
clientRequestId: '22222222-2222-4222-8222-222222222222',
|
|
555
|
+
passcode: null,
|
|
556
|
+
notifyOnView: false,
|
|
557
|
+
},
|
|
558
|
+
context
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const unprotectedArgs = (repository.reserveReplacement as ReturnType<typeof vi.fn>).mock
|
|
562
|
+
.calls[1][0];
|
|
563
|
+
expect(unprotectedArgs.passcodeHash).toBeNull();
|
|
564
|
+
expect(unprotectedArgs.notifyOnView).toBe(false);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('suppresses an update that enables notifications under restrictive policy', async () => {
|
|
568
|
+
(repository.reserveReplacement as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
569
|
+
outcome: 'reserved',
|
|
570
|
+
state: 'created',
|
|
571
|
+
share: makeShare({ status: 'active', contentState: 'finalized', version: 2 }),
|
|
572
|
+
reservation: makeReservation({
|
|
573
|
+
opKind: 'update',
|
|
574
|
+
objectRef: null,
|
|
575
|
+
contentVersion: null,
|
|
576
|
+
contentHash: null,
|
|
577
|
+
contentBytes: null,
|
|
578
|
+
recoveryHash: null,
|
|
579
|
+
recoveryBytes: null,
|
|
580
|
+
}),
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const coordinator = makeCoordinator(repository, client, {
|
|
584
|
+
policyResolver: {
|
|
585
|
+
resolve: vi.fn(async () => ({
|
|
586
|
+
isMinor: true,
|
|
587
|
+
policyResolved: true,
|
|
588
|
+
defaultExpiryDays: 30,
|
|
589
|
+
viewCountingEnabled: false,
|
|
590
|
+
})),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
await coordinator.updateShareLink(
|
|
594
|
+
{
|
|
595
|
+
id: SHARE_ID,
|
|
596
|
+
expectedVersion: 2,
|
|
597
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
598
|
+
notifyOnView: true,
|
|
599
|
+
},
|
|
600
|
+
context
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const reserveArgs = (repository.reserveReplacement as ReturnType<typeof vi.fn>).mock
|
|
604
|
+
.calls[0][0];
|
|
605
|
+
expect(reserveArgs.notifyOnView).toBe(false);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('content update uploads and finalizes the replacement', async () => {
|
|
609
|
+
(repository.reserveReplacement as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
610
|
+
outcome: 'reserved',
|
|
611
|
+
state: 'created',
|
|
612
|
+
share: makeShare({ status: 'active', contentState: 'finalized', version: 2 }),
|
|
613
|
+
reservation: makeReservation({
|
|
614
|
+
opKind: 'update',
|
|
615
|
+
contentVersion: 2,
|
|
616
|
+
baseContentVersion: 1,
|
|
617
|
+
contentHash: payloadHash,
|
|
618
|
+
}),
|
|
619
|
+
});
|
|
620
|
+
(client.stat as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
621
|
+
ok: true,
|
|
622
|
+
value: activeStat({ contentVersion: 2 }),
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const coordinator = makeCoordinator(repository, client);
|
|
626
|
+
const result = await coordinator.updateShareLink(
|
|
627
|
+
{
|
|
628
|
+
id: SHARE_ID,
|
|
629
|
+
expectedVersion: 2,
|
|
630
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
631
|
+
contentVersion: 2,
|
|
632
|
+
selectedCount: 1,
|
|
633
|
+
envelope,
|
|
634
|
+
ownerEncryptedRecovery: recovery,
|
|
635
|
+
},
|
|
636
|
+
context
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
expect(result.status).toBe('committed');
|
|
640
|
+
expect(client.put).toHaveBeenCalledTimes(1);
|
|
641
|
+
expect(client.stat).toHaveBeenCalledTimes(1);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('does not re-upload a committed update retry', async () => {
|
|
645
|
+
(repository.reserveReplacement as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
646
|
+
outcome: 'already_committed',
|
|
647
|
+
recorded: makeShare({ status: 'active', contentState: 'finalized', version: 3 }),
|
|
648
|
+
current: makeShare({ status: 'active', contentState: 'finalized', version: 3 }),
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const coordinator = makeCoordinator(repository, client);
|
|
652
|
+
const result = await coordinator.updateShareLink(
|
|
653
|
+
{
|
|
654
|
+
id: SHARE_ID,
|
|
655
|
+
expectedVersion: 2,
|
|
656
|
+
clientRequestId: CLIENT_REQUEST_ID,
|
|
657
|
+
title: 'Renamed',
|
|
658
|
+
},
|
|
659
|
+
context
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
expect(result.status).toBe('replayed');
|
|
663
|
+
expect(client.put).not.toHaveBeenCalled();
|
|
664
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
describe('share-link coordinator revoke and reads', () => {
|
|
669
|
+
it('revokes and returns the queued cleanup refs without any network call', async () => {
|
|
670
|
+
const repository = makeRepository();
|
|
671
|
+
const client = makeClient();
|
|
672
|
+
(repository.revokeShareLink as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
673
|
+
outcome: 'revoked',
|
|
674
|
+
share: makeShare({ status: 'stopped' }),
|
|
675
|
+
cleanupQueuedFor: [OBJECT_REF],
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const coordinator = makeCoordinator(repository, client);
|
|
679
|
+
const result = await coordinator.revokeShareLink(
|
|
680
|
+
{ id: SHARE_ID, clientRequestId: CLIENT_REQUEST_ID },
|
|
681
|
+
context
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
expect(result.status).toBe('revoked');
|
|
685
|
+
expect(result.cleanupQueuedFor).toEqual([OBJECT_REF]);
|
|
686
|
+
expect(client.delete).not.toHaveBeenCalled();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it.each(['stopped', 'expired', 'replaced'])(
|
|
690
|
+
'withholds content when %s during the remote read',
|
|
691
|
+
async change => {
|
|
692
|
+
const repository = makeRepository();
|
|
693
|
+
const client = makeClient();
|
|
694
|
+
const initial = {
|
|
695
|
+
state: 'active',
|
|
696
|
+
namespace: NAMESPACE,
|
|
697
|
+
ownerProfileId: OWNER,
|
|
698
|
+
shareId: SHARE_ID,
|
|
699
|
+
version: 1,
|
|
700
|
+
contentVersion: 1,
|
|
701
|
+
objectRef: OBJECT_REF,
|
|
702
|
+
operationId: OPERATION_ID,
|
|
703
|
+
};
|
|
704
|
+
(repository.getCurrentShareContent as ReturnType<typeof vi.fn>)
|
|
705
|
+
.mockResolvedValueOnce(initial)
|
|
706
|
+
.mockResolvedValueOnce(
|
|
707
|
+
change === 'replaced'
|
|
708
|
+
? { ...initial, version: 2, objectRef: 'replacement' }
|
|
709
|
+
: { state: 'not_active', reason: change }
|
|
710
|
+
);
|
|
711
|
+
(client.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
712
|
+
ok: true,
|
|
713
|
+
value: { envelope: { ct: 'withheld' } },
|
|
714
|
+
});
|
|
715
|
+
expect(
|
|
716
|
+
await makeCoordinator(repository, client).fetchShareContent(SHARE_ID, context)
|
|
717
|
+
).toEqual({ ok: false, error: 'NOT_FOUND' });
|
|
718
|
+
expect(client.get).toHaveBeenCalledOnce();
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
it('reads content with the original object operation id, never the metadata one', async () => {
|
|
723
|
+
const repository = makeRepository();
|
|
724
|
+
const client = makeClient();
|
|
725
|
+
(repository.getCurrentShareContent as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
726
|
+
state: 'active',
|
|
727
|
+
shareId: SHARE_ID,
|
|
728
|
+
namespace: NAMESPACE,
|
|
729
|
+
ownerProfileId: OWNER,
|
|
730
|
+
version: 5,
|
|
731
|
+
contentVersion: 2,
|
|
732
|
+
objectRef: OBJECT_REF,
|
|
733
|
+
operationId: OPERATION_ID,
|
|
734
|
+
contentHash: 'd'.repeat(64),
|
|
735
|
+
contentBytes: 32,
|
|
736
|
+
recoveryHash: 'c'.repeat(64),
|
|
737
|
+
selectedCount: 1,
|
|
738
|
+
expiresAt: null,
|
|
739
|
+
updatedAt: '2026-09-20T00:00:00.000Z',
|
|
740
|
+
});
|
|
741
|
+
(client.get as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: true, value: {} });
|
|
742
|
+
|
|
743
|
+
const coordinator = makeCoordinator(repository, client);
|
|
744
|
+
await coordinator.fetchShareContent(SHARE_ID, context);
|
|
745
|
+
|
|
746
|
+
const getArgs = (client.get as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
747
|
+
expect(getArgs.operationId).toBe(OPERATION_ID);
|
|
748
|
+
expect(getArgs.objectId).toBe(OBJECT_REF);
|
|
749
|
+
expect(getArgs.contentVersion).toBe(2);
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
it('allows an authenticated owner read to retain expired content semantics', async () => {
|
|
753
|
+
const repository = makeRepository();
|
|
754
|
+
const client = makeClient();
|
|
755
|
+
const current = {
|
|
756
|
+
state: 'active',
|
|
757
|
+
shareId: SHARE_ID,
|
|
758
|
+
namespace: NAMESPACE,
|
|
759
|
+
ownerProfileId: OWNER,
|
|
760
|
+
version: 5,
|
|
761
|
+
contentVersion: 2,
|
|
762
|
+
objectRef: OBJECT_REF,
|
|
763
|
+
operationId: OPERATION_ID,
|
|
764
|
+
};
|
|
765
|
+
(repository.getCurrentShareContent as ReturnType<typeof vi.fn>).mockResolvedValue(current);
|
|
766
|
+
(client.get as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: true, value: {} });
|
|
767
|
+
|
|
768
|
+
await makeCoordinator(repository, client).fetchShareContent(SHARE_ID, context, {
|
|
769
|
+
allowExpired: true,
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
expect(repository.getCurrentShareContent).toHaveBeenCalledTimes(2);
|
|
773
|
+
expect(repository.getCurrentShareContent).toHaveBeenNthCalledWith(
|
|
774
|
+
1,
|
|
775
|
+
expect.objectContaining({ allowExpired: true })
|
|
776
|
+
);
|
|
777
|
+
expect(repository.getCurrentShareContent).toHaveBeenNthCalledWith(
|
|
778
|
+
2,
|
|
779
|
+
expect.objectContaining({ allowExpired: true })
|
|
780
|
+
);
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
describe('share-link coordinator resumable pending operations', () => {
|
|
785
|
+
it('stats and finalizes a verified existing object without request bytes', async () => {
|
|
786
|
+
const repository = makeRepository();
|
|
787
|
+
const client = makeClient();
|
|
788
|
+
const coordinator = makeCoordinator(repository, client);
|
|
789
|
+
|
|
790
|
+
const result = await coordinator.resumePendingOperation({
|
|
791
|
+
reservation: makeReservation(),
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
expect(result.status).toBe('committed');
|
|
795
|
+
expect(client.put).not.toHaveBeenCalled();
|
|
796
|
+
expect(repository.finalizeReservation).toHaveBeenCalledTimes(1);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
it('abandons an absent object with cleanup instead of reconstructing ciphertext', async () => {
|
|
800
|
+
const repository = makeRepository();
|
|
801
|
+
const client = makeClient();
|
|
802
|
+
(client.stat as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
803
|
+
ok: false,
|
|
804
|
+
error: 'NOT_FOUND',
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const coordinator = makeCoordinator(repository, client);
|
|
808
|
+
const result = await coordinator.resumePendingOperation({
|
|
809
|
+
reservation: makeReservation(),
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(result.status).toBe('abandoned');
|
|
813
|
+
expect(repository.abandonReservation).toHaveBeenCalledTimes(1);
|
|
814
|
+
expect(repository.finalizeReservation).not.toHaveBeenCalled();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it('does not abandon on a transient stat failure', async () => {
|
|
818
|
+
const repository = makeRepository();
|
|
819
|
+
const client = makeClient();
|
|
820
|
+
(client.stat as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
821
|
+
ok: false,
|
|
822
|
+
error: 'TIMEOUT',
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const coordinator = makeCoordinator(repository, client);
|
|
826
|
+
const result = await coordinator.resumePendingOperation({
|
|
827
|
+
reservation: makeReservation(),
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
expect(result.status).toBe('pending');
|
|
831
|
+
expect(repository.abandonReservation).not.toHaveBeenCalled();
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('re-uploads the exact reserved ids on a stateless retry with bytes', async () => {
|
|
835
|
+
const repository = makeRepository();
|
|
836
|
+
const client = makeClient();
|
|
837
|
+
const coordinator = makeCoordinator(repository, client);
|
|
838
|
+
|
|
839
|
+
const result = await coordinator.resumePendingOperation(
|
|
840
|
+
{ reservation: makeReservation() },
|
|
841
|
+
{ envelope, ownerEncryptedRecovery: recovery }
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
expect(result.status).toBe('committed');
|
|
845
|
+
const putArgs = (client.put as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
846
|
+
expect(putArgs.objectId).toBe(OBJECT_REF);
|
|
847
|
+
expect(putArgs.operationId).toBe(OPERATION_ID);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('surfaces a typed lifecycle error instead of masking it as pending', async () => {
|
|
851
|
+
const repository = makeRepository();
|
|
852
|
+
const client = makeClient();
|
|
853
|
+
(repository.finalizeReservation as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
854
|
+
new ShareLinkRepositoryError('LEASE_EXPIRED', 'lease lapsed')
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
const coordinator = makeCoordinator(repository, client);
|
|
858
|
+
|
|
859
|
+
await expect(
|
|
860
|
+
coordinator.resumePendingOperation({ reservation: makeReservation() })
|
|
861
|
+
).rejects.toMatchObject({ code: 'LEASE_EXPIRED' });
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
describe('bounded one-shot cleanup runner', () => {
|
|
866
|
+
const job = (overrides: Record<string, unknown> = {}) => ({
|
|
867
|
+
objectRef: OBJECT_REF,
|
|
868
|
+
operationId: OPERATION_ID,
|
|
869
|
+
namespace: NAMESPACE,
|
|
870
|
+
ownerProfileId: OWNER,
|
|
871
|
+
shareId: SHARE_ID,
|
|
872
|
+
contentVersion: 1,
|
|
873
|
+
reason: 'stopped' as const,
|
|
874
|
+
status: 'claimed' as const,
|
|
875
|
+
attempts: 0,
|
|
876
|
+
nextAttemptAt: '2026-09-20T00:00:00.000Z',
|
|
877
|
+
claimToken: 'claim-1',
|
|
878
|
+
claimedBy: 'cleaner',
|
|
879
|
+
claimExpiresAt: '2099-01-01T00:00:00.000Z',
|
|
880
|
+
lastError: null,
|
|
881
|
+
createdAt: '2026-09-20T00:00:00.000Z',
|
|
882
|
+
updatedAt: '2026-09-20T00:00:00.000Z',
|
|
883
|
+
completedAt: null,
|
|
884
|
+
...overrides,
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('completes on a proven deletion using the original object operation id', async () => {
|
|
888
|
+
const repository = makeRepository();
|
|
889
|
+
const client = makeClient();
|
|
890
|
+
(repository.claimCleanupJobs as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
891
|
+
claimToken: 'claim-1',
|
|
892
|
+
jobs: [job()],
|
|
893
|
+
});
|
|
894
|
+
(repository.completeCleanupJob as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
895
|
+
outcome: 'completed',
|
|
896
|
+
job: job({ status: 'completed' }),
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const summary = await runShareContentCleanupOnce({
|
|
900
|
+
repository,
|
|
901
|
+
client,
|
|
902
|
+
claimant: 'cleaner',
|
|
903
|
+
namespace: NAMESPACE,
|
|
904
|
+
now: () => new Date('2026-09-20T00:00:00.000Z'),
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
expect(summary).toMatchObject({ claimed: 1, completed: 1, retried: 0, claimLost: 0 });
|
|
908
|
+
const deleteArgs = (client.delete as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
909
|
+
expect(deleteArgs.operationId).toBe(OPERATION_ID);
|
|
910
|
+
expect(deleteArgs.objectId).toBe(OBJECT_REF);
|
|
911
|
+
const completeArgs = (repository.completeCleanupJob as ReturnType<typeof vi.fn>).mock
|
|
912
|
+
.calls[0][0];
|
|
913
|
+
expect(completeArgs).toMatchObject({ outcome: 'completed', claimToken: 'claim-1' });
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
it('returns an unavailable deletion to the queue with backoff', async () => {
|
|
917
|
+
const repository = makeRepository();
|
|
918
|
+
const client = makeClient();
|
|
919
|
+
(repository.claimCleanupJobs as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
920
|
+
claimToken: 'claim-1',
|
|
921
|
+
jobs: [job()],
|
|
922
|
+
});
|
|
923
|
+
(client.delete as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
924
|
+
ok: false,
|
|
925
|
+
error: 'UNAVAILABLE',
|
|
926
|
+
});
|
|
927
|
+
(repository.completeCleanupJob as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
928
|
+
outcome: 'retry',
|
|
929
|
+
job: job({ status: 'queued', attempts: 1 }),
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const summary = await runShareContentCleanupOnce({
|
|
933
|
+
repository,
|
|
934
|
+
client,
|
|
935
|
+
claimant: 'cleaner',
|
|
936
|
+
namespace: NAMESPACE,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
expect(summary).toMatchObject({ claimed: 1, completed: 0, retried: 1 });
|
|
940
|
+
expect(
|
|
941
|
+
(repository.completeCleanupJob as ReturnType<typeof vi.fn>).mock.calls[0][0]
|
|
942
|
+
).toMatchObject({ outcome: 'retry', errorMessage: 'UNAVAILABLE' });
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('never completes a deterministic failure and leaves the claim fenced', async () => {
|
|
946
|
+
const repository = makeRepository();
|
|
947
|
+
const client = makeClient();
|
|
948
|
+
(repository.claimCleanupJobs as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
949
|
+
claimToken: 'claim-1',
|
|
950
|
+
jobs: [job()],
|
|
951
|
+
});
|
|
952
|
+
(client.delete as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
953
|
+
ok: false,
|
|
954
|
+
error: 'UNAUTHORIZED',
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
const summary = await runShareContentCleanupOnce({
|
|
958
|
+
repository,
|
|
959
|
+
client,
|
|
960
|
+
claimant: 'cleaner',
|
|
961
|
+
namespace: NAMESPACE,
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
expect(summary).toMatchObject({ claimed: 1, completed: 0, retried: 0, claimLost: 0 });
|
|
965
|
+
expect(summary.skipped).toBe(1);
|
|
966
|
+
expect(summary.categories).toEqual({ UNAUTHORIZED: 1 });
|
|
967
|
+
expect(repository.completeCleanupJob).not.toHaveBeenCalled();
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it('records a lost claim without pretending the deletion happened', async () => {
|
|
971
|
+
const repository = makeRepository();
|
|
972
|
+
const client = makeClient();
|
|
973
|
+
(repository.claimCleanupJobs as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
974
|
+
claimToken: 'old-claim',
|
|
975
|
+
jobs: [job()],
|
|
976
|
+
});
|
|
977
|
+
(repository.completeCleanupJob as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
978
|
+
outcome: 'claim_lost',
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const summary = await runShareContentCleanupOnce({
|
|
982
|
+
repository,
|
|
983
|
+
client,
|
|
984
|
+
claimant: 'cleaner',
|
|
985
|
+
namespace: NAMESPACE,
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
expect(summary).toMatchObject({ claimed: 1, completed: 0, claimLost: 1 });
|
|
989
|
+
});
|
|
990
|
+
});
|