@learncard/network-brain-service 3.19.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +47 -82
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +147 -0
- package/README.md +51 -2
- package/bootstrapLambda.test.ts +72 -0
- package/config/config.dev.json +29 -0
- package/config/config.example.json +29 -0
- package/config/config.json +1 -0
- package/config/config.production.json +29 -0
- package/config/config.scouts.dev.json +24 -0
- package/config/config.scouts.production.json +24 -0
- package/contractEventsLambda.ts +18 -0
- package/contractEventsLambdaApp.ts +19 -0
- package/didWebLambda.ts +13 -18
- package/didWebLambdaApp.ts +22 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
- package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
- package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
- package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/accesslayer/presentation/read.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
- package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts +3 -0
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
- package/dist/accesslayer/share-link/attachment.d.ts +37 -0
- package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
- package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
- package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
- package/dist/accesslayer/share-link/errors.d.ts +8 -0
- package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts +3 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
- package/dist/accesslayer/share-link/helpers.d.ts +59 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
- package/dist/accesslayer/share-link/index.d.ts +12 -0
- package/dist/accesslayer/share-link/index.d.ts.map +1 -0
- package/dist/accesslayer/share-link/list.d.ts +5 -0
- package/dist/accesslayer/share-link/list.d.ts.map +1 -0
- package/dist/accesslayer/share-link/read.d.ts +5 -0
- package/dist/accesslayer/share-link/read.d.ts.map +1 -0
- package/dist/accesslayer/share-link/receipt.d.ts +11 -0
- package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
- package/dist/accesslayer/share-link/recovery.d.ts +6 -0
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
- package/dist/accesslayer/share-link/reserve.d.ts +4 -0
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
- package/dist/accesslayer/share-link/revoke.d.ts +3 -0
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction.d.ts +15 -0
- package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
- package/dist/accesslayer/share-link/types.d.ts +375 -0
- package/dist/accesslayer/share-link/types.d.ts.map +1 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +49 -0
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/environment.d.ts +14 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +112 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/constants/auth-grant.d.ts +2 -0
- package/dist/constants/auth-grant.d.ts.map +1 -1
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/helpers/app-manifest.helpers.d.ts +5 -0
- package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
- package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
- package/dist/helpers/consent-referral.helpers.d.ts +2 -0
- package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
- package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
- package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts +25 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-requests.helpers.d.ts +19 -0
- package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
- package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
- package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inline-template.helpers.d.ts +45 -0
- package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts +6 -0
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/presentation-metadata.d.ts +4 -0
- package/dist/helpers/presentation-metadata.d.ts.map +1 -0
- package/dist/helpers/presentation.helpers.d.ts +1 -1
- package/dist/helpers/presentation.helpers.d.ts.map +1 -1
- package/dist/helpers/profile.helpers.d.ts +2 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/share-content-client/adapters.d.ts +10 -0
- package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
- package/dist/helpers/share-content-client/canonical.d.ts +7 -0
- package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
- package/dist/helpers/share-content-client/client.d.ts +5 -0
- package/dist/helpers/share-content-client/client.d.ts.map +1 -0
- package/dist/helpers/share-content-client/config.d.ts +11 -0
- package/dist/helpers/share-content-client/config.d.ts.map +1 -0
- package/dist/helpers/share-content-client/index.d.ts +6 -0
- package/dist/helpers/share-content-client/index.d.ts.map +1 -0
- package/dist/helpers/share-content-client/responses.d.ts +16 -0
- package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
- package/dist/helpers/share-content-client/types.d.ts +145 -0
- package/dist/helpers/share-content-client/types.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment-access.d.ts +4 -0
- package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment.d.ts +18 -0
- package/dist/helpers/share-link-attachment.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
- package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
- package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
- package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts +20 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts +38 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/types.d.ts +56 -0
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/config.d.ts +16 -0
- package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
- package/dist/helpers/share-link-owner-projection.d.ts +4 -0
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode.d.ts +6 -0
- package/dist/helpers/share-link-passcode.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/index.d.ts +4 -0
- package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/production.d.ts +11 -0
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/types.d.ts +22 -0
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
- package/dist/helpers/share-link-public/config.d.ts +13 -0
- package/dist/helpers/share-link-public/config.d.ts.map +1 -0
- package/dist/helpers/share-link-view-notification.d.ts +2 -0
- package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/smartResume.helpers.d.ts +3 -0
- package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AppManifestVersion.d.ts +20 -0
- package/dist/models/AppManifestVersion.d.ts.map +1 -0
- package/dist/models/AppStoreListing.d.ts +13 -0
- package/dist/models/AppStoreListing.d.ts.map +1 -1
- package/dist/models/ConsentFlowContract.d.ts +13 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +2 -0
- package/dist/models/Integration.d.ts.map +1 -1
- package/dist/models/Presentation.d.ts +2 -0
- package/dist/models/Presentation.d.ts.map +1 -1
- package/dist/models/Profile.d.ts +2 -0
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +46 -0
- package/dist/models/ShareLink.d.ts.map +1 -0
- package/dist/models/ShareViewReceipt.d.ts +22 -0
- package/dist/models/ShareViewReceipt.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/share-link-constraints.d.ts +3 -0
- package/dist/models/share-link-constraints.d.ts.map +1 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/public-share-link-http.d.ts +4 -0
- package/dist/public-share-link-http.d.ts.map +1 -0
- package/dist/routes/app-store.d.ts +619 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +2 -2
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +148 -5
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +35 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +52 -2
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +153 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +1 -0
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +285 -0
- package/dist/routes/public-share-links.d.ts.map +1 -0
- package/dist/routes/share-links.d.ts +1005 -0
- package/dist/routes/share-links.d.ts.map +1 -0
- package/dist/routes/test.d.ts +5 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/types/app-manifest.d.ts +339 -0
- package/dist/types/app-manifest.d.ts.map +1 -0
- package/dist/types/app-store-listing.d.ts +34 -0
- package/dist/types/app-store-listing.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +31 -4
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +1 -1
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +49 -192
- package/lambdaApp.ts +203 -0
- package/package.json +25 -17
- package/scripts/export-openapi.ts +20 -0
- package/scripts/offline-schema-environment.ts +18 -0
- package/scripts/seed-demo-persona.ts +425 -0
- package/scripts/verify-python-contracts.ts +72 -0
- package/serverless-local.yml +3 -6
- package/serverless.function-env.cjs +50 -0
- package/serverless.local.yml +0 -1
- package/serverless.yml +44 -55
- package/shareLinkMaintenanceLambda.ts +18 -0
- package/shareLinkMaintenanceLambdaApp.ts +50 -0
- package/src/accesslayer/app-manifest-version/create.ts +57 -0
- package/src/accesslayer/app-manifest-version/read.ts +129 -0
- package/src/accesslayer/app-manifest-version/update.ts +61 -0
- package/src/accesslayer/app-store-listing/read.ts +6 -0
- package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
- package/src/accesslayer/app-store-listing/update.ts +15 -0
- package/src/accesslayer/boost/relationships/create.ts +8 -2
- package/src/accesslayer/consentflowcontract/create.ts +81 -8
- package/src/accesslayer/consentflowcontract/read.ts +84 -10
- package/src/accesslayer/consentflowcontract/relationships/create.ts +125 -109
- package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
- package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
- package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/update.ts +255 -217
- package/src/accesslayer/credential/create.test.ts +2 -115
- package/src/accesslayer/credential/create.ts +11 -6
- package/src/accesslayer/credential/relationships/create.ts +17 -10
- package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
- package/src/accesslayer/inbox-credential/update.ts +4 -2
- package/src/accesslayer/presentation/read.ts +57 -25
- package/src/accesslayer/presentation/relationships/create.ts +54 -11
- package/src/accesslayer/presentation/relationships/read.ts +21 -6
- package/src/accesslayer/share-link/abandon.ts +120 -0
- package/src/accesslayer/share-link/attachment.ts +360 -0
- package/src/accesslayer/share-link/cleanup.ts +271 -0
- package/src/accesslayer/share-link/errors.ts +32 -0
- package/src/accesslayer/share-link/finalize.ts +313 -0
- package/src/accesslayer/share-link/helpers.ts +450 -0
- package/src/accesslayer/share-link/index.ts +11 -0
- package/src/accesslayer/share-link/list.ts +162 -0
- package/src/accesslayer/share-link/read.ts +107 -0
- package/src/accesslayer/share-link/receipt.ts +548 -0
- package/src/accesslayer/share-link/recovery.ts +565 -0
- package/src/accesslayer/share-link/reserve.ts +763 -0
- package/src/accesslayer/share-link/revoke.ts +232 -0
- package/src/accesslayer/share-link/transaction-execution.ts +72 -0
- package/src/accesslayer/share-link/transaction.ts +126 -0
- package/src/accesslayer/share-link/types.ts +606 -0
- package/src/app.ts +6 -0
- package/src/cache/app-store.caches.ts +35 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/environment.test.ts +17 -12
- package/src/config/environment.ts +15 -0
- package/src/config/serverlessFunctionEnv.test.ts +105 -0
- package/src/config/stageConfig.test.ts +25 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/constants/auth-grant.ts +5 -0
- package/src/docker-entry.ts +81 -0
- package/src/helpers/app-manifest.helpers.ts +185 -0
- package/src/helpers/boost.helpers.ts +4 -31
- package/src/helpers/claim-hooks.helpers.ts +20 -7
- package/src/helpers/consent-referral.helpers.ts +6 -0
- package/src/helpers/consent-scopes.helpers.ts +58 -0
- package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
- package/src/helpers/contract-events.helpers.ts +540 -0
- package/src/helpers/contract-requests.helpers.ts +211 -0
- package/src/helpers/credentialSubject.helpers.test.ts +114 -0
- package/src/helpers/credentialSubject.helpers.ts +40 -0
- package/src/helpers/finalize-inbox.helpers.ts +163 -44
- package/src/helpers/inbox.helpers.ts +58 -70
- package/src/helpers/inline-template.helpers.ts +560 -0
- package/src/helpers/notificationMessages.ts +77 -1
- package/src/helpers/notificationQueue.helpers.ts +23 -5
- package/src/helpers/notifications.helpers.ts +25 -1
- package/src/helpers/presentation-metadata.ts +41 -0
- package/src/helpers/presentation.helpers.ts +27 -20
- package/src/helpers/profile.helpers.ts +13 -0
- package/src/helpers/share-content-client/adapters.ts +55 -0
- package/src/helpers/share-content-client/canonical.ts +25 -0
- package/src/helpers/share-content-client/client.ts +440 -0
- package/src/helpers/share-content-client/config.ts +189 -0
- package/src/helpers/share-content-client/index.ts +27 -0
- package/src/helpers/share-content-client/responses.ts +273 -0
- package/src/helpers/share-content-client/types.ts +253 -0
- package/src/helpers/share-link-attachment-access.ts +87 -0
- package/src/helpers/share-link-attachment.ts +115 -0
- package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
- package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
- package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +727 -0
- package/src/helpers/share-link-coordinator/index.ts +8 -0
- package/src/helpers/share-link-coordinator/recovery-runner.ts +443 -0
- package/src/helpers/share-link-coordinator/runtime.ts +88 -0
- package/src/helpers/share-link-coordinator/stat.ts +30 -0
- package/src/helpers/share-link-coordinator/types.ts +353 -0
- package/src/helpers/share-link-lifecycle/index.ts +4 -0
- package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
- package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
- package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
- package/src/helpers/share-link-lifecycle/types.ts +58 -0
- package/src/helpers/share-link-maintenance/budget.ts +63 -0
- package/src/helpers/share-link-maintenance/config.ts +312 -0
- package/src/helpers/share-link-maintenance/index.ts +24 -0
- package/src/helpers/share-link-maintenance/runner.ts +251 -0
- package/src/helpers/share-link-maintenance/runtime.ts +340 -0
- package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +73 -0
- package/src/helpers/share-link-maintenance/types.ts +91 -0
- package/src/helpers/share-link-owner/config.ts +48 -0
- package/src/helpers/share-link-owner/diagnostics.ts +89 -0
- package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
- package/src/helpers/share-link-owner-projection.ts +54 -0
- package/src/helpers/share-link-passcode-abuse.ts +36 -0
- package/src/helpers/share-link-passcode.ts +47 -0
- package/src/helpers/share-link-policy/index.ts +7 -0
- package/src/helpers/share-link-policy/production.ts +108 -0
- package/src/helpers/share-link-policy/resolver.ts +113 -0
- package/src/helpers/share-link-policy/types.ts +56 -0
- package/src/helpers/share-link-public/config.ts +41 -0
- package/src/helpers/share-link-view-notification.ts +18 -0
- package/src/helpers/signingAuthority.helpers.ts +23 -14
- package/src/helpers/smartResume.helpers.ts +123 -0
- package/src/models/AppManifestVersion.ts +43 -0
- package/src/models/AppStoreListing.ts +32 -0
- package/src/models/ConsentFlowContract.ts +34 -3
- package/src/models/ConsentFlowTerms.ts +10 -0
- package/src/models/ConsentFlowTransaction.ts +3 -0
- package/src/models/Integration.ts +2 -0
- package/src/models/Presentation.ts +2 -2
- package/src/models/Profile.ts +3 -2
- package/src/models/ShareLink.ts +134 -0
- package/src/models/ShareViewReceipt.ts +69 -0
- package/src/models/index.ts +25 -0
- package/src/models/share-link-constraints.ts +98 -0
- package/src/openapi.ts +44 -8
- package/src/public-share-link-http.ts +49 -0
- package/src/routes/app-store.ts +1056 -38
- package/src/routes/boosts.ts +14 -25
- package/src/routes/contracts.ts +384 -273
- package/src/routes/inbox.ts +4 -2
- package/src/routes/index.ts +142 -44
- package/src/routes/presentations.ts +19 -3
- package/src/routes/profile-manager.ts +29 -2
- package/src/routes/profiles.ts +29 -6
- package/src/routes/public-share-links.ts +983 -0
- package/src/routes/share-links.ts +709 -0
- package/src/routes/workflows.ts +3 -2
- package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
- package/src/types/app-manifest.ts +28 -0
- package/src/types/app-store-listing.ts +13 -0
- package/src/types/consentflowcontract.ts +25 -4
- package/test/app-manifest.helpers.spec.ts +170 -0
- package/test/app-manifest.routes.spec.ts +299 -0
- package/test/app-store-listing-updates.spec.ts +236 -0
- package/test/app-store-listing.spec.ts +65 -3
- package/test/consent-data-boundaries.spec.ts +352 -0
- package/test/consent-recipients.spec.ts +513 -0
- package/test/consent-scopes.helpers.spec.ts +67 -0
- package/test/consentflow.spec.ts +63 -2
- package/test/contract-requests.spec.ts +1588 -0
- package/test/credential-storage.neo4j.spec.ts +122 -0
- package/test/dids.spec.ts +10 -1
- package/test/guardian-gated-routes.spec.ts +49 -0
- package/test/helpers/share-link-fixtures.ts +94 -0
- package/test/inbox-security.e2e.ts +26 -10
- package/test/inbox.spec.ts +184 -8
- package/test/inline-template.helpers.spec.ts +49 -0
- package/test/managed-recipient-inbox.spec.ts +152 -0
- package/test/notificationMessages.spec.ts +13 -0
- package/test/openapi.spec.ts +14 -53
- package/test/presentations.spec.ts +54 -6
- package/test/profiles.spec.ts +97 -1
- package/test/share-content-client.contract.spec.ts +365 -0
- package/test/share-content-client.unit.spec.ts +462 -0
- package/test/share-link-abuse.unit.spec.ts +65 -0
- package/test/share-link-attachment.neo4j.spec.ts +262 -0
- package/test/share-link-attachment.unit.spec.ts +244 -0
- package/test/share-link-coordinator.neo4j.spec.ts +684 -0
- package/test/share-link-coordinator.unit.spec.ts +990 -0
- package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +1411 -0
- package/test/share-link-lifecycle.unit.spec.ts +205 -0
- package/test/share-link-list-cursor.unit.spec.ts +86 -0
- package/test/share-link-list.neo4j.spec.ts +324 -0
- package/test/share-link-maintenance.neo4j.spec.ts +355 -0
- package/test/share-link-maintenance.unit.spec.ts +1140 -0
- package/test/share-link-owner-policy.unit.spec.ts +210 -0
- package/test/share-link-owner-route.unit.spec.ts +820 -0
- package/test/share-link-owner-runtime.unit.spec.ts +102 -0
- package/test/share-link-passcode.unit.spec.ts +29 -0
- package/test/share-link-policy.unit.spec.ts +149 -0
- package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
- package/test/share-link-production-policy.unit.spec.ts +126 -0
- package/test/share-link-public-config.unit.spec.ts +64 -0
- package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
- package/test/share-link-public-route.unit.spec.ts +1079 -0
- package/test/share-link-receipt.neo4j.spec.ts +943 -0
- package/test/share-link-recovery.neo4j.spec.ts +935 -0
- package/test/share-link-recovery.unit.spec.ts +726 -0
- package/test/share-link-service-policy.neo4j.spec.ts +403 -0
- package/test/signing-authority-encryption.spec.ts +75 -0
- package/test/smart-resume-consent.spec.ts +233 -0
- package/test/smart-resume-upload.spec.ts +65 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +14 -0
- package/vitest.inbox-e2e.config.ts +2 -0
- package/vitest.share-links-neo4j.config.ts +19 -0
- package/vitest.share-links-unit.config.ts +28 -0
- package/workerLambda.test.ts +100 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { neogma } from '@instance';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
consumeShareViewReceipt,
|
|
9
|
+
hashShareViewReceipt,
|
|
10
|
+
persistShareViewReceipt,
|
|
11
|
+
pruneShareViewReceipts,
|
|
12
|
+
readShareViewReceiptOwner,
|
|
13
|
+
} from '../src/accesslayer/share-link/receipt';
|
|
14
|
+
import type { ShareViewEligibilitySource } from '../src/accesslayer/share-link/types';
|
|
15
|
+
import { ensureShareLinkConstraints } from '../src/models/share-link-constraints';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* LC-2187 view-receipt repository against a REAL Neo4j (disposable
|
|
19
|
+
* testcontainer through the shared harness).
|
|
20
|
+
*
|
|
21
|
+
* The graph locking, uniqueness constraint and consume+increment transaction are
|
|
22
|
+
* real. There is no remote store or policy network here: the eligible-policy
|
|
23
|
+
* source below reads a PERSISTED test policy node inside the same transaction,
|
|
24
|
+
* so it proves the under-lock check observes a concurrent tightening rather than
|
|
25
|
+
* asserting a constant callback.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const NAMESPACE = 'receipt-test-ns';
|
|
29
|
+
const OWNER = 'owner-1';
|
|
30
|
+
const NOW = new Date('2026-09-21T00:00:00.000Z');
|
|
31
|
+
const OBJECT_REF = 'object-ref';
|
|
32
|
+
const OPERATION_ID = randomUUID();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Test-only persisted policy source. It reads a `TestShareViewPolicy` node with
|
|
36
|
+
* the SAME transaction runner the receipt check uses, so a policy change
|
|
37
|
+
* committed while the consume transaction is blocked on the share lock is
|
|
38
|
+
* observed by the under-lock re-read.
|
|
39
|
+
*/
|
|
40
|
+
const testPolicySource: ShareViewEligibilitySource = {
|
|
41
|
+
isEligible: async (tx, input) => {
|
|
42
|
+
const result = await tx.run(
|
|
43
|
+
`MATCH (p:TestShareViewPolicy {namespace: $namespace, ownerProfileId: $owner})
|
|
44
|
+
RETURN p.eligible AS eligible`,
|
|
45
|
+
{ namespace: input.namespace, owner: input.ownerProfileId }
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return result.records[0]?.get('eligible') === true;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const denyPolicySource: ShareViewEligibilitySource = {
|
|
53
|
+
isEligible: async () => false,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const setTestPolicy = async (eligible: boolean, namespace = NAMESPACE): Promise<void> => {
|
|
57
|
+
await neogma.queryRunner.run(
|
|
58
|
+
`MERGE (p:TestShareViewPolicy {namespace: $namespace, ownerProfileId: $owner})
|
|
59
|
+
SET p.eligible = $eligible`,
|
|
60
|
+
{ namespace, owner: OWNER, eligible }
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type SeedOverrides = Partial<{
|
|
65
|
+
namespace: string;
|
|
66
|
+
status: string;
|
|
67
|
+
contentState: string;
|
|
68
|
+
contentVersion: number;
|
|
69
|
+
expiresAt: string | null;
|
|
70
|
+
viewCountingEnabled: boolean;
|
|
71
|
+
policyResolved: boolean;
|
|
72
|
+
isMinor: boolean | null;
|
|
73
|
+
}>;
|
|
74
|
+
|
|
75
|
+
const seedShare = async (overrides: SeedOverrides = {}): Promise<string> => {
|
|
76
|
+
const id = randomBytes(16).toString('base64url');
|
|
77
|
+
const props: Record<string, unknown> = {
|
|
78
|
+
id,
|
|
79
|
+
namespace: overrides.namespace ?? NAMESPACE,
|
|
80
|
+
ownerProfileId: OWNER,
|
|
81
|
+
version: 1,
|
|
82
|
+
contentVersion: overrides.contentVersion ?? 1,
|
|
83
|
+
generation: 1,
|
|
84
|
+
status: overrides.status ?? 'active',
|
|
85
|
+
contentState: overrides.contentState ?? 'finalized',
|
|
86
|
+
activeObjectRef: OBJECT_REF,
|
|
87
|
+
activeObjectOperationId: OPERATION_ID,
|
|
88
|
+
activeContentHash: 'content-hash',
|
|
89
|
+
activeContentBytes: 32,
|
|
90
|
+
activeRecoveryHash: 'recovery-hash',
|
|
91
|
+
lastOperationId: randomUUID(),
|
|
92
|
+
createdByClientRequestId: randomUUID(),
|
|
93
|
+
title: 'Shared credentials',
|
|
94
|
+
selectedCount: 1,
|
|
95
|
+
viewCount: 0,
|
|
96
|
+
minorPolicyIsMinor: overrides.isMinor ?? false,
|
|
97
|
+
minorPolicyResolved: overrides.policyResolved ?? true,
|
|
98
|
+
minorPolicyDefaultExpiryDays: 365,
|
|
99
|
+
minorPolicyViewCountingEnabled: overrides.viewCountingEnabled ?? true,
|
|
100
|
+
createdAt: NOW.toISOString(),
|
|
101
|
+
updatedAt: NOW.toISOString(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (overrides.expiresAt) props.expiresAt = overrides.expiresAt;
|
|
105
|
+
|
|
106
|
+
await neogma.queryRunner.run('CREATE (s:ShareLink) SET s = $props', { props });
|
|
107
|
+
|
|
108
|
+
return id;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const readShare = async (
|
|
112
|
+
shareId: string
|
|
113
|
+
): Promise<{ viewCount: number; lastViewedAt: string | null }> => {
|
|
114
|
+
const result = await neogma.queryRunner.run(
|
|
115
|
+
'MATCH (s:ShareLink {id: $shareId}) RETURN s.viewCount AS viewCount, s.lastViewedAt AS lastViewedAt',
|
|
116
|
+
{ shareId }
|
|
117
|
+
);
|
|
118
|
+
const record = result.records[0];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
viewCount: Number(record?.get('viewCount') ?? 0),
|
|
122
|
+
lastViewedAt: (record?.get('lastViewedAt') as string | null) ?? null,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const readConsumedAt = async (receipt: string): Promise<string | null> => {
|
|
127
|
+
const result = await neogma.queryRunner.run(
|
|
128
|
+
'MATCH (v:ShareViewReceipt {receiptHash: $receiptHash}) RETURN v.consumedAt AS consumedAt',
|
|
129
|
+
{ receiptHash: hashShareViewReceipt(receipt) }
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (result.records[0]?.get('consumedAt') as string | null) ?? null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const receiptFor = (): string => randomBytes(32).toString('base64url');
|
|
136
|
+
|
|
137
|
+
const clearGraph = async (): Promise<void> => {
|
|
138
|
+
await neogma.queryRunner.run(
|
|
139
|
+
`MATCH (n) WHERE n:ShareLink OR n:ShareViewReceipt OR n:TestShareViewPolicy DETACH DELETE n`
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
describe('share view receipt repository (real Neo4j)', () => {
|
|
144
|
+
beforeAll(async () => {
|
|
145
|
+
await ensureShareLinkConstraints();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
beforeEach(async () => {
|
|
149
|
+
await clearGraph();
|
|
150
|
+
// Every test starts with an explicitly persisted, eligible test policy.
|
|
151
|
+
await setTestPolicy(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterAll(async () => {
|
|
155
|
+
await clearGraph();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it.each(['persist', 'consume'] as const)(
|
|
159
|
+
'rechecks expiry after awaited eligibility during %s',
|
|
160
|
+
async stage => {
|
|
161
|
+
const shareId = await seedShare({
|
|
162
|
+
expiresAt: new Date(NOW.getTime() + 1000).toISOString(),
|
|
163
|
+
});
|
|
164
|
+
const receipt = receiptFor();
|
|
165
|
+
let clock = NOW;
|
|
166
|
+
const slowSource: ShareViewEligibilitySource = {
|
|
167
|
+
isEligible: async (tx, input) => {
|
|
168
|
+
const eligible = await testPolicySource.isEligible(tx, input);
|
|
169
|
+
clock = new Date(NOW.getTime() + 2000);
|
|
170
|
+
return eligible;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const input = {
|
|
174
|
+
receipt,
|
|
175
|
+
namespace: NAMESPACE,
|
|
176
|
+
ownerProfileId: OWNER,
|
|
177
|
+
shareId,
|
|
178
|
+
contentVersion: 1,
|
|
179
|
+
shareVersion: 1,
|
|
180
|
+
objectRef: OBJECT_REF,
|
|
181
|
+
operationId: OPERATION_ID,
|
|
182
|
+
ttlSeconds: 600,
|
|
183
|
+
now: () => clock,
|
|
184
|
+
eligibilitySource: stage === 'persist' ? slowSource : testPolicySource,
|
|
185
|
+
};
|
|
186
|
+
if (stage === 'persist') {
|
|
187
|
+
expect(await persistShareViewReceipt(input)).toBe(false);
|
|
188
|
+
} else {
|
|
189
|
+
expect(await persistShareViewReceipt(input)).toBe(true);
|
|
190
|
+
expect(
|
|
191
|
+
await consumeShareViewReceipt({
|
|
192
|
+
receipt,
|
|
193
|
+
namespace: NAMESPACE,
|
|
194
|
+
now: () => clock,
|
|
195
|
+
eligibilitySource: slowSource,
|
|
196
|
+
})
|
|
197
|
+
).not.toBe('consumed');
|
|
198
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
it('does not persist when the committed counting policy is disabled', async () => {
|
|
204
|
+
const shareId = await seedShare({ viewCountingEnabled: false });
|
|
205
|
+
const receipt = receiptFor();
|
|
206
|
+
expect(
|
|
207
|
+
await persistShareViewReceipt({
|
|
208
|
+
receipt,
|
|
209
|
+
namespace: NAMESPACE,
|
|
210
|
+
ownerProfileId: OWNER,
|
|
211
|
+
shareId,
|
|
212
|
+
contentVersion: 1,
|
|
213
|
+
shareVersion: 1,
|
|
214
|
+
objectRef: OBJECT_REF,
|
|
215
|
+
operationId: OPERATION_ID,
|
|
216
|
+
ttlSeconds: 600,
|
|
217
|
+
now: () => NOW,
|
|
218
|
+
eligibilitySource: testPolicySource,
|
|
219
|
+
})
|
|
220
|
+
).toBe(false);
|
|
221
|
+
expect(await readShareViewReceiptOwner(receipt, NAMESPACE)).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('rejects a receipt for a different immutable object at the same content version', async () => {
|
|
225
|
+
const shareId = await seedShare();
|
|
226
|
+
const receipt = receiptFor();
|
|
227
|
+
await persistShareViewReceipt({
|
|
228
|
+
receipt,
|
|
229
|
+
namespace: NAMESPACE,
|
|
230
|
+
ownerProfileId: OWNER,
|
|
231
|
+
shareId,
|
|
232
|
+
contentVersion: 1,
|
|
233
|
+
shareVersion: 1,
|
|
234
|
+
objectRef: 'different-object',
|
|
235
|
+
operationId: OPERATION_ID,
|
|
236
|
+
ttlSeconds: 600,
|
|
237
|
+
now: () => NOW,
|
|
238
|
+
eligibilitySource: testPolicySource,
|
|
239
|
+
});
|
|
240
|
+
expect(
|
|
241
|
+
await consumeShareViewReceipt({
|
|
242
|
+
receipt,
|
|
243
|
+
namespace: NAMESPACE,
|
|
244
|
+
eligibilitySource: testPolicySource,
|
|
245
|
+
now: () => NOW,
|
|
246
|
+
})
|
|
247
|
+
).not.toBe('consumed');
|
|
248
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('persists, consumes and increments exactly once, then rejects the duplicate', async () => {
|
|
252
|
+
const shareId = await seedShare();
|
|
253
|
+
const receipt = receiptFor();
|
|
254
|
+
|
|
255
|
+
await persistShareViewReceipt({
|
|
256
|
+
receipt,
|
|
257
|
+
namespace: NAMESPACE,
|
|
258
|
+
ownerProfileId: OWNER,
|
|
259
|
+
shareId,
|
|
260
|
+
contentVersion: 1,
|
|
261
|
+
shareVersion: 1,
|
|
262
|
+
objectRef: OBJECT_REF,
|
|
263
|
+
operationId: OPERATION_ID,
|
|
264
|
+
ttlSeconds: 600,
|
|
265
|
+
now: () => NOW,
|
|
266
|
+
eligibilitySource: testPolicySource,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(await readShareViewReceiptOwner(receipt, NAMESPACE)).toBe(OWNER);
|
|
270
|
+
|
|
271
|
+
await expect(
|
|
272
|
+
consumeShareViewReceipt({
|
|
273
|
+
receipt,
|
|
274
|
+
namespace: NAMESPACE,
|
|
275
|
+
eligibilitySource: testPolicySource,
|
|
276
|
+
now: () => NOW,
|
|
277
|
+
})
|
|
278
|
+
).resolves.toBe('consumed');
|
|
279
|
+
|
|
280
|
+
const after = await readShare(shareId);
|
|
281
|
+
expect(after.viewCount).toBe(1);
|
|
282
|
+
expect(after.lastViewedAt).not.toBeNull();
|
|
283
|
+
expect(await readConsumedAt(receipt)).not.toBeNull();
|
|
284
|
+
|
|
285
|
+
await expect(
|
|
286
|
+
consumeShareViewReceipt({
|
|
287
|
+
receipt,
|
|
288
|
+
namespace: NAMESPACE,
|
|
289
|
+
eligibilitySource: testPolicySource,
|
|
290
|
+
now: () => NOW,
|
|
291
|
+
})
|
|
292
|
+
).resolves.toBe('already_consumed');
|
|
293
|
+
expect((await readShare(shareId)).viewCount).toBe(1);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('increments exactly once under concurrent duplicate acknowledgements', async () => {
|
|
297
|
+
const shareId = await seedShare();
|
|
298
|
+
const receipt = receiptFor();
|
|
299
|
+
|
|
300
|
+
await persistShareViewReceipt({
|
|
301
|
+
receipt,
|
|
302
|
+
namespace: NAMESPACE,
|
|
303
|
+
ownerProfileId: OWNER,
|
|
304
|
+
shareId,
|
|
305
|
+
contentVersion: 1,
|
|
306
|
+
shareVersion: 1,
|
|
307
|
+
objectRef: OBJECT_REF,
|
|
308
|
+
operationId: OPERATION_ID,
|
|
309
|
+
ttlSeconds: 600,
|
|
310
|
+
now: () => NOW,
|
|
311
|
+
eligibilitySource: testPolicySource,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const outcomes = await Promise.all(
|
|
315
|
+
Array.from({ length: 5 }, () =>
|
|
316
|
+
consumeShareViewReceipt({
|
|
317
|
+
receipt,
|
|
318
|
+
namespace: NAMESPACE,
|
|
319
|
+
eligibilitySource: testPolicySource,
|
|
320
|
+
now: () => NOW,
|
|
321
|
+
})
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(outcomes.filter(outcome => outcome === 'consumed')).toHaveLength(1);
|
|
326
|
+
expect(
|
|
327
|
+
outcomes.every(outcome => outcome === 'consumed' || outcome === 'already_consumed')
|
|
328
|
+
).toBe(true);
|
|
329
|
+
expect((await readShare(shareId)).viewCount).toBe(1);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('persists nothing and counts nothing for an ineligible, unknown or padding token', async () => {
|
|
333
|
+
const shareId = await seedShare();
|
|
334
|
+
|
|
335
|
+
// Unknown/padding token: never in the graph.
|
|
336
|
+
await expect(
|
|
337
|
+
consumeShareViewReceipt({
|
|
338
|
+
receipt: receiptFor(),
|
|
339
|
+
namespace: NAMESPACE,
|
|
340
|
+
eligibilitySource: testPolicySource,
|
|
341
|
+
now: () => NOW,
|
|
342
|
+
})
|
|
343
|
+
).resolves.toBe('not_found');
|
|
344
|
+
|
|
345
|
+
// Persisted receipt but the fresh authoritative policy says ineligible.
|
|
346
|
+
const receipt = receiptFor();
|
|
347
|
+
await persistShareViewReceipt({
|
|
348
|
+
receipt,
|
|
349
|
+
namespace: NAMESPACE,
|
|
350
|
+
ownerProfileId: OWNER,
|
|
351
|
+
shareId,
|
|
352
|
+
contentVersion: 1,
|
|
353
|
+
shareVersion: 1,
|
|
354
|
+
objectRef: OBJECT_REF,
|
|
355
|
+
operationId: OPERATION_ID,
|
|
356
|
+
ttlSeconds: 600,
|
|
357
|
+
now: () => NOW,
|
|
358
|
+
eligibilitySource: testPolicySource,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await expect(
|
|
362
|
+
consumeShareViewReceipt({
|
|
363
|
+
receipt,
|
|
364
|
+
namespace: NAMESPACE,
|
|
365
|
+
eligibilitySource: denyPolicySource,
|
|
366
|
+
now: () => NOW,
|
|
367
|
+
})
|
|
368
|
+
).resolves.toBe('ineligible');
|
|
369
|
+
|
|
370
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
371
|
+
expect(await readConsumedAt(receipt)).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('refuses to count when the committed policy has become ineligible (policy transition)', async () => {
|
|
375
|
+
const shareId = await seedShare();
|
|
376
|
+
const receipt = receiptFor();
|
|
377
|
+
|
|
378
|
+
await persistShareViewReceipt({
|
|
379
|
+
receipt,
|
|
380
|
+
namespace: NAMESPACE,
|
|
381
|
+
ownerProfileId: OWNER,
|
|
382
|
+
shareId,
|
|
383
|
+
contentVersion: 1,
|
|
384
|
+
shareVersion: 1,
|
|
385
|
+
objectRef: OBJECT_REF,
|
|
386
|
+
operationId: OPERATION_ID,
|
|
387
|
+
ttlSeconds: 600,
|
|
388
|
+
now: () => NOW,
|
|
389
|
+
eligibilitySource: testPolicySource,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await neogma.queryRunner.run(
|
|
393
|
+
'MATCH (s:ShareLink {id: $shareId}) SET s.minorPolicyViewCountingEnabled = false',
|
|
394
|
+
{ shareId }
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Eligibility source still permits; the tightened committed snapshot must veto.
|
|
398
|
+
await expect(
|
|
399
|
+
consumeShareViewReceipt({
|
|
400
|
+
receipt,
|
|
401
|
+
namespace: NAMESPACE,
|
|
402
|
+
eligibilitySource: testPolicySource,
|
|
403
|
+
now: () => NOW,
|
|
404
|
+
})
|
|
405
|
+
).resolves.toBe('ineligible');
|
|
406
|
+
|
|
407
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('does not count a receipt for a superseded or stopped or expired share', async () => {
|
|
411
|
+
const shareId = await seedShare();
|
|
412
|
+
const receipt = receiptFor();
|
|
413
|
+
await persistShareViewReceipt({
|
|
414
|
+
receipt,
|
|
415
|
+
namespace: NAMESPACE,
|
|
416
|
+
ownerProfileId: OWNER,
|
|
417
|
+
shareId,
|
|
418
|
+
contentVersion: 1,
|
|
419
|
+
shareVersion: 1,
|
|
420
|
+
objectRef: OBJECT_REF,
|
|
421
|
+
operationId: OPERATION_ID,
|
|
422
|
+
ttlSeconds: 600,
|
|
423
|
+
now: () => NOW,
|
|
424
|
+
eligibilitySource: testPolicySource,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Content replacement: version moves on, receipt stays bound to v1.
|
|
428
|
+
await neogma.queryRunner.run(
|
|
429
|
+
'MATCH (s:ShareLink {id: $shareId}) SET s.contentVersion = 2',
|
|
430
|
+
{ shareId }
|
|
431
|
+
);
|
|
432
|
+
await expect(
|
|
433
|
+
consumeShareViewReceipt({
|
|
434
|
+
receipt,
|
|
435
|
+
namespace: NAMESPACE,
|
|
436
|
+
eligibilitySource: testPolicySource,
|
|
437
|
+
now: () => NOW,
|
|
438
|
+
})
|
|
439
|
+
).resolves.toBe('version_mismatch');
|
|
440
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
441
|
+
|
|
442
|
+
// Revoke.
|
|
443
|
+
await neogma.queryRunner.run(
|
|
444
|
+
`MATCH (s:ShareLink {id: $shareId}) SET s.status = 'stopped', s.stoppedAt = $now`,
|
|
445
|
+
{ shareId, now: NOW.toISOString() }
|
|
446
|
+
);
|
|
447
|
+
await expect(
|
|
448
|
+
consumeShareViewReceipt({
|
|
449
|
+
receipt,
|
|
450
|
+
namespace: NAMESPACE,
|
|
451
|
+
eligibilitySource: testPolicySource,
|
|
452
|
+
now: () => NOW,
|
|
453
|
+
})
|
|
454
|
+
).resolves.toBe('not_active');
|
|
455
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('rejects an expired receipt via the clock sampled after locks', async () => {
|
|
459
|
+
const shareId = await seedShare();
|
|
460
|
+
const receipt = receiptFor();
|
|
461
|
+
await persistShareViewReceipt({
|
|
462
|
+
receipt,
|
|
463
|
+
namespace: NAMESPACE,
|
|
464
|
+
ownerProfileId: OWNER,
|
|
465
|
+
shareId,
|
|
466
|
+
contentVersion: 1,
|
|
467
|
+
shareVersion: 1,
|
|
468
|
+
objectRef: OBJECT_REF,
|
|
469
|
+
operationId: OPERATION_ID,
|
|
470
|
+
ttlSeconds: 600,
|
|
471
|
+
now: () => NOW,
|
|
472
|
+
eligibilitySource: testPolicySource,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const later = new Date(NOW.getTime() + 601_000);
|
|
476
|
+
await expect(
|
|
477
|
+
consumeShareViewReceipt({
|
|
478
|
+
receipt,
|
|
479
|
+
namespace: NAMESPACE,
|
|
480
|
+
eligibilitySource: testPolicySource,
|
|
481
|
+
now: () => later,
|
|
482
|
+
})
|
|
483
|
+
).resolves.toBe('expired');
|
|
484
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('samples time after a lock-blocked acknowledgement, not before', async () => {
|
|
488
|
+
const shareId = await seedShare();
|
|
489
|
+
const receipt = receiptFor();
|
|
490
|
+
// Receipt expires 700ms after NOW.
|
|
491
|
+
await persistShareViewReceipt({
|
|
492
|
+
receipt,
|
|
493
|
+
namespace: NAMESPACE,
|
|
494
|
+
ownerProfileId: OWNER,
|
|
495
|
+
shareId,
|
|
496
|
+
contentVersion: 1,
|
|
497
|
+
shareVersion: 1,
|
|
498
|
+
objectRef: OBJECT_REF,
|
|
499
|
+
operationId: OPERATION_ID,
|
|
500
|
+
ttlSeconds: 600,
|
|
501
|
+
now: () => NOW,
|
|
502
|
+
eligibilitySource: testPolicySource,
|
|
503
|
+
});
|
|
504
|
+
await neogma.queryRunner.run(
|
|
505
|
+
`MATCH (v:ShareViewReceipt) WHERE v.shareId = $shareId SET v.expiresAt = $expiresAt`,
|
|
506
|
+
{ shareId, expiresAt: new Date(NOW.getTime() + 700).toISOString() }
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// Hold the share lock for longer than the receipt TTL.
|
|
510
|
+
const driver = neogma.queryRunner.getDriver();
|
|
511
|
+
const blocker = driver.session();
|
|
512
|
+
const blockerTx = await blocker.beginTransaction();
|
|
513
|
+
await blockerTx.run(
|
|
514
|
+
'MATCH (s:ShareLink {id: $shareId}) SET s.lockTick = coalesce(s.lockTick,0) + 1',
|
|
515
|
+
{ shareId }
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const started = Date.now();
|
|
519
|
+
const pending = consumeShareViewReceipt({
|
|
520
|
+
receipt,
|
|
521
|
+
namespace: NAMESPACE,
|
|
522
|
+
eligibilitySource: testPolicySource,
|
|
523
|
+
// Real clock: must sample after the lock is acquired.
|
|
524
|
+
now: () => new Date(NOW.getTime() + (Date.now() - started)),
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await new Promise(resolve => setTimeout(resolve, 1_200));
|
|
528
|
+
await blockerTx.commit();
|
|
529
|
+
await blocker.close();
|
|
530
|
+
|
|
531
|
+
await expect(pending).resolves.toBe('expired');
|
|
532
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('prunes only expired/consumed receipts, never share state', async () => {
|
|
536
|
+
const shareId = await seedShare();
|
|
537
|
+
|
|
538
|
+
const expired = receiptFor();
|
|
539
|
+
const consumed = receiptFor();
|
|
540
|
+
const live = receiptFor();
|
|
541
|
+
|
|
542
|
+
await persistShareViewReceipt({
|
|
543
|
+
receipt: expired,
|
|
544
|
+
namespace: NAMESPACE,
|
|
545
|
+
ownerProfileId: OWNER,
|
|
546
|
+
shareId,
|
|
547
|
+
contentVersion: 1,
|
|
548
|
+
shareVersion: 1,
|
|
549
|
+
objectRef: OBJECT_REF,
|
|
550
|
+
operationId: OPERATION_ID,
|
|
551
|
+
ttlSeconds: 0,
|
|
552
|
+
now: () => NOW,
|
|
553
|
+
eligibilitySource: testPolicySource,
|
|
554
|
+
});
|
|
555
|
+
await persistShareViewReceipt({
|
|
556
|
+
receipt: consumed,
|
|
557
|
+
namespace: NAMESPACE,
|
|
558
|
+
ownerProfileId: OWNER,
|
|
559
|
+
shareId,
|
|
560
|
+
contentVersion: 1,
|
|
561
|
+
shareVersion: 1,
|
|
562
|
+
objectRef: OBJECT_REF,
|
|
563
|
+
operationId: OPERATION_ID,
|
|
564
|
+
ttlSeconds: 600,
|
|
565
|
+
now: () => NOW,
|
|
566
|
+
eligibilitySource: testPolicySource,
|
|
567
|
+
});
|
|
568
|
+
await persistShareViewReceipt({
|
|
569
|
+
receipt: live,
|
|
570
|
+
namespace: NAMESPACE,
|
|
571
|
+
ownerProfileId: OWNER,
|
|
572
|
+
shareId,
|
|
573
|
+
contentVersion: 1,
|
|
574
|
+
shareVersion: 1,
|
|
575
|
+
objectRef: OBJECT_REF,
|
|
576
|
+
operationId: OPERATION_ID,
|
|
577
|
+
ttlSeconds: 600,
|
|
578
|
+
now: () => NOW,
|
|
579
|
+
eligibilitySource: testPolicySource,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
await consumeShareViewReceipt({
|
|
583
|
+
receipt: consumed,
|
|
584
|
+
namespace: NAMESPACE,
|
|
585
|
+
eligibilitySource: testPolicySource,
|
|
586
|
+
now: () => NOW,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const deleted = await pruneShareViewReceipts({
|
|
590
|
+
namespace: NAMESPACE,
|
|
591
|
+
limit: 100,
|
|
592
|
+
now: NOW,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(deleted).toBe(2);
|
|
596
|
+
expect(await readShareViewReceiptOwner(live, NAMESPACE)).toBe(OWNER);
|
|
597
|
+
expect(await readShareViewReceiptOwner(expired, NAMESPACE)).toBeNull();
|
|
598
|
+
expect(await readShareViewReceiptOwner(consumed, NAMESPACE)).toBeNull();
|
|
599
|
+
|
|
600
|
+
// Share state is untouched by pruning.
|
|
601
|
+
const result = await neogma.queryRunner.run(
|
|
602
|
+
'MATCH (s:ShareLink {id: $shareId}) RETURN count(s) AS count',
|
|
603
|
+
{ shareId }
|
|
604
|
+
);
|
|
605
|
+
expect(Number(result.records[0]?.get('count') ?? 0)).toBe(1);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('never consumes a receipt minted in another namespace', async () => {
|
|
609
|
+
const shareId = await seedShare();
|
|
610
|
+
const receipt = receiptFor();
|
|
611
|
+
await persistShareViewReceipt({
|
|
612
|
+
receipt,
|
|
613
|
+
namespace: NAMESPACE,
|
|
614
|
+
ownerProfileId: OWNER,
|
|
615
|
+
shareId,
|
|
616
|
+
shareVersion: 1,
|
|
617
|
+
contentVersion: 1,
|
|
618
|
+
objectRef: OBJECT_REF,
|
|
619
|
+
operationId: OPERATION_ID,
|
|
620
|
+
ttlSeconds: 600,
|
|
621
|
+
now: () => NOW,
|
|
622
|
+
eligibilitySource: testPolicySource,
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
expect(await readShareViewReceiptOwner(receipt, 'other-ns')).toBeNull();
|
|
626
|
+
await expect(
|
|
627
|
+
consumeShareViewReceipt({
|
|
628
|
+
receipt,
|
|
629
|
+
namespace: 'other-ns',
|
|
630
|
+
eligibilitySource: testPolicySource,
|
|
631
|
+
now: () => NOW,
|
|
632
|
+
})
|
|
633
|
+
).resolves.toBe('binding_mismatch');
|
|
634
|
+
expect(await readShareViewReceiptOwner(receipt, NAMESPACE)).toBe(OWNER);
|
|
635
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('prunes only the requested namespace', async () => {
|
|
639
|
+
const shareId = await seedShare();
|
|
640
|
+
const otherShareId = await seedShare({ namespace: 'other-ns' });
|
|
641
|
+
await setTestPolicy(true, 'other-ns');
|
|
642
|
+
const mine = receiptFor();
|
|
643
|
+
const other = receiptFor();
|
|
644
|
+
await persistShareViewReceipt({
|
|
645
|
+
receipt: mine,
|
|
646
|
+
namespace: NAMESPACE,
|
|
647
|
+
ownerProfileId: OWNER,
|
|
648
|
+
shareId,
|
|
649
|
+
shareVersion: 1,
|
|
650
|
+
contentVersion: 1,
|
|
651
|
+
objectRef: OBJECT_REF,
|
|
652
|
+
operationId: OPERATION_ID,
|
|
653
|
+
ttlSeconds: 0,
|
|
654
|
+
now: () => NOW,
|
|
655
|
+
eligibilitySource: testPolicySource,
|
|
656
|
+
});
|
|
657
|
+
await persistShareViewReceipt({
|
|
658
|
+
receipt: other,
|
|
659
|
+
namespace: 'other-ns',
|
|
660
|
+
ownerProfileId: OWNER,
|
|
661
|
+
shareId: otherShareId,
|
|
662
|
+
shareVersion: 1,
|
|
663
|
+
contentVersion: 1,
|
|
664
|
+
objectRef: OBJECT_REF,
|
|
665
|
+
operationId: OPERATION_ID,
|
|
666
|
+
ttlSeconds: 0,
|
|
667
|
+
now: () => NOW,
|
|
668
|
+
eligibilitySource: testPolicySource,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
expect(await pruneShareViewReceipts({ namespace: NAMESPACE, now: NOW })).toBe(1);
|
|
672
|
+
expect(await readShareViewReceiptOwner(mine, NAMESPACE)).toBeNull();
|
|
673
|
+
expect(await readShareViewReceiptOwner(other, 'other-ns')).toBe(OWNER);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('never counts malformed durable receipt state', async () => {
|
|
677
|
+
const shareId = await seedShare();
|
|
678
|
+
const missingExpiry = receiptFor();
|
|
679
|
+
const missingIdentity = receiptFor();
|
|
680
|
+
const badConsumedAt = receiptFor();
|
|
681
|
+
const badReceiptExpiry = receiptFor();
|
|
682
|
+
const fractionalVersion = receiptFor();
|
|
683
|
+
const negativeVersion = receiptFor();
|
|
684
|
+
|
|
685
|
+
const receipts = [
|
|
686
|
+
missingExpiry,
|
|
687
|
+
missingIdentity,
|
|
688
|
+
badConsumedAt,
|
|
689
|
+
badReceiptExpiry,
|
|
690
|
+
fractionalVersion,
|
|
691
|
+
negativeVersion,
|
|
692
|
+
];
|
|
693
|
+
for (const receipt of receipts) {
|
|
694
|
+
const ok = await persistShareViewReceipt({
|
|
695
|
+
receipt,
|
|
696
|
+
namespace: NAMESPACE,
|
|
697
|
+
ownerProfileId: OWNER,
|
|
698
|
+
shareId,
|
|
699
|
+
shareVersion: 1,
|
|
700
|
+
contentVersion: 1,
|
|
701
|
+
objectRef: OBJECT_REF,
|
|
702
|
+
operationId: OPERATION_ID,
|
|
703
|
+
ttlSeconds: 600,
|
|
704
|
+
now: () => NOW,
|
|
705
|
+
eligibilitySource: testPolicySource,
|
|
706
|
+
});
|
|
707
|
+
expect(ok).toBe(true);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
await neogma.queryRunner.run(
|
|
711
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) SET v.expiresAt = null`,
|
|
712
|
+
{ hash: hashShareViewReceipt(missingExpiry) }
|
|
713
|
+
);
|
|
714
|
+
await neogma.queryRunner.run(
|
|
715
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) SET v.objectRef = null`,
|
|
716
|
+
{ hash: hashShareViewReceipt(missingIdentity) }
|
|
717
|
+
);
|
|
718
|
+
// A non-string consumedAt must not become "unconsumed".
|
|
719
|
+
await neogma.queryRunner.run(
|
|
720
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) SET v.consumedAt = 12345`,
|
|
721
|
+
{ hash: hashShareViewReceipt(badConsumedAt) }
|
|
722
|
+
);
|
|
723
|
+
// A garbage receipt expiry must fail closed, not become unlimited.
|
|
724
|
+
await neogma.queryRunner.run(
|
|
725
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) SET v.expiresAt = 'not-a-date'`,
|
|
726
|
+
{ hash: hashShareViewReceipt(badReceiptExpiry) }
|
|
727
|
+
);
|
|
728
|
+
// Negative/fractional versions are malformed, not coerced.
|
|
729
|
+
await neogma.queryRunner.run(
|
|
730
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) SET v.contentVersion = 1.5`,
|
|
731
|
+
{ hash: hashShareViewReceipt(fractionalVersion) }
|
|
732
|
+
);
|
|
733
|
+
await neogma.queryRunner.run(
|
|
734
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) SET v.shareVersion = -1`,
|
|
735
|
+
{ hash: hashShareViewReceipt(negativeVersion) }
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
const consume = (receipt: string) =>
|
|
739
|
+
consumeShareViewReceipt({
|
|
740
|
+
receipt,
|
|
741
|
+
namespace: NAMESPACE,
|
|
742
|
+
eligibilitySource: testPolicySource,
|
|
743
|
+
now: () => NOW,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
await expect(consume(missingExpiry)).resolves.toBe('expired');
|
|
747
|
+
await expect(consume(missingIdentity)).resolves.toBe('binding_mismatch');
|
|
748
|
+
await expect(consume(badConsumedAt)).resolves.toBe('not_found');
|
|
749
|
+
await expect(consume(badReceiptExpiry)).resolves.toBe('expired');
|
|
750
|
+
await expect(consume(fractionalVersion)).resolves.toBe('not_found');
|
|
751
|
+
await expect(consume(negativeVersion)).resolves.toBe('not_found');
|
|
752
|
+
|
|
753
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
754
|
+
for (const receipt of [missingExpiry, missingIdentity, badReceiptExpiry]) {
|
|
755
|
+
expect(await readConsumedAt(receipt)).toBeNull();
|
|
756
|
+
}
|
|
757
|
+
// The malformed marker is left untouched (never rewritten to a real
|
|
758
|
+
// consumption instant) and the counter never moved.
|
|
759
|
+
expect(await readConsumedAt(badConsumedAt)).not.toBeNull();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('treats a malformed share expiry as not_active, never unlimited', async () => {
|
|
763
|
+
const shareId = await seedShare();
|
|
764
|
+
const receipt = receiptFor();
|
|
765
|
+
await persistShareViewReceipt({
|
|
766
|
+
receipt,
|
|
767
|
+
namespace: NAMESPACE,
|
|
768
|
+
ownerProfileId: OWNER,
|
|
769
|
+
shareId,
|
|
770
|
+
shareVersion: 1,
|
|
771
|
+
contentVersion: 1,
|
|
772
|
+
objectRef: OBJECT_REF,
|
|
773
|
+
operationId: OPERATION_ID,
|
|
774
|
+
ttlSeconds: 600,
|
|
775
|
+
now: () => NOW,
|
|
776
|
+
eligibilitySource: testPolicySource,
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
await neogma.queryRunner.run(`MATCH (s:ShareLink {id: $shareId}) SET s.expiresAt = 12345`, {
|
|
780
|
+
shareId,
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
await expect(
|
|
784
|
+
consumeShareViewReceipt({
|
|
785
|
+
receipt,
|
|
786
|
+
namespace: NAMESPACE,
|
|
787
|
+
eligibilitySource: testPolicySource,
|
|
788
|
+
now: () => NOW,
|
|
789
|
+
})
|
|
790
|
+
).resolves.toBe('not_found');
|
|
791
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('re-reads the persisted policy under the share lock and honors a concurrent tightening', async () => {
|
|
795
|
+
const shareId = await seedShare();
|
|
796
|
+
const receipt = receiptFor();
|
|
797
|
+
await persistShareViewReceipt({
|
|
798
|
+
receipt,
|
|
799
|
+
namespace: NAMESPACE,
|
|
800
|
+
ownerProfileId: OWNER,
|
|
801
|
+
shareId,
|
|
802
|
+
shareVersion: 1,
|
|
803
|
+
contentVersion: 1,
|
|
804
|
+
objectRef: OBJECT_REF,
|
|
805
|
+
operationId: OPERATION_ID,
|
|
806
|
+
ttlSeconds: 600,
|
|
807
|
+
now: () => NOW,
|
|
808
|
+
eligibilitySource: testPolicySource,
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// Hold the share lock so the consume transaction blocks after receipt
|
|
812
|
+
// discovery but before it reads the persisted policy.
|
|
813
|
+
const driver = neogma.queryRunner.getDriver();
|
|
814
|
+
const blocker = driver.session();
|
|
815
|
+
const blockerTx = await blocker.beginTransaction();
|
|
816
|
+
await blockerTx.run(
|
|
817
|
+
'MATCH (s:ShareLink {id: $shareId}) SET s.lockTick = coalesce(s.lockTick,0) + 1',
|
|
818
|
+
{ shareId }
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
const pending = consumeShareViewReceipt({
|
|
822
|
+
receipt,
|
|
823
|
+
namespace: NAMESPACE,
|
|
824
|
+
eligibilitySource: testPolicySource,
|
|
825
|
+
now: () => NOW,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// Give the consume transaction time to block on the share lock, then
|
|
829
|
+
// tighten the SAME persisted policy node it will read under the lock.
|
|
830
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
831
|
+
await setTestPolicy(false);
|
|
832
|
+
await blockerTx.commit();
|
|
833
|
+
await blocker.close();
|
|
834
|
+
|
|
835
|
+
await expect(pending).resolves.toBe('ineligible');
|
|
836
|
+
expect((await readShare(shareId)).viewCount).toBe(0);
|
|
837
|
+
expect(await readConsumedAt(receipt)).toBeNull();
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('writes no receipt when the trusted eligibility source is absent', async () => {
|
|
841
|
+
const shareId = await seedShare();
|
|
842
|
+
const receipt = receiptFor();
|
|
843
|
+
|
|
844
|
+
await expect(
|
|
845
|
+
persistShareViewReceipt({
|
|
846
|
+
receipt,
|
|
847
|
+
namespace: NAMESPACE,
|
|
848
|
+
ownerProfileId: OWNER,
|
|
849
|
+
shareId,
|
|
850
|
+
shareVersion: 1,
|
|
851
|
+
contentVersion: 1,
|
|
852
|
+
objectRef: OBJECT_REF,
|
|
853
|
+
operationId: OPERATION_ID,
|
|
854
|
+
ttlSeconds: 600,
|
|
855
|
+
now: () => NOW,
|
|
856
|
+
})
|
|
857
|
+
).resolves.toBe(false);
|
|
858
|
+
|
|
859
|
+
expect(await readShareViewReceiptOwner(receipt, NAMESPACE)).toBeNull();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('writes no receipt when the source denies or when the current share is not active', async () => {
|
|
863
|
+
const shareId = await seedShare();
|
|
864
|
+
const denied = receiptFor();
|
|
865
|
+
|
|
866
|
+
await expect(
|
|
867
|
+
persistShareViewReceipt({
|
|
868
|
+
receipt: denied,
|
|
869
|
+
namespace: NAMESPACE,
|
|
870
|
+
ownerProfileId: OWNER,
|
|
871
|
+
shareId,
|
|
872
|
+
shareVersion: 1,
|
|
873
|
+
contentVersion: 1,
|
|
874
|
+
objectRef: OBJECT_REF,
|
|
875
|
+
operationId: OPERATION_ID,
|
|
876
|
+
ttlSeconds: 600,
|
|
877
|
+
now: () => NOW,
|
|
878
|
+
eligibilitySource: denyPolicySource,
|
|
879
|
+
})
|
|
880
|
+
).resolves.toBe(false);
|
|
881
|
+
expect(await readShareViewReceiptOwner(denied, NAMESPACE)).toBeNull();
|
|
882
|
+
|
|
883
|
+
// The pre-lock caller may believe the share is active, but the under-lock
|
|
884
|
+
// re-read observes the stop and refuses the write.
|
|
885
|
+
await neogma.queryRunner.run(
|
|
886
|
+
`MATCH (s:ShareLink {id: $shareId}) SET s.status = 'stopped', s.stoppedAt = $now`,
|
|
887
|
+
{ shareId, now: NOW.toISOString() }
|
|
888
|
+
);
|
|
889
|
+
const stopped = receiptFor();
|
|
890
|
+
await expect(
|
|
891
|
+
persistShareViewReceipt({
|
|
892
|
+
receipt: stopped,
|
|
893
|
+
namespace: NAMESPACE,
|
|
894
|
+
ownerProfileId: OWNER,
|
|
895
|
+
shareId,
|
|
896
|
+
shareVersion: 1,
|
|
897
|
+
contentVersion: 1,
|
|
898
|
+
objectRef: OBJECT_REF,
|
|
899
|
+
operationId: OPERATION_ID,
|
|
900
|
+
ttlSeconds: 600,
|
|
901
|
+
now: () => NOW,
|
|
902
|
+
eligibilitySource: testPolicySource,
|
|
903
|
+
})
|
|
904
|
+
).resolves.toBe(false);
|
|
905
|
+
expect(await readShareViewReceiptOwner(stopped, NAMESPACE)).toBeNull();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('rejects malformed persist bindings and never rebinds on a collision', async () => {
|
|
909
|
+
const shareId = await seedShare();
|
|
910
|
+
const receipt = receiptFor();
|
|
911
|
+
|
|
912
|
+
const base = {
|
|
913
|
+
receipt,
|
|
914
|
+
namespace: NAMESPACE,
|
|
915
|
+
ownerProfileId: OWNER,
|
|
916
|
+
shareId,
|
|
917
|
+
shareVersion: 1,
|
|
918
|
+
contentVersion: 1,
|
|
919
|
+
objectRef: OBJECT_REF,
|
|
920
|
+
operationId: OPERATION_ID,
|
|
921
|
+
ttlSeconds: 600,
|
|
922
|
+
now: () => NOW,
|
|
923
|
+
eligibilitySource: testPolicySource,
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
await expect(persistShareViewReceipt({ ...base, shareVersion: -1 })).resolves.toBe(false);
|
|
927
|
+
await expect(persistShareViewReceipt({ ...base, contentVersion: 1.5 })).resolves.toBe(
|
|
928
|
+
false
|
|
929
|
+
);
|
|
930
|
+
await expect(persistShareViewReceipt({ ...base, objectRef: '' })).resolves.toBe(false);
|
|
931
|
+
|
|
932
|
+
// First create succeeds; a second create on the same hash is a collision
|
|
933
|
+
// and must not rebind the existing node.
|
|
934
|
+
await expect(persistShareViewReceipt(base)).resolves.toBe(true);
|
|
935
|
+
await expect(persistShareViewReceipt(base)).resolves.toBe(false);
|
|
936
|
+
|
|
937
|
+
const result = await neogma.queryRunner.run(
|
|
938
|
+
`MATCH (v:ShareViewReceipt {receiptHash: $hash}) RETURN count(v) AS count`,
|
|
939
|
+
{ hash: hashShareViewReceipt(receipt) }
|
|
940
|
+
);
|
|
941
|
+
expect(Number(result.records[0]?.get('count') ?? 0)).toBe(1);
|
|
942
|
+
});
|
|
943
|
+
});
|