@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
package/src/routes/boosts.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertInboxRefreshEnabled,
|
|
3
|
+
inboxRefreshRequestDigest,
|
|
4
|
+
getInboxRefreshReplay,
|
|
5
|
+
getInboxRefreshReceipt,
|
|
6
|
+
resumeInboxRefreshDelivery,
|
|
7
|
+
} from '@helpers/inbox-refresh.helpers';
|
|
1
8
|
import { TRPCError } from '@trpc/server';
|
|
2
9
|
import { z } from 'zod';
|
|
3
10
|
import { v4 as uuid } from 'uuid';
|
|
@@ -27,11 +34,19 @@ import {
|
|
|
27
34
|
PaginatedBoostRecipientsWithChildrenValidator,
|
|
28
35
|
SkillQueryValidator,
|
|
29
36
|
SendBoostInputValidator,
|
|
37
|
+
SendBoostTemplateValidator,
|
|
30
38
|
SendBoostResponseValidator,
|
|
39
|
+
PrepareRefreshableSendInputValidator,
|
|
40
|
+
PrepareRefreshableSendResultValidator,
|
|
41
|
+
PrepareRefreshableSendResult,
|
|
31
42
|
AllocateCredentialStatusInputValidator,
|
|
32
43
|
AllocatedBitstringStatusListEntryValidator,
|
|
33
44
|
} from '@learncard/types';
|
|
34
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
isVC2Format,
|
|
47
|
+
injectManagedRefreshService,
|
|
48
|
+
getCredentialIssuerId,
|
|
49
|
+
} from '@learncard/helpers';
|
|
35
50
|
import {
|
|
36
51
|
renderBoostTemplate,
|
|
37
52
|
parseRenderedTemplate,
|
|
@@ -47,6 +62,7 @@ import { t, profileRoute } from '@routes';
|
|
|
47
62
|
|
|
48
63
|
import {
|
|
49
64
|
getBoostByUri,
|
|
65
|
+
getBoostById,
|
|
50
66
|
getBoostsForProfile,
|
|
51
67
|
countBoostsForProfile,
|
|
52
68
|
getBoostsByUri,
|
|
@@ -99,6 +115,7 @@ import {
|
|
|
99
115
|
prepareCredentialFromBoost,
|
|
100
116
|
appendTemplateEvidenceToCredential,
|
|
101
117
|
} from '@helpers/boost.helpers';
|
|
118
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
102
119
|
import {
|
|
103
120
|
BoostValidator,
|
|
104
121
|
BoostGenerateClaimLinkInput,
|
|
@@ -176,6 +193,28 @@ import {
|
|
|
176
193
|
import { updateDefaultPermissionsForBoost } from '@accesslayer/role/relationships/update';
|
|
177
194
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
178
195
|
import type { IssuedCredential } from '../types/credential';
|
|
196
|
+
import {
|
|
197
|
+
allocateCredentialRefresh,
|
|
198
|
+
extractManagedRefreshHandoff,
|
|
199
|
+
getBoundRefreshBoostId,
|
|
200
|
+
getCredentialRefreshServiceUrl,
|
|
201
|
+
peekCredentialRefreshInitialBinding,
|
|
202
|
+
sendRefreshableCredential,
|
|
203
|
+
} from '@helpers/credential-refresh.helpers';
|
|
204
|
+
import {
|
|
205
|
+
claimRefreshSendIntent,
|
|
206
|
+
computeRefreshSendRequestDigest,
|
|
207
|
+
getRefreshSendIntent,
|
|
208
|
+
markRefreshSendIntentDelivered,
|
|
209
|
+
reconcileBoundRefreshSendIntent,
|
|
210
|
+
recordRefreshSendIntent,
|
|
211
|
+
recordRefreshSendIntentPending,
|
|
212
|
+
type RefreshSendIntent,
|
|
213
|
+
} from '@helpers/refresh-send-intent.helpers';
|
|
214
|
+
import { getCredentialRefreshRuntimeEnvironment } from '@environment';
|
|
215
|
+
import { ensureCredentialRefreshConstraints } from '../models/credential-refresh-constraints';
|
|
216
|
+
import { userHasRequiredScopes } from '@helpers/auth-grant.helpers';
|
|
217
|
+
import { AUTH_GRANT_NO_ACCESS_SCOPE } from 'src/constants/auth-grant';
|
|
179
218
|
import { removeConnectionsForBoost } from '@helpers/connection.helpers';
|
|
180
219
|
import { issueToInbox } from '@helpers/inbox.helpers';
|
|
181
220
|
import { findInboxServiceEndpoint } from '@helpers/federation.helpers';
|
|
@@ -242,7 +281,336 @@ const resolveBoostCredentialInstance = async ({
|
|
|
242
281
|
return getCredentialInstanceForBoostAndProfile(boostId, recipientProfileId);
|
|
243
282
|
};
|
|
244
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Resolves the approved consent-flow contract terms for a unified send, and links a
|
|
286
|
+
* newly created boost to the contract. Shared by the normal and managed-refresh send
|
|
287
|
+
* paths so contract linkage semantics stay identical.
|
|
288
|
+
*/
|
|
289
|
+
const resolveContractForSend = async (params: {
|
|
290
|
+
profile: ProfileType;
|
|
291
|
+
targetProfile: ProfileType;
|
|
292
|
+
boost: BoostInstance;
|
|
293
|
+
boostCreated: boolean;
|
|
294
|
+
contractUri?: string;
|
|
295
|
+
}) => {
|
|
296
|
+
const { profile, targetProfile, boost, boostCreated, contractUri } = params;
|
|
297
|
+
|
|
298
|
+
let contractTerms = null as Awaited<ReturnType<typeof getContractTermsForProfile>> | null;
|
|
299
|
+
let contractDetails: Awaited<ReturnType<typeof getContractDetailsByUri>> | null = null;
|
|
300
|
+
|
|
301
|
+
if (contractUri) {
|
|
302
|
+
const decodedContractUri = decodeURIComponent(contractUri);
|
|
303
|
+
contractDetails = await traceDb('getContractDetailsByUri', () =>
|
|
304
|
+
getContractDetailsByUri(decodedContractUri)
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (!contractDetails) {
|
|
308
|
+
throw new TRPCError({
|
|
309
|
+
code: 'NOT_FOUND',
|
|
310
|
+
message: 'Could not find contract',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const terms = await traceDb('getContractTermsForProfile', () =>
|
|
315
|
+
getContractTermsForProfile(targetProfile, contractDetails!.contract)
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const writers = await traceDb('getWritersForContract', () =>
|
|
319
|
+
getWritersForContract(contractDetails!.contract)
|
|
320
|
+
);
|
|
321
|
+
const isWriter = writers.some(writer => writer.profileId === profile.profileId);
|
|
322
|
+
const isDenied = terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
|
|
323
|
+
const categoryAllowed = boost.category
|
|
324
|
+
? terms?.terms.write?.credentials?.categories?.[boost.category] === true
|
|
325
|
+
: true;
|
|
326
|
+
|
|
327
|
+
if (terms && isWriter && !isDenied && categoryAllowed) {
|
|
328
|
+
contractTerms = terms;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (boostCreated && contractDetails) {
|
|
333
|
+
await traceDb('setRelatedBoostForContract', () =>
|
|
334
|
+
setRelatedBoostForContract(contractDetails!.contract, boost)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return contractTerms;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
/** Validate managed send support and recipient before creating any send state. */
|
|
342
|
+
const validateRefreshSendRecipient = async ({
|
|
343
|
+
profile,
|
|
344
|
+
scope,
|
|
345
|
+
recipient,
|
|
346
|
+
domain,
|
|
347
|
+
}: {
|
|
348
|
+
profile: ProfileType;
|
|
349
|
+
scope?: string;
|
|
350
|
+
recipient: string;
|
|
351
|
+
domain: string;
|
|
352
|
+
}): Promise<ProfileType> => {
|
|
353
|
+
if (!getCredentialRefreshRuntimeEnvironment().CREDENTIAL_REFRESH_ENABLED) {
|
|
354
|
+
throw new TRPCError({
|
|
355
|
+
code: 'NOT_FOUND',
|
|
356
|
+
message: 'Credential refresh is not available',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await ensureCredentialRefreshConstraints();
|
|
361
|
+
|
|
362
|
+
// The route itself requires boosts:write; managed refresh additionally
|
|
363
|
+
// requires the same credentials:write scope as the dedicated
|
|
364
|
+
// /credential-refresh routes.
|
|
365
|
+
if (!userHasRequiredScopes(scope ?? AUTH_GRANT_NO_ACCESS_SCOPE, 'credentials:write')) {
|
|
366
|
+
throw new TRPCError({
|
|
367
|
+
code: 'UNAUTHORIZED',
|
|
368
|
+
message: 'This operation requires credentials:write scope',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (isInboxRecipient(recipient)) {
|
|
373
|
+
throw new TRPCError({
|
|
374
|
+
code: 'BAD_REQUEST',
|
|
375
|
+
message:
|
|
376
|
+
'Managed credential refresh requires a profile or DID recipient; email and phone recipients cannot request refresh.',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Recipients must resolve to local profiles before anything is
|
|
381
|
+
// created; remote or unresolvable DIDs are rejected here instead of
|
|
382
|
+
// entering the federation/inbox flows.
|
|
383
|
+
const refreshRecipientProfileId = await traceInternal('getProfileIdFromString:refresh', () =>
|
|
384
|
+
getProfileIdFromString(recipient, domain)
|
|
385
|
+
);
|
|
386
|
+
const refreshTargetProfile = refreshRecipientProfileId
|
|
387
|
+
? await traceDb('getProfileByProfileId:refresh', () =>
|
|
388
|
+
getProfileByProfileId(refreshRecipientProfileId)
|
|
389
|
+
)
|
|
390
|
+
: null;
|
|
391
|
+
|
|
392
|
+
if (!refreshTargetProfile) {
|
|
393
|
+
throw new TRPCError({
|
|
394
|
+
code: 'NOT_FOUND',
|
|
395
|
+
message:
|
|
396
|
+
'Managed credential refresh requires a recipient resolvable to a local profile. Remote or unresolvable DIDs are not supported.',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const refreshTarget = refreshTargetProfile;
|
|
401
|
+
|
|
402
|
+
if (
|
|
403
|
+
await traceDb('isRelationshipBlocked:refresh', () =>
|
|
404
|
+
isRelationshipBlocked(profile, refreshTarget)
|
|
405
|
+
)
|
|
406
|
+
) {
|
|
407
|
+
throw new TRPCError({
|
|
408
|
+
code: 'NOT_FOUND',
|
|
409
|
+
message: 'Profile not found. Are you sure this person exists?',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return refreshTargetProfile;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/** Preserve the inline send template's metadata, skills and claim permissions. */
|
|
417
|
+
const createInlineBoostForSend = async (
|
|
418
|
+
template: z.infer<typeof SendBoostTemplateValidator>,
|
|
419
|
+
profile: ProfileType,
|
|
420
|
+
domain: string
|
|
421
|
+
): Promise<BoostInstance> => {
|
|
422
|
+
const { credential, claimPermissions, skills, ...metadata } = template;
|
|
423
|
+
const boost = await traceDb('createBoost', () =>
|
|
424
|
+
createBoost(credential, profile, metadata, domain)
|
|
425
|
+
);
|
|
426
|
+
if (Array.isArray(skills) && skills.length > 0) {
|
|
427
|
+
await traceDb('addAlignedSkillsToBoost', () => addAlignedSkillsToBoost(boost, skills));
|
|
428
|
+
}
|
|
429
|
+
if (claimPermissions) {
|
|
430
|
+
await traceDb('addClaimPermissionsForBoost', () =>
|
|
431
|
+
addClaimPermissionsForBoost(boost, { ...EMPTY_PERMISSIONS, ...claimPermissions })
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return boost;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const managedRefreshServiceFor = (refreshId: string, domain: string) => ({
|
|
438
|
+
id: getCredentialRefreshServiceUrl(refreshId, domain),
|
|
439
|
+
type: 'LearnCardCredentialRefresh2026' as const,
|
|
440
|
+
authorization: { type: 'LearnCardDIDAuth' as const },
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The single server preparation step for a managed refresh send (SDK and signing
|
|
445
|
+
* authority paths): after recipient validation, create/reuse the boost and allocate
|
|
446
|
+
* the refresh. With an idempotencyKey, progress is recorded on a RefreshSendIntent so a
|
|
447
|
+
* retried call reuses the same boost/allocation, or returns the completed result.
|
|
448
|
+
*/
|
|
449
|
+
const prepareManagedRefreshSend = async (params: {
|
|
450
|
+
profile: ProfileType;
|
|
451
|
+
/** Returned by validateRefreshSendRecipient in this request, before any mutation. */
|
|
452
|
+
targetProfile: ProfileType;
|
|
453
|
+
domain: string;
|
|
454
|
+
templateUri?: string;
|
|
455
|
+
template?: z.infer<typeof SendBoostTemplateValidator>;
|
|
456
|
+
contractUri?: string;
|
|
457
|
+
credentialId?: string;
|
|
458
|
+
templateData?: Record<string, unknown>;
|
|
459
|
+
integrationId?: string;
|
|
460
|
+
idempotencyKey?: string;
|
|
461
|
+
}): Promise<PrepareRefreshableSendResult & { intent?: RefreshSendIntent }> => {
|
|
462
|
+
const { profile, targetProfile, domain, templateUri, template, contractUri } = params;
|
|
463
|
+
// Local recipients authenticate refresh requests as their network profile DID,
|
|
464
|
+
// including when the caller addressed them by their controller did:key.
|
|
465
|
+
const holderDid = getDidWeb(domain, targetProfile.profileId);
|
|
466
|
+
|
|
467
|
+
let intent: RefreshSendIntent | undefined;
|
|
468
|
+
|
|
469
|
+
if (params.idempotencyKey) {
|
|
470
|
+
const claim = await claimRefreshSendIntent({
|
|
471
|
+
issuerProfileId: profile.profileId,
|
|
472
|
+
idempotencyKey: params.idempotencyKey,
|
|
473
|
+
requestDigest: computeRefreshSendRequestDigest({
|
|
474
|
+
recipientProfileId: targetProfile.profileId,
|
|
475
|
+
holderDid,
|
|
476
|
+
templateUri,
|
|
477
|
+
template,
|
|
478
|
+
contractUri,
|
|
479
|
+
templateData: params.templateData,
|
|
480
|
+
integrationId: params.integrationId,
|
|
481
|
+
credentialId: params.credentialId,
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
intent = claim.intent;
|
|
486
|
+
|
|
487
|
+
if (claim.kind !== 'owned') {
|
|
488
|
+
const prepared = {
|
|
489
|
+
boostUri: intent.boostUri!,
|
|
490
|
+
credentialId: intent.credentialId!,
|
|
491
|
+
refreshId: intent.refreshId!,
|
|
492
|
+
refreshService: managedRefreshServiceFor(intent.refreshId!, domain),
|
|
493
|
+
holderDid: intent.holderDid!,
|
|
494
|
+
intent,
|
|
495
|
+
};
|
|
496
|
+
const completed =
|
|
497
|
+
claim.kind === 'delivered'
|
|
498
|
+
? intent.result
|
|
499
|
+
: await reconcileBoundRefreshSendIntent(intent, domain);
|
|
500
|
+
|
|
501
|
+
return completed ? { ...prepared, completed } : prepared;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (template?.status === 'DRAFT') {
|
|
506
|
+
throw new TRPCError({
|
|
507
|
+
code: 'FORBIDDEN',
|
|
508
|
+
message: 'Draft Boosts can not be sent. Only Published Boosts can be sent.',
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
let boostUri = intent?.boostUri ?? templateUri;
|
|
513
|
+
let boost: BoostInstance | null;
|
|
514
|
+
|
|
515
|
+
if (boostUri) {
|
|
516
|
+
boost = await traceDb('getBoostByUri:prepareRefresh', () => getBoostByUri(boostUri!));
|
|
517
|
+
|
|
518
|
+
if (!boost) throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find boost' });
|
|
519
|
+
} else {
|
|
520
|
+
boost = await createInlineBoostForSend(template!, profile, domain);
|
|
521
|
+
boostUri = getBoostUri(boost.id, domain);
|
|
522
|
+
|
|
523
|
+
if (intent) intent = await recordRefreshSendIntent(intent, { boostUri });
|
|
524
|
+
|
|
525
|
+
await resolveContractForSend({
|
|
526
|
+
profile,
|
|
527
|
+
targetProfile,
|
|
528
|
+
boost,
|
|
529
|
+
boostCreated: true,
|
|
530
|
+
contractUri,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (
|
|
535
|
+
!(await traceDb('canProfileIssueBoost:prepareRefresh', () =>
|
|
536
|
+
canProfileIssueBoost(profile, boost!)
|
|
537
|
+
))
|
|
538
|
+
) {
|
|
539
|
+
throw new TRPCError({
|
|
540
|
+
code: 'UNAUTHORIZED',
|
|
541
|
+
message: 'Profile does not have permissions to issue boost',
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (isDraftBoost(boost)) {
|
|
546
|
+
throw new TRPCError({
|
|
547
|
+
code: 'FORBIDDEN',
|
|
548
|
+
message: 'Draft Boosts can not be sent. Only Published Boosts can be sent.',
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const credentialId = intent?.credentialId ?? params.credentialId ?? `urn:uuid:${uuid()}`;
|
|
553
|
+
let refreshId = intent?.refreshId;
|
|
554
|
+
|
|
555
|
+
if (!refreshId) {
|
|
556
|
+
const allocation = await traceInternal('allocateCredentialRefresh:prepare', () =>
|
|
557
|
+
allocateCredentialRefresh({
|
|
558
|
+
issuerProfile: profile,
|
|
559
|
+
holderProfile: targetProfile,
|
|
560
|
+
holderDid,
|
|
561
|
+
credentialId,
|
|
562
|
+
domain,
|
|
563
|
+
})
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
refreshId = allocation.refreshId;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (intent) {
|
|
570
|
+
intent = await recordRefreshSendIntent(intent, {
|
|
571
|
+
boostUri,
|
|
572
|
+
credentialId,
|
|
573
|
+
refreshId,
|
|
574
|
+
holderDid,
|
|
575
|
+
state: 'prepared',
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
boostUri,
|
|
581
|
+
credentialId,
|
|
582
|
+
refreshId,
|
|
583
|
+
refreshService: managedRefreshServiceFor(refreshId, domain),
|
|
584
|
+
holderDid,
|
|
585
|
+
...(intent ? { intent } : {}),
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
|
|
245
589
|
export const boostsRouter = t.router({
|
|
590
|
+
// SDK preparation only: guards, boost anchor and refresh allocation. Never signs or
|
|
591
|
+
// delivers. With an idempotencyKey a retried call reuses the same boost/allocation,
|
|
592
|
+
// or returns `completed` when that key already delivered.
|
|
593
|
+
prepareRefreshableSend: profileRoute
|
|
594
|
+
.meta({ requiredScope: 'boosts:write' })
|
|
595
|
+
.input(PrepareRefreshableSendInputValidator)
|
|
596
|
+
.output(PrepareRefreshableSendResultValidator)
|
|
597
|
+
.mutation(async ({ ctx, input }) => {
|
|
598
|
+
const targetProfile = await validateRefreshSendRecipient({
|
|
599
|
+
profile: ctx.user.profile,
|
|
600
|
+
scope: ctx.user.scope,
|
|
601
|
+
recipient: input.recipient,
|
|
602
|
+
domain: ctx.domain,
|
|
603
|
+
});
|
|
604
|
+
const { intent: _intent, ...prepared } = await prepareManagedRefreshSend({
|
|
605
|
+
...input,
|
|
606
|
+
profile: ctx.user.profile,
|
|
607
|
+
targetProfile,
|
|
608
|
+
domain: ctx.domain,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return prepared;
|
|
612
|
+
}),
|
|
613
|
+
|
|
246
614
|
getBoostAlignments: profileRoute
|
|
247
615
|
.meta({
|
|
248
616
|
openapi: {
|
|
@@ -692,6 +1060,7 @@ export const boostsRouter = t.router({
|
|
|
692
1060
|
// Log credential activity FIRST to get activityId for chaining
|
|
693
1061
|
const activityId = await logCredentialSent({
|
|
694
1062
|
actorProfileId: profile.profileId,
|
|
1063
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
695
1064
|
recipientType: 'profile',
|
|
696
1065
|
recipientIdentifier: targetProfile.profileId,
|
|
697
1066
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -736,6 +1105,147 @@ export const boostsRouter = t.router({
|
|
|
736
1105
|
const { contractUri } = input;
|
|
737
1106
|
const { domain } = ctx;
|
|
738
1107
|
|
|
1108
|
+
// LC-2198: managed refresh requests are validated in full BEFORE any
|
|
1109
|
+
// mutation — an unsupported refresh send must not create a boost,
|
|
1110
|
+
// allocation, inbox entry, activity, or delivery.
|
|
1111
|
+
const inboxRefreshRequested =
|
|
1112
|
+
input.refresh === true && !!isInboxRecipient(input.recipient);
|
|
1113
|
+
const refreshRequested = input.refresh === true && !inboxRefreshRequested;
|
|
1114
|
+
let inboxIntent: RefreshSendIntent | undefined;
|
|
1115
|
+
const inboxDigest = inboxRefreshRequested
|
|
1116
|
+
? inboxRefreshRequestDigest(input)
|
|
1117
|
+
: undefined;
|
|
1118
|
+
const inboxKey = input.idempotencyKey
|
|
1119
|
+
? `send:${input.idempotencyKey}`
|
|
1120
|
+
: undefined;
|
|
1121
|
+
if (inboxRefreshRequested) {
|
|
1122
|
+
await assertInboxRefreshEnabled(ctx.user.scope);
|
|
1123
|
+
if (input.signedCredential || input.contractUri)
|
|
1124
|
+
throw new TRPCError({
|
|
1125
|
+
code: 'BAD_REQUEST',
|
|
1126
|
+
message:
|
|
1127
|
+
'Inbox refresh requires unsigned content and does not support a consent contract before holder binding.',
|
|
1128
|
+
});
|
|
1129
|
+
if (input.idempotencyKey) {
|
|
1130
|
+
const claim = await claimRefreshSendIntent({
|
|
1131
|
+
issuerProfileId: profile.profileId,
|
|
1132
|
+
idempotencyKey: input.idempotencyKey,
|
|
1133
|
+
requestDigest: inboxDigest!,
|
|
1134
|
+
});
|
|
1135
|
+
if (claim.kind === 'delivered' && claim.intent.result)
|
|
1136
|
+
return claim.intent.result;
|
|
1137
|
+
inboxIntent = claim.intent;
|
|
1138
|
+
const replay = await getInboxRefreshReplay(
|
|
1139
|
+
profile.profileId,
|
|
1140
|
+
inboxKey,
|
|
1141
|
+
inboxDigest!
|
|
1142
|
+
);
|
|
1143
|
+
if (
|
|
1144
|
+
replay &&
|
|
1145
|
+
(replay.claimUrl || replay.inbox.currentStatus !== 'PENDING')
|
|
1146
|
+
) {
|
|
1147
|
+
await resumeInboxRefreshDelivery(replay.inbox.refreshId!, domain);
|
|
1148
|
+
const result = {
|
|
1149
|
+
type: 'boost' as const,
|
|
1150
|
+
uri: inboxIntent.boostUri!,
|
|
1151
|
+
credentialUri: '',
|
|
1152
|
+
activityId: replay.inbox.activityId ?? '',
|
|
1153
|
+
inbox: {
|
|
1154
|
+
issuanceId: replay.inbox.id,
|
|
1155
|
+
status: replay.inbox.currentStatus,
|
|
1156
|
+
claimUrl: replay.claimUrl,
|
|
1157
|
+
guardianStatus: replay.inbox.guardianStatus,
|
|
1158
|
+
refresh: await getInboxRefreshReceipt(
|
|
1159
|
+
replay.inbox.refreshId!,
|
|
1160
|
+
domain
|
|
1161
|
+
),
|
|
1162
|
+
},
|
|
1163
|
+
};
|
|
1164
|
+
await markRefreshSendIntentDelivered(inboxIntent, result);
|
|
1165
|
+
return result;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
let refreshTargetProfile: ProfileType | null = null;
|
|
1170
|
+
|
|
1171
|
+
if (input.idempotencyKey && !input.refresh) {
|
|
1172
|
+
throw new TRPCError({
|
|
1173
|
+
code: 'BAD_REQUEST',
|
|
1174
|
+
message: 'idempotencyKey is only supported with refresh: true.',
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (refreshRequested) {
|
|
1179
|
+
refreshTargetProfile = await validateRefreshSendRecipient({
|
|
1180
|
+
profile,
|
|
1181
|
+
scope: ctx.user.scope,
|
|
1182
|
+
recipient: input.recipient,
|
|
1183
|
+
domain,
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
if (input.signedCredential) {
|
|
1187
|
+
// Reject signed credentials without a local managed allocation
|
|
1188
|
+
// before any mutation (e.g. boost auto-creation). No unsigned
|
|
1189
|
+
// fallback and no second allocation (plan decision 6).
|
|
1190
|
+
const handoff = extractManagedRefreshHandoff(
|
|
1191
|
+
input.signedCredential,
|
|
1192
|
+
domain
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
if (!handoff) {
|
|
1196
|
+
throw new TRPCError({
|
|
1197
|
+
code: 'BAD_REQUEST',
|
|
1198
|
+
message:
|
|
1199
|
+
'A signed credential sent with refresh must already contain its allocated managed refresh service.',
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (input.idempotencyKey) {
|
|
1204
|
+
const intent = await getRefreshSendIntent(
|
|
1205
|
+
profile.profileId,
|
|
1206
|
+
input.idempotencyKey
|
|
1207
|
+
);
|
|
1208
|
+
if (!intent) {
|
|
1209
|
+
throw new TRPCError({
|
|
1210
|
+
code: 'BAD_REQUEST',
|
|
1211
|
+
message:
|
|
1212
|
+
'idempotencyKey with a pre-signed credential requires a prior boost.prepareRefreshableSend call (tRPC only). For a directly signed REST send, omit idempotencyKey and retry the same signed credential and templateUri.',
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (
|
|
1216
|
+
intent.refreshId !== handoff.refreshId ||
|
|
1217
|
+
(input.templateUri && intent.boostUri !== input.templateUri)
|
|
1218
|
+
) {
|
|
1219
|
+
throw new TRPCError({
|
|
1220
|
+
code: 'CONFLICT',
|
|
1221
|
+
message:
|
|
1222
|
+
'This idempotencyKey does not match the prepared refresh send for this credential.',
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Keyed managed send without a pre-signed credential: preparation (boost +
|
|
1230
|
+
// allocation) goes through the idempotency intent instead of the generic
|
|
1231
|
+
// boost creation below.
|
|
1232
|
+
const keyedPrepared =
|
|
1233
|
+
refreshRequested && input.idempotencyKey && !input.signedCredential
|
|
1234
|
+
? await prepareManagedRefreshSend({
|
|
1235
|
+
profile,
|
|
1236
|
+
targetProfile: refreshTargetProfile!,
|
|
1237
|
+
domain,
|
|
1238
|
+
templateUri: input.templateUri,
|
|
1239
|
+
template: input.templateUri ? undefined : input.template,
|
|
1240
|
+
contractUri,
|
|
1241
|
+
templateData: input.templateData,
|
|
1242
|
+
integrationId: input.integrationId,
|
|
1243
|
+
idempotencyKey: input.idempotencyKey,
|
|
1244
|
+
})
|
|
1245
|
+
: undefined;
|
|
1246
|
+
|
|
1247
|
+
if (keyedPrepared?.completed) return keyedPrepared.completed;
|
|
1248
|
+
|
|
739
1249
|
// Check if recipient is email/phone (routes to Universal Inbox)
|
|
740
1250
|
const inboxRecipient = isInboxRecipient(input.recipient);
|
|
741
1251
|
|
|
@@ -744,7 +1254,22 @@ export const boostsRouter = t.router({
|
|
|
744
1254
|
let boostUri = '';
|
|
745
1255
|
let boostCreated = false;
|
|
746
1256
|
|
|
747
|
-
if (
|
|
1257
|
+
if (inboxIntent?.boostUri) {
|
|
1258
|
+
boost = await getBoostByUri(inboxIntent.boostUri);
|
|
1259
|
+
boostUri = inboxIntent.boostUri;
|
|
1260
|
+
} else if (keyedPrepared) {
|
|
1261
|
+
const resolved = await traceDb('getBoostByUri', () =>
|
|
1262
|
+
getBoostByUri(keyedPrepared.boostUri)
|
|
1263
|
+
);
|
|
1264
|
+
if (!resolved) {
|
|
1265
|
+
throw new TRPCError({
|
|
1266
|
+
code: 'NOT_FOUND',
|
|
1267
|
+
message: 'Could not find boost',
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
boost = resolved;
|
|
1271
|
+
boostUri = keyedPrepared.boostUri;
|
|
1272
|
+
} else if (input.templateUri) {
|
|
748
1273
|
const resolved = await traceDb('getBoostByUri', () =>
|
|
749
1274
|
getBoostByUri(input.templateUri!)
|
|
750
1275
|
);
|
|
@@ -757,47 +1282,77 @@ export const boostsRouter = t.router({
|
|
|
757
1282
|
boost = resolved;
|
|
758
1283
|
boostUri = input.templateUri;
|
|
759
1284
|
} else if (input.template) {
|
|
760
|
-
|
|
761
|
-
input.template;
|
|
762
|
-
|
|
763
|
-
boost = await traceDb('createBoost', () =>
|
|
764
|
-
createBoost(credential, profile, metadata, domain)
|
|
765
|
-
);
|
|
766
|
-
|
|
767
|
-
if (Array.isArray(skills) && skills.length > 0) {
|
|
768
|
-
await traceDb('addAlignedSkillsToBoost', () =>
|
|
769
|
-
addAlignedSkillsToBoost(boost!, skills)
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (claimPermissions) {
|
|
774
|
-
await traceDb('addClaimPermissionsForBoost', () =>
|
|
775
|
-
addClaimPermissionsForBoost(boost!, {
|
|
776
|
-
...EMPTY_PERMISSIONS,
|
|
777
|
-
...claimPermissions,
|
|
778
|
-
})
|
|
779
|
-
);
|
|
780
|
-
}
|
|
1285
|
+
boost = await createInlineBoostForSend(input.template, profile, domain);
|
|
781
1286
|
|
|
782
1287
|
boostUri = getBoostUri(boost.id, domain);
|
|
783
1288
|
boostCreated = true;
|
|
784
1289
|
} else if (input.signedCredential) {
|
|
785
|
-
//
|
|
786
|
-
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
)
|
|
797
|
-
|
|
1290
|
+
// A replayed managed handoff reuses the boost its refresh is already
|
|
1291
|
+
// bound to (the pre-mutation guard validated the handoff above).
|
|
1292
|
+
const boundBoostId = refreshRequested
|
|
1293
|
+
? await traceDb('getBoundRefreshBoostId', () =>
|
|
1294
|
+
getBoundRefreshBoostId(
|
|
1295
|
+
extractManagedRefreshHandoff(input.signedCredential!, domain)!
|
|
1296
|
+
.refreshId
|
|
1297
|
+
)
|
|
1298
|
+
)
|
|
1299
|
+
: undefined;
|
|
1300
|
+
const boundBoost = boundBoostId
|
|
1301
|
+
? await traceDb('getBoostById:boundRefresh', () =>
|
|
1302
|
+
getBoostById(boundBoostId)
|
|
1303
|
+
)
|
|
1304
|
+
: null;
|
|
1305
|
+
// A keyed handoff whose preparation already created/resolved a
|
|
1306
|
+
// boost (but never delivered) reuses that prepared boost.
|
|
1307
|
+
const preparedBoostUri =
|
|
1308
|
+
!boundBoost && refreshRequested && input.idempotencyKey
|
|
1309
|
+
? (
|
|
1310
|
+
await traceDb('getRefreshSendIntent:signedCredential', () =>
|
|
1311
|
+
getRefreshSendIntent(
|
|
1312
|
+
profile.profileId,
|
|
1313
|
+
input.idempotencyKey!
|
|
1314
|
+
)
|
|
1315
|
+
)
|
|
1316
|
+
)?.boostUri
|
|
1317
|
+
: undefined;
|
|
1318
|
+
const preparedBoost = preparedBoostUri
|
|
1319
|
+
? await traceDb('getBoostByUri:preparedRefresh', () =>
|
|
1320
|
+
getBoostByUri(preparedBoostUri)
|
|
1321
|
+
)
|
|
1322
|
+
: null;
|
|
1323
|
+
|
|
1324
|
+
if (boundBoost) {
|
|
1325
|
+
boost = boundBoost;
|
|
1326
|
+
boostUri = getBoostUri(boundBoost.id, domain);
|
|
1327
|
+
} else if (preparedBoost && preparedBoostUri) {
|
|
1328
|
+
boost = preparedBoost;
|
|
1329
|
+
boostUri = preparedBoostUri;
|
|
1330
|
+
} else {
|
|
1331
|
+
// Auto-create boost from the signed credential. A managed refresh
|
|
1332
|
+
// service belongs to one holder's credential, never to a reusable
|
|
1333
|
+
// template, so it is not stored on the boost.
|
|
1334
|
+
const { refreshService: _refreshService, ...templateCredential } =
|
|
1335
|
+
input.signedCredential as Record<string, unknown>;
|
|
1336
|
+
const name =
|
|
1337
|
+
typeof templateCredential.name === 'string'
|
|
1338
|
+
? templateCredential.name
|
|
1339
|
+
: undefined;
|
|
1340
|
+
|
|
1341
|
+
boost = await traceDb('createBoost:fromSignedCredential', () =>
|
|
1342
|
+
createBoost(
|
|
1343
|
+
(refreshRequested
|
|
1344
|
+
? templateCredential
|
|
1345
|
+
: input.signedCredential!) as typeof input.signedCredential &
|
|
1346
|
+
object,
|
|
1347
|
+
profile,
|
|
1348
|
+
{ ...(name ? { name } : {}) },
|
|
1349
|
+
domain
|
|
1350
|
+
)
|
|
1351
|
+
);
|
|
798
1352
|
|
|
799
|
-
|
|
800
|
-
|
|
1353
|
+
boostUri = getBoostUri(boost.id, domain);
|
|
1354
|
+
boostCreated = true;
|
|
1355
|
+
}
|
|
801
1356
|
}
|
|
802
1357
|
|
|
803
1358
|
if (!boost) {
|
|
@@ -827,6 +1382,298 @@ export const boostsRouter = t.router({
|
|
|
827
1382
|
});
|
|
828
1383
|
}
|
|
829
1384
|
|
|
1385
|
+
if (inboxIntent?.state === 'preparing')
|
|
1386
|
+
inboxIntent = await recordRefreshSendIntent(inboxIntent, {
|
|
1387
|
+
boostUri,
|
|
1388
|
+
state: 'prepared',
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
// LC-2198: managed refresh delivery. The recipient was fully validated
|
|
1392
|
+
// in the pre-mutation guard above (feature, scope, supported recipient,
|
|
1393
|
+
// blocklist); the boost passed the same issue-permission and draft
|
|
1394
|
+
// checks as a normal send.
|
|
1395
|
+
if (refreshRequested) {
|
|
1396
|
+
return trace('route', 'sendRefreshable', async () => {
|
|
1397
|
+
const targetProfile = refreshTargetProfile!;
|
|
1398
|
+
let intent: RefreshSendIntent | undefined = keyedPrepared?.intent;
|
|
1399
|
+
|
|
1400
|
+
const contractTerms = await resolveContractForSend({
|
|
1401
|
+
profile,
|
|
1402
|
+
targetProfile,
|
|
1403
|
+
boost: boost!,
|
|
1404
|
+
boostCreated,
|
|
1405
|
+
contractUri,
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// Matches the unified send's self-send notification behavior.
|
|
1409
|
+
const skipNotification = profile.profileId === targetProfile.profileId;
|
|
1410
|
+
|
|
1411
|
+
let signedVc: VC;
|
|
1412
|
+
let refreshId: string;
|
|
1413
|
+
|
|
1414
|
+
if (input.signedCredential) {
|
|
1415
|
+
// Preallocated handoff (plan decision 6): the caller already
|
|
1416
|
+
// allocated and embedded the managed service before signing.
|
|
1417
|
+
// Ownership, holder, ID, boost anchor, proof, and descriptor
|
|
1418
|
+
// are enforced by the managed send helper.
|
|
1419
|
+
const handoff = extractManagedRefreshHandoff(
|
|
1420
|
+
input.signedCredential,
|
|
1421
|
+
domain
|
|
1422
|
+
)!;
|
|
1423
|
+
signedVc = input.signedCredential;
|
|
1424
|
+
refreshId = handoff.refreshId;
|
|
1425
|
+
|
|
1426
|
+
if (input.idempotencyKey) {
|
|
1427
|
+
const existing = await getRefreshSendIntent(
|
|
1428
|
+
profile.profileId,
|
|
1429
|
+
input.idempotencyKey
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
if (
|
|
1433
|
+
!existing ||
|
|
1434
|
+
existing.refreshId !== refreshId ||
|
|
1435
|
+
existing.boostUri !== boostUri
|
|
1436
|
+
) {
|
|
1437
|
+
throw new TRPCError({
|
|
1438
|
+
code: 'CONFLICT',
|
|
1439
|
+
message:
|
|
1440
|
+
'This idempotencyKey does not match the prepared refresh send for this credential.',
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
if (existing.state === 'delivered' && existing.result) {
|
|
1445
|
+
return existing.result;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
intent = existing;
|
|
1449
|
+
const reconciled = await reconcileBoundRefreshSendIntent(
|
|
1450
|
+
existing,
|
|
1451
|
+
domain
|
|
1452
|
+
);
|
|
1453
|
+
if (reconciled) return reconciled;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Resume detection: reuse the original delivery activity
|
|
1457
|
+
// instead of duplicating issuance or activity when the exact
|
|
1458
|
+
// same credential is redelivered (e.g. a network retry after
|
|
1459
|
+
// binding already succeeded).
|
|
1460
|
+
const binding = await traceInternal(
|
|
1461
|
+
'peekCredentialRefreshInitialBinding',
|
|
1462
|
+
() =>
|
|
1463
|
+
peekCredentialRefreshInitialBinding({
|
|
1464
|
+
refreshId,
|
|
1465
|
+
credential: signedVc,
|
|
1466
|
+
boostId: boost!.id,
|
|
1467
|
+
})
|
|
1468
|
+
);
|
|
1469
|
+
|
|
1470
|
+
if (binding.bound) {
|
|
1471
|
+
const { uri, receipt } = await sendRefreshableCredential({
|
|
1472
|
+
issuerProfile: profile,
|
|
1473
|
+
refreshId,
|
|
1474
|
+
credential: signedVc,
|
|
1475
|
+
boostUri,
|
|
1476
|
+
skipNotification,
|
|
1477
|
+
domain,
|
|
1478
|
+
activityId: binding.activityId,
|
|
1479
|
+
integrationId: input.integrationId,
|
|
1480
|
+
contractTerms: contractTerms ?? undefined,
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
const response = {
|
|
1484
|
+
type: 'boost' as const,
|
|
1485
|
+
credentialUri: uri,
|
|
1486
|
+
uri: boostUri,
|
|
1487
|
+
activityId: binding.activityId ?? '',
|
|
1488
|
+
refresh: receipt,
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
if (intent)
|
|
1492
|
+
await markRefreshSendIntentDelivered(intent, response);
|
|
1493
|
+
|
|
1494
|
+
return response;
|
|
1495
|
+
}
|
|
1496
|
+
} else {
|
|
1497
|
+
if (keyedPrepared && intent) {
|
|
1498
|
+
const reconciled = await reconcileBoundRefreshSendIntent(
|
|
1499
|
+
intent,
|
|
1500
|
+
domain
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
if (reconciled) return reconciled;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const signingAuthority = await traceDb(
|
|
1507
|
+
'getPrimarySigningAuthorityForUser:refresh',
|
|
1508
|
+
() => getPrimarySigningAuthorityForUser(profile)
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
if (!signingAuthority) {
|
|
1512
|
+
throw new TRPCError({
|
|
1513
|
+
code: 'PRECONDITION_FAILED',
|
|
1514
|
+
message:
|
|
1515
|
+
'You must register a signing authority before using send without a pre-signed credential. Please register one via registerSigningAuthority or sign the credential client-side.',
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
let unsignedVc: UnsignedVC;
|
|
1520
|
+
|
|
1521
|
+
try {
|
|
1522
|
+
unsignedVc = await traceInternal(
|
|
1523
|
+
'prepareCredentialFromBoost:refresh',
|
|
1524
|
+
() =>
|
|
1525
|
+
prepareCredentialFromBoost(boost!, boostUri, domain, {
|
|
1526
|
+
templateData: input.templateData as Record<
|
|
1527
|
+
string,
|
|
1528
|
+
unknown
|
|
1529
|
+
>,
|
|
1530
|
+
issuerDid: signingAuthority.relationship.did,
|
|
1531
|
+
recipientDid:
|
|
1532
|
+
keyedPrepared?.holderDid ??
|
|
1533
|
+
getDidWeb(domain, targetProfile.profileId),
|
|
1534
|
+
recipientName: targetProfile.displayName,
|
|
1535
|
+
})
|
|
1536
|
+
);
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
console.error('Failed to prepare boost credential', e);
|
|
1539
|
+
throw new TRPCError({
|
|
1540
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
1541
|
+
message: 'Failed to prepare boost credential',
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// A stable credential ID must exist before allocation: the
|
|
1546
|
+
// refresh aggregate is permanently bound to it.
|
|
1547
|
+
unsignedVc.id =
|
|
1548
|
+
keyedPrepared?.credentialId ??
|
|
1549
|
+
unsignedVc.id ??
|
|
1550
|
+
`urn:uuid:${uuid()}`;
|
|
1551
|
+
|
|
1552
|
+
// A keyed send already allocated its refresh during preparation;
|
|
1553
|
+
// otherwise allocate once, then inject the managed service +
|
|
1554
|
+
// inline JSON-LD context so both become part of the signed payload.
|
|
1555
|
+
const allocation = keyedPrepared
|
|
1556
|
+
? {
|
|
1557
|
+
refreshId: keyedPrepared.refreshId,
|
|
1558
|
+
refreshService: keyedPrepared.refreshService,
|
|
1559
|
+
}
|
|
1560
|
+
: await traceInternal('allocateCredentialRefresh:send', () =>
|
|
1561
|
+
allocateCredentialRefresh({
|
|
1562
|
+
issuerProfile: profile,
|
|
1563
|
+
holderProfile: targetProfile,
|
|
1564
|
+
holderDid: getDidWeb(domain, targetProfile.profileId),
|
|
1565
|
+
credentialId: unsignedVc.id!,
|
|
1566
|
+
domain,
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
|
|
1570
|
+
refreshId = allocation.refreshId;
|
|
1571
|
+
|
|
1572
|
+
const prepared = injectManagedRefreshService(
|
|
1573
|
+
unsignedVc,
|
|
1574
|
+
allocation.refreshService
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
signedVc = await traceInternal(
|
|
1578
|
+
'issueCredentialWithSigningAuthority:refresh',
|
|
1579
|
+
async () =>
|
|
1580
|
+
(
|
|
1581
|
+
await issueCredentialWithSigningAuthority(
|
|
1582
|
+
{ type: 'profile', profile },
|
|
1583
|
+
prepared,
|
|
1584
|
+
signingAuthority,
|
|
1585
|
+
domain,
|
|
1586
|
+
false
|
|
1587
|
+
)
|
|
1588
|
+
).credential as VC
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// Plaintext templateData is intentionally omitted from refresh
|
|
1593
|
+
// activity metadata: refresh records never persist claim content.
|
|
1594
|
+
const activityId = await traceDb('logCredentialSent:refresh', () =>
|
|
1595
|
+
logCredentialSent({
|
|
1596
|
+
actorProfileId: profile.profileId,
|
|
1597
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1598
|
+
recipientType: 'profile',
|
|
1599
|
+
recipientIdentifier: targetProfile.profileId,
|
|
1600
|
+
recipientProfileId: targetProfile.profileId,
|
|
1601
|
+
boostUri,
|
|
1602
|
+
source: 'send',
|
|
1603
|
+
integrationId: input.integrationId,
|
|
1604
|
+
})
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
if (intent) {
|
|
1608
|
+
await recordRefreshSendIntentPending(
|
|
1609
|
+
intent,
|
|
1610
|
+
{
|
|
1611
|
+
refreshId,
|
|
1612
|
+
refreshService: managedRefreshServiceFor(refreshId, domain),
|
|
1613
|
+
credentialId: signedVc.id as string,
|
|
1614
|
+
issuerDid:
|
|
1615
|
+
getCredentialIssuerId(signedVc) ??
|
|
1616
|
+
getDidWeb(domain, profile.profileId),
|
|
1617
|
+
holderDid:
|
|
1618
|
+
intent.holderDid ??
|
|
1619
|
+
getDidWeb(domain, targetProfile.profileId),
|
|
1620
|
+
...(signedVc.credentialStatus
|
|
1621
|
+
? { credentialStatus: signedVc.credentialStatus }
|
|
1622
|
+
: {}),
|
|
1623
|
+
},
|
|
1624
|
+
activityId
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
try {
|
|
1629
|
+
const { uri, receipt } = await sendRefreshableCredential({
|
|
1630
|
+
issuerProfile: profile,
|
|
1631
|
+
refreshId,
|
|
1632
|
+
credential: signedVc,
|
|
1633
|
+
boostUri,
|
|
1634
|
+
skipNotification,
|
|
1635
|
+
domain,
|
|
1636
|
+
activityId,
|
|
1637
|
+
integrationId: input.integrationId,
|
|
1638
|
+
contractTerms: contractTerms ?? undefined,
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
const response = {
|
|
1642
|
+
type: 'boost' as const,
|
|
1643
|
+
credentialUri: uri,
|
|
1644
|
+
uri: boostUri,
|
|
1645
|
+
activityId,
|
|
1646
|
+
refresh: receipt,
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
if (intent) await markRefreshSendIntentDelivered(intent, response);
|
|
1650
|
+
|
|
1651
|
+
return response;
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
await traceDb('logCredentialFailed:refresh', () =>
|
|
1654
|
+
logCredentialFailed({
|
|
1655
|
+
activityId,
|
|
1656
|
+
actorProfileId: profile.profileId,
|
|
1657
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1658
|
+
recipientType: 'profile',
|
|
1659
|
+
recipientIdentifier: targetProfile.profileId,
|
|
1660
|
+
recipientProfileId: targetProfile.profileId,
|
|
1661
|
+
boostUri,
|
|
1662
|
+
integrationId: input.integrationId,
|
|
1663
|
+
source: 'send',
|
|
1664
|
+
metadata: {
|
|
1665
|
+
error:
|
|
1666
|
+
error instanceof Error
|
|
1667
|
+
? error.message
|
|
1668
|
+
: 'Unknown error',
|
|
1669
|
+
},
|
|
1670
|
+
})
|
|
1671
|
+
);
|
|
1672
|
+
throw error;
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
830
1677
|
// Route to Universal Inbox for email/phone recipients
|
|
831
1678
|
if (inboxRecipient) {
|
|
832
1679
|
// Try to resolve recipient profile for auto-populating template variables + recipient DID
|
|
@@ -883,11 +1730,12 @@ export const boostsRouter = t.router({
|
|
|
883
1730
|
});
|
|
884
1731
|
}
|
|
885
1732
|
|
|
886
|
-
|
|
887
|
-
credential
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1733
|
+
if (!inboxRefreshRequested)
|
|
1734
|
+
credential = await appendBitstringStatusListEntries(
|
|
1735
|
+
credential,
|
|
1736
|
+
profile.profileId,
|
|
1737
|
+
domain
|
|
1738
|
+
);
|
|
891
1739
|
}
|
|
892
1740
|
|
|
893
1741
|
// Build inbox configuration from SendOptions
|
|
@@ -895,6 +1743,7 @@ export const boostsRouter = t.router({
|
|
|
895
1743
|
const activityId = await traceDb('logCredentialSent:inbox', () =>
|
|
896
1744
|
logCredentialSent({
|
|
897
1745
|
actorProfileId: profile.profileId,
|
|
1746
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
898
1747
|
recipientType: inboxRecipient.type,
|
|
899
1748
|
recipientIdentifier: inboxRecipient.value,
|
|
900
1749
|
boostUri,
|
|
@@ -914,6 +1763,9 @@ export const boostsRouter = t.router({
|
|
|
914
1763
|
credential,
|
|
915
1764
|
{
|
|
916
1765
|
...inboxConfig,
|
|
1766
|
+
refresh: inboxRefreshRequested,
|
|
1767
|
+
idempotencyKey: inboxKey,
|
|
1768
|
+
refreshRequestDigest: inboxDigest,
|
|
917
1769
|
activityId,
|
|
918
1770
|
integrationId: input.integrationId,
|
|
919
1771
|
},
|
|
@@ -921,12 +1773,15 @@ export const boostsRouter = t.router({
|
|
|
921
1773
|
)
|
|
922
1774
|
);
|
|
923
1775
|
|
|
924
|
-
|
|
1776
|
+
const result = {
|
|
925
1777
|
type: 'boost' as const,
|
|
926
1778
|
credentialUri: '',
|
|
927
1779
|
uri: boostUri,
|
|
928
|
-
activityId,
|
|
1780
|
+
activityId: inboxResult.inboxCredential.activityId ?? activityId,
|
|
929
1781
|
inbox: {
|
|
1782
|
+
...(inboxResult.refresh
|
|
1783
|
+
? { refresh: inboxResult.refresh }
|
|
1784
|
+
: {}),
|
|
930
1785
|
issuanceId: inboxResult.inboxCredential.id,
|
|
931
1786
|
status: inboxResult.status,
|
|
932
1787
|
claimUrl: inboxResult.claimUrl,
|
|
@@ -935,12 +1790,16 @@ export const boostsRouter = t.router({
|
|
|
935
1790
|
: {}),
|
|
936
1791
|
},
|
|
937
1792
|
};
|
|
1793
|
+
if (inboxIntent)
|
|
1794
|
+
await markRefreshSendIntentDelivered(inboxIntent, result);
|
|
1795
|
+
return result;
|
|
938
1796
|
} catch (error) {
|
|
939
1797
|
// Log FAILED activity when issueToInbox fails
|
|
940
1798
|
await traceDb('logCredentialFailed:inbox', () =>
|
|
941
1799
|
logCredentialFailed({
|
|
942
1800
|
activityId,
|
|
943
1801
|
actorProfileId: profile.profileId,
|
|
1802
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
944
1803
|
recipientType: inboxRecipient.type,
|
|
945
1804
|
recipientIdentifier: inboxRecipient.value,
|
|
946
1805
|
boostUri,
|
|
@@ -1029,6 +1888,7 @@ export const boostsRouter = t.router({
|
|
|
1029
1888
|
const activityId = await traceDb('logCredentialSent:remoteInbox', () =>
|
|
1030
1889
|
logCredentialSent({
|
|
1031
1890
|
actorProfileId: profile.profileId,
|
|
1891
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1032
1892
|
recipientType: 'profile',
|
|
1033
1893
|
recipientIdentifier: input.recipient,
|
|
1034
1894
|
boostUri,
|
|
@@ -1071,6 +1931,7 @@ export const boostsRouter = t.router({
|
|
|
1071
1931
|
logCredentialFailed({
|
|
1072
1932
|
activityId,
|
|
1073
1933
|
actorProfileId: profile.profileId,
|
|
1934
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1074
1935
|
recipientType: 'profile',
|
|
1075
1936
|
recipientIdentifier: input.recipient,
|
|
1076
1937
|
boostUri,
|
|
@@ -1130,53 +1991,13 @@ export const boostsRouter = t.router({
|
|
|
1130
1991
|
});
|
|
1131
1992
|
}
|
|
1132
1993
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (contractUri) {
|
|
1142
|
-
decodedContractUri = decodeURIComponent(contractUri);
|
|
1143
|
-
contractDetails = await traceDb('getContractDetailsByUri', () =>
|
|
1144
|
-
getContractDetailsByUri(decodedContractUri!)
|
|
1145
|
-
);
|
|
1146
|
-
|
|
1147
|
-
if (!contractDetails) {
|
|
1148
|
-
throw new TRPCError({
|
|
1149
|
-
code: 'NOT_FOUND',
|
|
1150
|
-
message: 'Could not find contract',
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const terms = await traceDb('getContractTermsForProfile', () =>
|
|
1155
|
-
getContractTermsForProfile(targetProfile, contractDetails!.contract)
|
|
1156
|
-
);
|
|
1157
|
-
|
|
1158
|
-
const writers = await traceDb('getWritersForContract', () =>
|
|
1159
|
-
getWritersForContract(contractDetails!.contract)
|
|
1160
|
-
);
|
|
1161
|
-
const isWriter = writers.some(
|
|
1162
|
-
writer => writer.profileId === profile.profileId
|
|
1163
|
-
);
|
|
1164
|
-
const isDenied =
|
|
1165
|
-
terms?.terms.deniedWriters?.includes(profile.profileId) ?? false;
|
|
1166
|
-
const categoryAllowed = boost.category
|
|
1167
|
-
? terms?.terms.write?.credentials?.categories?.[boost.category] === true
|
|
1168
|
-
: true;
|
|
1169
|
-
|
|
1170
|
-
if (terms && isWriter && !isDenied && categoryAllowed) {
|
|
1171
|
-
contractTerms = terms;
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
if (boostCreated && contractDetails) {
|
|
1176
|
-
await traceDb('setRelatedBoostForContract', () =>
|
|
1177
|
-
setRelatedBoostForContract(contractDetails!.contract, boost!)
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1994
|
+
const contractTerms = await resolveContractForSend({
|
|
1995
|
+
profile,
|
|
1996
|
+
targetProfile: targetProfile!,
|
|
1997
|
+
boost: boost!,
|
|
1998
|
+
boostCreated,
|
|
1999
|
+
contractUri,
|
|
2000
|
+
});
|
|
1180
2001
|
|
|
1181
2002
|
let signedVc: VC | JWE;
|
|
1182
2003
|
|
|
@@ -1240,6 +2061,7 @@ export const boostsRouter = t.router({
|
|
|
1240
2061
|
const activityId = await traceDb('logCredentialSent', () =>
|
|
1241
2062
|
logCredentialSent({
|
|
1242
2063
|
actorProfileId: profile.profileId,
|
|
2064
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1243
2065
|
recipientType: 'profile',
|
|
1244
2066
|
recipientIdentifier: targetProfile.profileId,
|
|
1245
2067
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -1270,6 +2092,7 @@ export const boostsRouter = t.router({
|
|
|
1270
2092
|
logCredentialFailed({
|
|
1271
2093
|
activityId,
|
|
1272
2094
|
actorProfileId: profile.profileId,
|
|
2095
|
+
onBehalfOf: ctx.user.onBehalfOf,
|
|
1273
2096
|
recipientType: 'profile',
|
|
1274
2097
|
recipientIdentifier: targetProfile.profileId,
|
|
1275
2098
|
recipientProfileId: targetProfile.profileId,
|
|
@@ -3727,17 +4550,7 @@ export const boostsRouter = t.router({
|
|
|
3727
4550
|
}
|
|
3728
4551
|
unsignedVc.issuer = { id: getDidWeb(ctx.domain, profile.profileId) };
|
|
3729
4552
|
|
|
3730
|
-
|
|
3731
|
-
unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
|
|
3732
|
-
...subject,
|
|
3733
|
-
id: getDidWeb(ctx.domain, targetProfile.profileId),
|
|
3734
|
-
}));
|
|
3735
|
-
} else {
|
|
3736
|
-
unsignedVc.credentialSubject = {
|
|
3737
|
-
...unsignedVc.credentialSubject,
|
|
3738
|
-
id: getDidWeb(ctx.domain, targetProfile.profileId),
|
|
3739
|
-
};
|
|
3740
|
-
}
|
|
4553
|
+
setCredentialSubjectIds(unsignedVc, getDidWeb(ctx.domain, targetProfile.profileId));
|
|
3741
4554
|
if (unsignedVc?.type?.includes('BoostCredential')) unsignedVc.boostId = boostUri;
|
|
3742
4555
|
// Inject OBv3 skill alignments based on boost's framework/skills
|
|
3743
4556
|
await injectObv3AlignmentsIntoCredentialForBoost(unsignedVc, boost, ctx.domain);
|