@learncard/network-brain-service 4.1.2 → 5.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 +14 -0
- package/CHANGELOG.md +164 -0
- package/README.md +27 -1
- package/bootstrapLambda.test.ts +70 -32
- package/config/config.dev.json +1 -0
- package/config/config.example.json +2 -1
- package/config/config.production.json +1 -0
- package/config/config.scouts.dev.json +1 -0
- package/config/config.scouts.production.json +1 -0
- package/consentFlowDeadlineLambda.test.ts +108 -0
- package/consentFlowDeadlineLambda.ts +18 -0
- package/consentFlowDeadlineLambdaApp.ts +14 -0
- package/dist/accesslayer/boost/create.d.ts +8 -2
- package/dist/accesslayer/boost/create.d.ts.map +1 -1
- package/dist/accesslayer/campaign/audience.d.ts +4 -4
- package/dist/accesslayer/campaign/audience.d.ts.map +1 -1
- package/dist/accesslayer/campaign/create.d.ts +1 -1
- package/dist/accesslayer/campaign/create.d.ts.map +1 -1
- package/dist/accesslayer/campaign/delivery.d.ts +5 -0
- package/dist/accesslayer/campaign/delivery.d.ts.map +1 -0
- package/dist/accesslayer/campaign/dispatch.d.ts +5 -0
- package/dist/accesslayer/campaign/dispatch.d.ts.map +1 -0
- package/dist/accesslayer/campaign/dispatch.test.d.ts +2 -0
- package/dist/accesslayer/campaign/dispatch.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/email.d.ts +10 -0
- package/dist/accesslayer/campaign/email.d.ts.map +1 -0
- package/dist/accesslayer/campaign/email.test.d.ts +2 -0
- package/dist/accesslayer/campaign/email.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/preferences.d.ts +5 -0
- package/dist/accesslayer/campaign/preferences.d.ts.map +1 -0
- package/dist/accesslayer/campaign/read.d.ts.map +1 -1
- package/dist/accesslayer/campaign/test.d.ts +5 -0
- package/dist/accesslayer/campaign/test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/test.test.d.ts +2 -0
- package/dist/accesslayer/campaign/test.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/update.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/delete.d.ts +1 -1
- package/dist/accesslayer/consentflowcontract/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +2 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +19 -4
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +6 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/versions.d.ts +68 -0
- package/dist/accesslayer/consentflowcontract/versions.d.ts.map +1 -0
- package/dist/accesslayer/credential/create.d.ts +3 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/delete.d.ts +1 -0
- package/dist/accesslayer/credential/delete.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +15 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/read.d.ts +2 -0
- package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts +1 -0
- 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-credential/create.d.ts +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +7 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -1
- package/dist/app.d.ts +11 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +61 -1
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/campaign-unsubscribe.d.ts +7 -0
- package/dist/campaign-unsubscribe.d.ts.map +1 -0
- package/dist/campaign-unsubscribe.test.d.ts +2 -0
- package/dist/campaign-unsubscribe.test.d.ts.map +1 -0
- package/dist/config/environment.d.ts +42 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/stageConfig.d.ts +4 -0
- package/dist/config/stageConfig.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/boost.helpers.d.ts +4 -1
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/campaign-app-origin.helpers.d.ts +2 -0
- package/dist/helpers/campaign-app-origin.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-email.helpers.d.ts +13 -0
- package/dist/helpers/campaign-email.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-notifications.helpers.d.ts +7 -0
- package/dist/helpers/campaign-notifications.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-queue.helpers.d.ts +7 -0
- package/dist/helpers/campaign-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/campaign-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/campaign-unsubscribe-page.d.ts +2 -0
- package/dist/helpers/campaign-unsubscribe-page.d.ts.map +1 -0
- package/dist/helpers/campaign-unsubscribe.helpers.d.ts +7 -0
- package/dist/helpers/campaign-unsubscribe.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-webhook.helpers.test.d.ts +2 -0
- package/dist/helpers/campaign-webhook.helpers.test.d.ts.map +1 -0
- package/dist/helpers/consentflow-deadline.helpers.d.ts +5 -0
- package/dist/helpers/consentflow-deadline.helpers.d.ts.map +1 -0
- package/dist/helpers/consentflow-legacy-migration.helpers.d.ts +2 -0
- package/dist/helpers/consentflow-legacy-migration.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -1
- package/dist/helpers/contract.helpers.d.ts +1 -1
- package/dist/helpers/contract.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-display.helpers.d.ts +5 -0
- package/dist/helpers/credential-display.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-display.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-display.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-refresh-materiality.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-materiality.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-refresh.helpers.d.ts +30 -1
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-status-mutation.helpers.d.ts +5 -0
- package/dist/helpers/credential-status-mutation.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-status-mutation.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-status-mutation.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential.helpers.d.ts +1 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-refresh.helpers.d.ts +4 -2
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts +1 -0
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/learnCard.helpers.d.ts +1 -0
- package/dist/helpers/learnCard.helpers.d.ts.map +1 -1
- package/dist/helpers/managed-status.helpers.d.ts +7 -0
- package/dist/helpers/managed-status.helpers.d.ts.map +1 -0
- package/dist/helpers/managed-status.helpers.test.d.ts +2 -0
- package/dist/helpers/managed-status.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts +2 -0
- package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notification-webhook-fetch.helpers.d.ts +13 -0
- package/dist/helpers/notification-webhook-fetch.helpers.d.ts.map +1 -0
- package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts +2 -0
- package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +13 -5
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.test.d.ts +2 -0
- package/dist/helpers/refresh-send-intent.helpers.test.d.ts.map +1 -0
- package/dist/helpers/send-contract.helpers.d.ts +11 -0
- package/dist/helpers/send-contract.helpers.d.ts.map +1 -0
- package/dist/helpers/status-list.helpers.d.ts +12 -1
- package/dist/helpers/status-list.helpers.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowContractVersion.d.ts +12 -0
- package/dist/models/ConsentFlowContractVersion.d.ts.map +1 -0
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/Credential.d.ts +2 -0
- package/dist/models/Credential.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -0
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +1 -1
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/campaign-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/openapi.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/routes/activity.d.ts +15 -5
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts +15 -5
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +12 -2
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/bench.d.ts +11 -1
- package/dist/routes/bench.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +70 -15
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/campaigns.d.ts +183 -51
- package/dist/routes/campaigns.d.ts.map +1 -1
- package/dist/routes/claim-hooks.d.ts +11 -1
- package/dist/routes/claim-hooks.d.ts.map +1 -1
- package/dist/routes/contact-methods.d.ts +11 -1
- package/dist/routes/contact-methods.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +374 -8
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-input-privacy.test.d.ts +2 -0
- package/dist/routes/credential-input-privacy.test.d.ts.map +1 -0
- package/dist/routes/credential-refreshes.d.ts +210 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credential-share-previews.d.ts +11 -1
- package/dist/routes/credential-share-previews.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +132 -3
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/did-metadata.d.ts +11 -1
- package/dist/routes/did-metadata.d.ts.map +1 -1
- package/dist/routes/federation.d.ts +11 -1
- package/dist/routes/federation.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +54 -19
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +100 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/integrations.d.ts +14 -4
- package/dist/routes/integrations.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +11 -1
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts +11 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts +11 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +22 -2
- package/dist/routes/public-share-links.d.ts.map +1 -1
- package/dist/routes/share-links.d.ts +36 -16
- package/dist/routes/share-links.d.ts.map +1 -1
- package/dist/routes/skill-frameworks.d.ts +11 -1
- package/dist/routes/skill-frameworks.d.ts.map +1 -1
- package/dist/routes/skills.d.ts +11 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/storage.d.ts +37 -23
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/test.d.ts +79 -3
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/utilities.d.ts +11 -1
- package/dist/routes/utilities.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts +11 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts +3 -0
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/delivery.service.d.ts +10 -0
- package/dist/services/delivery/delivery.service.d.ts.map +1 -1
- package/dist/types/activity.d.ts +3 -3
- package/dist/types/app-manifest.d.ts +10 -10
- package/dist/types/campaign.d.ts +13 -1
- package/dist/types/campaign.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +175 -7
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +27 -0
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/credential.d.ts +5 -0
- package/dist/types/credential.d.ts.map +1 -1
- package/dist/types/integration.d.ts +3 -3
- package/lambda.ts +44 -14
- package/lambdaApp.ts +20 -0
- package/package.json +20 -19
- package/project.json +5 -0
- package/scripts/migrate-consent-flow-versions.ts +9 -0
- package/scripts/report-send-template-exposure.ts +47 -0
- package/scripts/verify-python-contracts.ts +16 -1
- package/serverless-local.yml +15 -2
- package/serverless.function-env.cjs +4 -0
- package/serverless.local.yml +12 -0
- package/serverless.yml +48 -3
- package/src/accesslayer/boost/create.ts +62 -7
- package/src/accesslayer/campaign/README.md +275 -6
- package/src/accesslayer/campaign/audience.ts +39 -7
- package/src/accesslayer/campaign/create.ts +4 -2
- package/src/accesslayer/campaign/delivery.ts +66 -0
- package/src/accesslayer/campaign/dispatch.test.ts +59 -0
- package/src/accesslayer/campaign/dispatch.ts +172 -0
- package/src/accesslayer/campaign/email.test.ts +73 -0
- package/src/accesslayer/campaign/email.ts +91 -0
- package/src/accesslayer/campaign/preferences.ts +53 -0
- package/src/accesslayer/campaign/read.ts +3 -1
- package/src/accesslayer/campaign/state.ts +1 -1
- package/src/accesslayer/campaign/test.test.ts +31 -0
- package/src/accesslayer/campaign/test.ts +75 -0
- package/src/accesslayer/campaign/update.ts +15 -4
- package/src/accesslayer/consentflowcontract/create.ts +20 -1
- package/src/accesslayer/consentflowcontract/delete.ts +121 -3
- package/src/accesslayer/consentflowcontract/read.ts +15 -9
- package/src/accesslayer/consentflowcontract/relationships/create.ts +70 -36
- package/src/accesslayer/consentflowcontract/relationships/read.ts +305 -73
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +1 -1
- package/src/accesslayer/consentflowcontract/relationships/update.ts +269 -98
- package/src/accesslayer/consentflowcontract/versions.ts +790 -0
- package/src/accesslayer/credential/create.ts +33 -9
- package/src/accesslayer/credential/delete.ts +45 -0
- package/src/accesslayer/credential/relationships/create.ts +102 -47
- package/src/accesslayer/credential/relationships/update.ts +8 -116
- package/src/accesslayer/credential-refresh/read.ts +26 -2
- package/src/accesslayer/credential-refresh/update.ts +6 -2
- package/src/accesslayer/inbox-credential/create.ts +7 -1
- package/src/accesslayer/inbox-credential/refresh.ts +14 -4
- package/src/campaign-unsubscribe.test.ts +158 -0
- package/src/campaign-unsubscribe.ts +86 -0
- package/src/config/environment.test.ts +52 -0
- package/src/config/environment.ts +58 -0
- package/src/config/serverlessFunctionEnv.test.ts +59 -0
- package/src/credential-refresh.ts +1 -0
- package/src/docker-entry.ts +26 -1
- package/src/helpers/boost-storage.helpers.test.ts +107 -41
- package/src/helpers/boost.helpers.ts +88 -40
- package/src/helpers/campaign-app-origin.helpers.ts +25 -0
- package/src/helpers/campaign-email.helpers.ts +113 -0
- package/src/helpers/campaign-notifications.helpers.ts +127 -0
- package/src/helpers/campaign-queue.helpers.test.ts +442 -0
- package/src/helpers/campaign-queue.helpers.ts +122 -0
- package/src/helpers/campaign-unsubscribe-page.ts +107 -0
- package/src/helpers/campaign-unsubscribe.helpers.ts +56 -0
- package/src/helpers/campaign-webhook.helpers.test.ts +137 -0
- package/src/helpers/consentflow-deadline.helpers.ts +22 -0
- package/src/helpers/consentflow-legacy-migration.helpers.ts +179 -0
- package/src/helpers/contract-events.helpers.ts +35 -3
- package/src/helpers/contract-requests.helpers.ts +1 -1
- package/src/helpers/contract.helpers.ts +5 -1
- package/src/helpers/credential-display.helpers.test.ts +68 -0
- package/src/helpers/credential-display.helpers.ts +15 -0
- package/src/helpers/credential-refresh-email.helpers.ts +5 -12
- package/src/helpers/credential-refresh-materiality.helpers.test.ts +138 -1
- package/src/helpers/credential-refresh-materiality.helpers.ts +62 -1
- package/src/helpers/credential-refresh-proof.helpers.test.ts +1 -1
- package/src/helpers/credential-refresh-proof.helpers.ts +2 -2
- package/src/helpers/credential-refresh.helpers.ts +1166 -376
- package/src/helpers/credential-status-mutation.helpers.test.ts +155 -0
- package/src/helpers/credential-status-mutation.helpers.ts +166 -0
- package/src/helpers/credential.helpers.ts +19 -11
- package/src/helpers/finalize-inbox.helpers.ts +1 -1
- package/src/helpers/inbox-refresh.helpers.ts +34 -8
- package/src/helpers/inbox.helpers.ts +2 -0
- package/src/helpers/learnCard.helpers.ts +23 -0
- package/src/helpers/managed-status.helpers.test.ts +101 -0
- package/src/helpers/managed-status.helpers.ts +147 -0
- package/src/helpers/notification-delivery-boundary.helpers.test.ts +114 -0
- package/src/helpers/notification-webhook-fetch.helpers.test.ts +228 -0
- package/src/helpers/notification-webhook-fetch.helpers.ts +166 -0
- package/src/helpers/notificationQueue.helpers.ts +5 -0
- package/src/helpers/notifications.helpers.ts +19 -12
- package/src/helpers/refresh-send-intent.helpers.test.ts +60 -0
- package/src/helpers/refresh-send-intent.helpers.ts +81 -18
- package/src/helpers/send-contract.helpers.ts +36 -0
- package/src/helpers/status-list.helpers.test.ts +29 -12
- package/src/helpers/status-list.helpers.ts +329 -20
- package/src/models/ConsentFlowContract.ts +3 -0
- package/src/models/ConsentFlowContractVersion.ts +50 -0
- package/src/models/ConsentFlowTerms.ts +4 -0
- package/src/models/Credential.ts +2 -0
- package/src/models/CredentialRefresh.ts +2 -0
- package/src/models/Profile.ts +2 -0
- package/src/models/campaign-constraints.ts +13 -8
- package/src/models/index.ts +2 -0
- package/src/openapi.ts +21 -0
- package/src/routes/app-store.ts +2 -1
- package/src/routes/boosts.ts +219 -137
- package/src/routes/campaigns.ts +98 -19
- package/src/routes/contracts.ts +433 -115
- package/src/routes/credential-input-privacy.test.ts +92 -0
- package/src/routes/credential-refreshes.ts +273 -2
- package/src/routes/federation.ts +7 -2
- package/src/routes/inbox.ts +6 -1
- package/src/routes/index.ts +31 -2
- package/src/routes/workflows.ts +61 -46
- package/src/services/delivery/adapters/postmark.adapter.test.ts +70 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +84 -11
- package/src/services/delivery/delivery.service.ts +4 -0
- package/src/types/campaign.ts +14 -1
- package/src/types/consentflowcontract.ts +43 -2
- package/src/types/credential-refresh.ts +11 -1
- package/src/types/credential.ts +2 -0
- package/test/boost-status-cleanup.spec.ts +123 -0
- package/test/campaigns-email.unit.spec.ts +268 -0
- package/test/campaigns.spec.ts +804 -30
- package/test/consent-autoboost-idempotency.spec.ts +432 -0
- package/test/consent-data-boundaries.spec.ts +322 -26
- package/test/consent-legacy-migration.spec.ts +315 -0
- package/test/consent-recipients.spec.ts +7 -7
- package/test/consent-version-updates.spec.ts +761 -0
- package/test/consentflow.spec.ts +698 -226
- package/test/contract-requests.spec.ts +55 -9
- package/test/credential-refresh-allocation.spec.ts +212 -14
- package/test/credential-refresh-email.spec.ts +6 -10
- package/test/credential-refresh-endpoint.spec.ts +3 -0
- package/test/credential-refresh-publication.spec.ts +52 -15
- package/test/credential-storage.neo4j.spec.ts +33 -6
- package/test/helpers/legacy-autoboost.ts +48 -0
- package/test/holder-mode-send.spec.ts +710 -0
- package/test/inbox-refresh.spec.ts +105 -0
- package/test/inbox-security.e2e.ts +18 -22
- package/test/managed-status-revocation.spec.ts +970 -0
- package/test/notifications-worker.spec.ts +31 -25
- package/test/refresh-send.spec.ts +334 -5
- package/test/smart-resume-consent.spec.ts +1 -1
- package/test/verification/campaigns.ts +40 -0
- package/vitest.campaigns.config.ts +2 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { getIdFromUri } from '@helpers/uri.helpers';
|
|
3
|
+
import { flattenObject } from '@helpers/objects.helpers';
|
|
4
|
+
import {
|
|
5
|
+
classifyContractChange,
|
|
6
|
+
diffContractChange,
|
|
7
|
+
buildContractUpdatePreview,
|
|
8
|
+
} from '@learncard/helpers';
|
|
9
|
+
import {
|
|
10
|
+
ConsentFlowContractValidator,
|
|
11
|
+
ConsentFlowContractChangeKindValidator,
|
|
12
|
+
} from '@learncard/types';
|
|
13
|
+
import type {
|
|
14
|
+
ConsentFlowContract as ConsentFlowContractType,
|
|
15
|
+
ConsentFlowContractChangeKind,
|
|
16
|
+
ConsentFlowContractUpdateResult,
|
|
17
|
+
ConsentFlowContractChanges,
|
|
18
|
+
ConsentFlowContractDetails,
|
|
19
|
+
ConsentFlowTermsStatus,
|
|
20
|
+
ConsentFlowContractDiff,
|
|
21
|
+
} from '@learncard/types';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { migrateLegacyConsentFlowContracts } from '@helpers/consentflow-legacy-migration.helpers';
|
|
24
|
+
import { neogma } from '@instance';
|
|
25
|
+
import { TRPCError } from '@trpc/server';
|
|
26
|
+
import { v4 as uuid } from 'uuid';
|
|
27
|
+
import { getProfileByProfileId } from '@accesslayer/profile/read';
|
|
28
|
+
import { getSigningAuthorityForUserByName } from '@accesslayer/signing-authority/relationships/read';
|
|
29
|
+
import { getBoostById } from '@accesslayer/boost/read';
|
|
30
|
+
import { canProfileIssueBoost } from '@accesslayer/boost/relationships/read';
|
|
31
|
+
import { tryDispatchContractEvent } from '@helpers/contract-events.helpers';
|
|
32
|
+
|
|
33
|
+
export type StoredContractVersion = {
|
|
34
|
+
id: string;
|
|
35
|
+
version: number;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
kind?: ConsentFlowContractChangeKind;
|
|
39
|
+
contract: ConsentFlowContractType;
|
|
40
|
+
name: string;
|
|
41
|
+
subtitle?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
reasonForAccessing?: string;
|
|
44
|
+
image?: string;
|
|
45
|
+
needsGuardianConsent?: boolean;
|
|
46
|
+
redirectUrl?: string;
|
|
47
|
+
frontDoorBoostUri?: string;
|
|
48
|
+
expiresAt?: string;
|
|
49
|
+
autoboosts: {
|
|
50
|
+
id: string;
|
|
51
|
+
signingAuthorityEndpoint: string;
|
|
52
|
+
signingAuthorityName: string;
|
|
53
|
+
issuer?: string;
|
|
54
|
+
}[];
|
|
55
|
+
writers: string[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const StoredAutoBoostValidator = z.object({
|
|
59
|
+
id: z.string(),
|
|
60
|
+
signingAuthorityEndpoint: z.string(),
|
|
61
|
+
signingAuthorityName: z.string(),
|
|
62
|
+
issuer: z
|
|
63
|
+
.string()
|
|
64
|
+
.nullable()
|
|
65
|
+
.optional()
|
|
66
|
+
.transform(value => value ?? undefined),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const parseJson = <T>(value: unknown, validator: z.ZodType<T>, field: string): T => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
72
|
+
return validator.parse(parsed);
|
|
73
|
+
} catch {
|
|
74
|
+
throw new TRPCError({
|
|
75
|
+
code: 'PRECONDITION_FAILED',
|
|
76
|
+
message: `Stored contract version has invalid ${field}`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const parseVersion = (raw: Record<string, unknown>): StoredContractVersion =>
|
|
82
|
+
({
|
|
83
|
+
...raw,
|
|
84
|
+
version: Number(raw.version),
|
|
85
|
+
contract: parseJson(raw.contract, ConsentFlowContractValidator, 'contract'),
|
|
86
|
+
autoboosts: parseJson(raw.autoboosts, z.array(StoredAutoBoostValidator), 'autoboosts'),
|
|
87
|
+
writers: parseJson(raw.writers, z.array(z.string()), 'writers'),
|
|
88
|
+
...(raw.kind ? { kind: ConsentFlowContractChangeKindValidator.parse(raw.kind) } : {}),
|
|
89
|
+
}) as StoredContractVersion;
|
|
90
|
+
|
|
91
|
+
const mergeScopePatch = (
|
|
92
|
+
current: ConsentFlowContractType['read']['personal'],
|
|
93
|
+
patch?: Record<string, ConsentFlowContractType['read']['personal'][string] | null>
|
|
94
|
+
) => {
|
|
95
|
+
if (!patch) return current;
|
|
96
|
+
const merged = { ...current };
|
|
97
|
+
for (const [key, config] of Object.entries(patch)) {
|
|
98
|
+
if (config === null) delete merged[key];
|
|
99
|
+
else merged[key] = { ...current[key], ...config };
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const mergeConsentFlowContractPatch = (
|
|
105
|
+
current: ConsentFlowContractType,
|
|
106
|
+
patch: ConsentFlowContractChanges['contract']
|
|
107
|
+
): ConsentFlowContractType => {
|
|
108
|
+
const read = patch?.read
|
|
109
|
+
? {
|
|
110
|
+
...current.read,
|
|
111
|
+
...patch.read,
|
|
112
|
+
credentials: patch.read.credentials
|
|
113
|
+
? {
|
|
114
|
+
...current.read.credentials,
|
|
115
|
+
...patch.read.credentials,
|
|
116
|
+
categories: mergeScopePatch(
|
|
117
|
+
current.read.credentials.categories,
|
|
118
|
+
patch.read.credentials.categories
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
: current.read.credentials,
|
|
122
|
+
personal: mergeScopePatch(current.read.personal, patch.read.personal),
|
|
123
|
+
}
|
|
124
|
+
: current.read;
|
|
125
|
+
const write = patch?.write
|
|
126
|
+
? {
|
|
127
|
+
...current.write,
|
|
128
|
+
...patch.write,
|
|
129
|
+
credentials: patch.write.credentials
|
|
130
|
+
? {
|
|
131
|
+
...current.write.credentials,
|
|
132
|
+
...patch.write.credentials,
|
|
133
|
+
categories: mergeScopePatch(
|
|
134
|
+
current.write.credentials.categories,
|
|
135
|
+
patch.write.credentials.categories
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
: current.write.credentials,
|
|
139
|
+
personal: mergeScopePatch(current.write.personal, patch.write.personal),
|
|
140
|
+
}
|
|
141
|
+
: current.write;
|
|
142
|
+
return ConsentFlowContractValidator.parse({ ...current, read, write });
|
|
143
|
+
};
|
|
144
|
+
export const getConsentFlowContractVersionRecord = async (
|
|
145
|
+
contractId: string,
|
|
146
|
+
versionNumber: number
|
|
147
|
+
): Promise<StoredContractVersion | null> => {
|
|
148
|
+
let result = await neogma.queryRunner.run(
|
|
149
|
+
`MATCH (:ConsentFlowContract {id:$contractId})-[:HAS_VERSION]->(v:ConsentFlowContractVersion {version:$version})
|
|
150
|
+
RETURN properties(v) AS version LIMIT 1`,
|
|
151
|
+
{ contractId, version: versionNumber }
|
|
152
|
+
);
|
|
153
|
+
if (!result.records.length && versionNumber === 1) {
|
|
154
|
+
await migrateLegacyConsentFlowContracts(contractId);
|
|
155
|
+
result = await neogma.queryRunner.run(
|
|
156
|
+
`MATCH (:ConsentFlowContract {id:$contractId})-[:HAS_VERSION]->(v:ConsentFlowContractVersion {version:$version})
|
|
157
|
+
RETURN properties(v) AS version LIMIT 1`,
|
|
158
|
+
{ contractId, version: versionNumber }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const raw = result.records[0]?.get('version') as Record<string, unknown> | undefined;
|
|
162
|
+
return raw ? parseVersion(raw) : null;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Loads accepted snapshots in one query per bounded page, not per consenter. */
|
|
166
|
+
export const getConsentFlowContractVersionRecords = async (
|
|
167
|
+
requests: { contractId: string; version: number }[]
|
|
168
|
+
): Promise<Map<string, StoredContractVersion>> => {
|
|
169
|
+
const unique = new Map(
|
|
170
|
+
requests.map(request => [`${request.contractId}:${request.version}`, request])
|
|
171
|
+
);
|
|
172
|
+
const versions = new Map<string, StoredContractVersion>();
|
|
173
|
+
if (!unique.size) return versions;
|
|
174
|
+
const result = await neogma.queryRunner.run(
|
|
175
|
+
`UNWIND $requests AS request
|
|
176
|
+
MATCH (:ConsentFlowContract {id:request.contractId})-[:HAS_VERSION]->(v:ConsentFlowContractVersion {version:request.version})
|
|
177
|
+
RETURN request.contractId AS contractId, properties(v) AS version`,
|
|
178
|
+
{ requests: [...unique.values()] }
|
|
179
|
+
);
|
|
180
|
+
for (const record of result.records) {
|
|
181
|
+
const version = parseVersion(record.get('version') as Record<string, unknown>);
|
|
182
|
+
versions.set(`${record.get('contractId')}:${version.version}`, version);
|
|
183
|
+
}
|
|
184
|
+
return versions;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const getConsentFlowContractVersion = getConsentFlowContractVersionRecord;
|
|
188
|
+
|
|
189
|
+
export const getConsentFlowContractVersions = async (uri: string) => {
|
|
190
|
+
const contractId = getIdFromUri(uri);
|
|
191
|
+
const result = await neogma.queryRunner.run(
|
|
192
|
+
`MATCH (c:ConsentFlowContract {id:$contractId})-[:HAS_VERSION]->(v:ConsentFlowContractVersion)
|
|
193
|
+
RETURN properties(v) AS version ORDER BY v.version DESC`,
|
|
194
|
+
{ contractId }
|
|
195
|
+
);
|
|
196
|
+
return result.records.map(record =>
|
|
197
|
+
parseVersion(record.get('version') as Record<string, unknown>)
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export const getContractVersions = getConsentFlowContractVersions;
|
|
202
|
+
type CumulativeContractUpdateRequest = {
|
|
203
|
+
key: string;
|
|
204
|
+
contractId: string;
|
|
205
|
+
acceptedVersion: number;
|
|
206
|
+
status?: ConsentFlowTermsStatus;
|
|
207
|
+
reconsentDeadline?: string;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** Fetches each accepted/current pair once while retaining per-holder status and deadline. */
|
|
211
|
+
export const getCumulativeContractUpdates = async (
|
|
212
|
+
requests: CumulativeContractUpdateRequest[]
|
|
213
|
+
): Promise<Map<string, NonNullable<ConsentFlowContractDetails['latestUpdate']>>> => {
|
|
214
|
+
const updates = new Map<string, NonNullable<ConsentFlowContractDetails['latestUpdate']>>();
|
|
215
|
+
if (!requests.length) return updates;
|
|
216
|
+
const pairs = new Map(
|
|
217
|
+
requests.map(request => [
|
|
218
|
+
`${request.contractId}:${request.acceptedVersion}`,
|
|
219
|
+
{
|
|
220
|
+
key: `${request.contractId}:${request.acceptedVersion}`,
|
|
221
|
+
contractId: request.contractId,
|
|
222
|
+
acceptedVersion: request.acceptedVersion,
|
|
223
|
+
},
|
|
224
|
+
])
|
|
225
|
+
);
|
|
226
|
+
const result = await neogma.queryRunner.run(
|
|
227
|
+
`UNWIND $requests AS request
|
|
228
|
+
OPTIONAL MATCH (c:ConsentFlowContract {id:request.contractId})
|
|
229
|
+
OPTIONAL MATCH (c)-[:HAS_VERSION]->(accepted:ConsentFlowContractVersion {version:request.acceptedVersion})
|
|
230
|
+
OPTIONAL MATCH (c)-[:HAS_VERSION]->(current:ConsentFlowContractVersion {version:c.currentVersion})
|
|
231
|
+
OPTIONAL MATCH (c)-[:HAS_VERSION]->(intermediate:ConsentFlowContractVersion)
|
|
232
|
+
WHERE intermediate.version > request.acceptedVersion
|
|
233
|
+
AND intermediate.version <= c.currentVersion AND intermediate.kind='requires_consent'
|
|
234
|
+
WITH request, c, accepted, current, intermediate ORDER BY intermediate.version ASC
|
|
235
|
+
RETURN request.key AS key, c.id AS contractId, c.currentVersion AS currentVersion,
|
|
236
|
+
c.latestUpdate AS latestUpdate, properties(accepted) AS accepted, properties(current) AS current,
|
|
237
|
+
head(collect(CASE WHEN intermediate IS NULL THEN null ELSE {reason:intermediate.reason} END)) AS firstRequired`,
|
|
238
|
+
{ requests: [...pairs.values()] }
|
|
239
|
+
);
|
|
240
|
+
const rows = new Map(result.records.map(record => [String(record.get('key')), record]));
|
|
241
|
+
const parsedPairs = new Map<
|
|
242
|
+
string,
|
|
243
|
+
{
|
|
244
|
+
current: StoredContractVersion;
|
|
245
|
+
diff: ConsentFlowContractDiff;
|
|
246
|
+
kind: ConsentFlowContractChangeKind;
|
|
247
|
+
reason?: string;
|
|
248
|
+
requiredReason?: string;
|
|
249
|
+
deadline?: string;
|
|
250
|
+
}
|
|
251
|
+
>();
|
|
252
|
+
for (const request of requests) {
|
|
253
|
+
const key = `${request.contractId}:${request.acceptedVersion}`;
|
|
254
|
+
const record = rows.get(key);
|
|
255
|
+
if (!record?.get('contractId'))
|
|
256
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
|
|
257
|
+
if (request.acceptedVersion >= Number(record.get('currentVersion') ?? 1)) continue;
|
|
258
|
+
let pair = parsedPairs.get(key);
|
|
259
|
+
if (!pair) {
|
|
260
|
+
if (!record.get('accepted') || !record.get('current'))
|
|
261
|
+
throw new TRPCError({
|
|
262
|
+
code: 'PRECONDITION_FAILED',
|
|
263
|
+
message: 'Contract version snapshot is missing',
|
|
264
|
+
});
|
|
265
|
+
const accepted = parseVersion(record.get('accepted') as Record<string, unknown>);
|
|
266
|
+
const current = parseVersion(record.get('current') as Record<string, unknown>);
|
|
267
|
+
const latest = record.get('latestUpdate')
|
|
268
|
+
? parseJson(
|
|
269
|
+
record.get('latestUpdate'),
|
|
270
|
+
z.object({
|
|
271
|
+
reason: z.string().optional(),
|
|
272
|
+
reconsentDeadline: z.string().optional(),
|
|
273
|
+
}),
|
|
274
|
+
'latest update'
|
|
275
|
+
)
|
|
276
|
+
: undefined;
|
|
277
|
+
const firstRequired = record.get('firstRequired') as { reason?: string } | null;
|
|
278
|
+
pair = {
|
|
279
|
+
current,
|
|
280
|
+
diff: diffContractChange(accepted, current),
|
|
281
|
+
kind: classifyContractChange(accepted, current),
|
|
282
|
+
reason: latest?.reason,
|
|
283
|
+
requiredReason: firstRequired?.reason ?? latest?.reason,
|
|
284
|
+
deadline: latest?.reconsentDeadline,
|
|
285
|
+
};
|
|
286
|
+
parsedPairs.set(key, pair);
|
|
287
|
+
}
|
|
288
|
+
const requiresApproval =
|
|
289
|
+
request.status === 'needs_reconsent' || request.status === 'suspended';
|
|
290
|
+
const kind = requiresApproval ? 'requires_consent' : pair.kind;
|
|
291
|
+
const reason = requiresApproval ? pair.requiredReason : pair.reason;
|
|
292
|
+
const deadline = requiresApproval
|
|
293
|
+
? (request.reconsentDeadline ?? pair.deadline)
|
|
294
|
+
: undefined;
|
|
295
|
+
updates.set(request.key, {
|
|
296
|
+
fromVersion: request.acceptedVersion,
|
|
297
|
+
toVersion: pair.current.version,
|
|
298
|
+
kind,
|
|
299
|
+
...(reason ? { reason } : {}),
|
|
300
|
+
diff: pair.diff,
|
|
301
|
+
...(deadline ? { reconsentDeadline: deadline } : {}),
|
|
302
|
+
createdAt: pair.current.createdAt,
|
|
303
|
+
preview: buildContractUpdatePreview({
|
|
304
|
+
name: pair.current.name,
|
|
305
|
+
kind,
|
|
306
|
+
diff: pair.diff,
|
|
307
|
+
status: request.status,
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return updates;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const getCumulativeContractUpdateById = async (
|
|
315
|
+
contractId: string,
|
|
316
|
+
acceptedVersion: number,
|
|
317
|
+
status?: ConsentFlowTermsStatus,
|
|
318
|
+
reconsentDeadline?: string
|
|
319
|
+
): Promise<ConsentFlowContractDetails['latestUpdate']> =>
|
|
320
|
+
(
|
|
321
|
+
await getCumulativeContractUpdates([
|
|
322
|
+
{ key: contractId, contractId, acceptedVersion, status, reconsentDeadline },
|
|
323
|
+
])
|
|
324
|
+
).get(contractId);
|
|
325
|
+
|
|
326
|
+
export const getCumulativeContractUpdate = async (
|
|
327
|
+
uri: string,
|
|
328
|
+
acceptedVersion: number,
|
|
329
|
+
status?: ConsentFlowTermsStatus,
|
|
330
|
+
reconsentDeadline?: string
|
|
331
|
+
): Promise<ConsentFlowContractDetails['latestUpdate']> =>
|
|
332
|
+
getCumulativeContractUpdateById(getIdFromUri(uri), acceptedVersion, status, reconsentDeadline);
|
|
333
|
+
|
|
334
|
+
type ContractChanges = {
|
|
335
|
+
contract?: ConsentFlowContractChanges['contract'];
|
|
336
|
+
name?: string;
|
|
337
|
+
subtitle?: string;
|
|
338
|
+
description?: string;
|
|
339
|
+
reasonForAccessing?: string;
|
|
340
|
+
image?: string;
|
|
341
|
+
needsGuardianConsent?: boolean;
|
|
342
|
+
redirectUrl?: string;
|
|
343
|
+
frontDoorBoostUri?: string;
|
|
344
|
+
expiresAt?: string | null;
|
|
345
|
+
autoboosts?: Omit<StoredContractVersion['autoboosts'][number], 'issuer'>[];
|
|
346
|
+
writers?: string[];
|
|
347
|
+
recipients?: string[];
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
export const updateConsentFlowContractVersion = async (
|
|
351
|
+
{
|
|
352
|
+
uri,
|
|
353
|
+
changes,
|
|
354
|
+
reason,
|
|
355
|
+
reconsentDeadline,
|
|
356
|
+
dryRun,
|
|
357
|
+
expectedVersion,
|
|
358
|
+
}: {
|
|
359
|
+
uri: string;
|
|
360
|
+
changes: ContractChanges;
|
|
361
|
+
reason?: string;
|
|
362
|
+
reconsentDeadline?: string;
|
|
363
|
+
dryRun?: boolean;
|
|
364
|
+
expectedVersion?: number;
|
|
365
|
+
},
|
|
366
|
+
actorProfileId: string,
|
|
367
|
+
domain: string,
|
|
368
|
+
appBaseUrl?: string
|
|
369
|
+
): Promise<ConsentFlowContractUpdateResult> => {
|
|
370
|
+
const contractId = getIdFromUri(uri);
|
|
371
|
+
const storedResult = await neogma.queryRunner.run(
|
|
372
|
+
`MATCH (c:ConsentFlowContract {id:$id})
|
|
373
|
+
RETURN properties(c) AS contract, c.status AS contractStatus, c.currentVersion AS currentVersion,
|
|
374
|
+
[(c)-[r:AUTO_RECEIVE]->(b:Boost) | {id:b.id,signingAuthorityEndpoint:r.signingAuthorityEndpoint,signingAuthorityName:r.signingAuthorityName,issuer:r.issuer}] AS autoBoosts,
|
|
375
|
+
[(c)-[:CREATED_BY]->(owner:Profile) | owner.profileId][0] AS ownerProfileId,
|
|
376
|
+
EXISTS { MATCH (c)-[:CREATED_BY]->(:Profile {profileId:$actor}) } AS owner,
|
|
377
|
+
EXISTS { MATCH (c)-[:CAN_WRITE]->(:Profile {profileId:$actor}) } AS writer`,
|
|
378
|
+
{ id: contractId, actor: actorProfileId }
|
|
379
|
+
);
|
|
380
|
+
const stored = storedResult.records[0];
|
|
381
|
+
if (!stored) throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
|
|
382
|
+
if (!stored.get('owner')) {
|
|
383
|
+
throw new TRPCError({
|
|
384
|
+
code: 'FORBIDDEN',
|
|
385
|
+
message: 'Only the contract owner can update it',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (stored.get('contractStatus') === 'retired') {
|
|
389
|
+
throw new TRPCError({ code: 'FORBIDDEN', message: 'Retired contracts cannot be updated' });
|
|
390
|
+
}
|
|
391
|
+
if (changes.recipients !== undefined) {
|
|
392
|
+
throw new TRPCError({
|
|
393
|
+
code: 'BAD_REQUEST',
|
|
394
|
+
message: 'Use addContractRecipient or removeContractRecipient to update recipients',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const currentVersion = Number(stored.get('currentVersion') ?? 1);
|
|
398
|
+
if (expectedVersion !== undefined && expectedVersion !== currentVersion) {
|
|
399
|
+
throw new TRPCError({
|
|
400
|
+
code: 'CONFLICT',
|
|
401
|
+
message: 'Contract changed since this draft was opened; reload before publishing',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
const current = await getConsentFlowContractVersionRecord(contractId, currentVersion);
|
|
405
|
+
if (!current)
|
|
406
|
+
throw new TRPCError({
|
|
407
|
+
code: 'PRECONDITION_FAILED',
|
|
408
|
+
message: 'Contract version snapshot is missing',
|
|
409
|
+
});
|
|
410
|
+
const existingAutoBoosts =
|
|
411
|
+
(stored.get('autoBoosts') as StoredContractVersion['autoboosts'] | null) ?? [];
|
|
412
|
+
const requestedAutoBoosts = changes.autoboosts ?? current.autoboosts;
|
|
413
|
+
const uniqueAutoBoosts = new Map<string, (typeof requestedAutoBoosts)[number]>();
|
|
414
|
+
for (const config of requestedAutoBoosts) {
|
|
415
|
+
const previous = uniqueAutoBoosts.get(config.id);
|
|
416
|
+
if (
|
|
417
|
+
previous &&
|
|
418
|
+
(previous.signingAuthorityEndpoint !== config.signingAuthorityEndpoint ||
|
|
419
|
+
previous.signingAuthorityName !== config.signingAuthorityName)
|
|
420
|
+
) {
|
|
421
|
+
throw new TRPCError({
|
|
422
|
+
code: 'BAD_REQUEST',
|
|
423
|
+
message: `Conflicting signing authorities for autoboost: ${config.id}`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
uniqueAutoBoosts.set(config.id, config);
|
|
427
|
+
}
|
|
428
|
+
const requestedWriterIds = [...new Set(changes.writers ?? current.writers)];
|
|
429
|
+
const targets = await neogma.queryRunner.run(
|
|
430
|
+
`OPTIONAL MATCH (boost:Boost) WHERE boost.id IN $boostIds
|
|
431
|
+
WITH collect(DISTINCT boost.id) AS boostIds
|
|
432
|
+
OPTIONAL MATCH (writer:Profile) WHERE writer.profileId IN $writerIds
|
|
433
|
+
RETURN boostIds, collect(DISTINCT writer.profileId) AS writerIds`,
|
|
434
|
+
{ boostIds: [...uniqueAutoBoosts.keys()], writerIds: requestedWriterIds }
|
|
435
|
+
);
|
|
436
|
+
const existingBoostIds = new Set((targets.records[0]?.get('boostIds') as string[]) ?? []);
|
|
437
|
+
const existingWriterIds = new Set((targets.records[0]?.get('writerIds') as string[]) ?? []);
|
|
438
|
+
const missingBoost = [...uniqueAutoBoosts.keys()].find(id => !existingBoostIds.has(id));
|
|
439
|
+
const missingWriter = requestedWriterIds.find(id => !existingWriterIds.has(id));
|
|
440
|
+
if (missingBoost || missingWriter) {
|
|
441
|
+
throw new TRPCError({
|
|
442
|
+
code: 'BAD_REQUEST',
|
|
443
|
+
message: missingBoost
|
|
444
|
+
? `Unknown AutoBoost: ${missingBoost}`
|
|
445
|
+
: `Unknown writer: ${missingWriter}`,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const autoboosts = await Promise.all(
|
|
449
|
+
[...uniqueAutoBoosts.values()].map(async config => {
|
|
450
|
+
const existing = existingAutoBoosts.find(item => item.id === config.id);
|
|
451
|
+
const unchanged =
|
|
452
|
+
existing &&
|
|
453
|
+
existing.signingAuthorityEndpoint === config.signingAuthorityEndpoint &&
|
|
454
|
+
existing.signingAuthorityName === config.signingAuthorityName;
|
|
455
|
+
if (existing?.issuer && existing.issuer !== actorProfileId && !unchanged) {
|
|
456
|
+
throw new TRPCError({
|
|
457
|
+
code: 'FORBIDDEN',
|
|
458
|
+
message:
|
|
459
|
+
'Existing delegated delivery cannot change signers; remove it before configuring owner delivery',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
const issuer = unchanged ? (existing.issuer ?? actorProfileId) : actorProfileId;
|
|
463
|
+
if (!unchanged) {
|
|
464
|
+
const issuerProfile = await getProfileByProfileId(issuer);
|
|
465
|
+
const boost = await getBoostById(config.id);
|
|
466
|
+
if (
|
|
467
|
+
!issuerProfile ||
|
|
468
|
+
!boost ||
|
|
469
|
+
!(await canProfileIssueBoost(issuerProfile, boost))
|
|
470
|
+
) {
|
|
471
|
+
throw new TRPCError({
|
|
472
|
+
code: 'UNAUTHORIZED',
|
|
473
|
+
message: 'The contract owner cannot issue this AutoBoost',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const signingAuthority =
|
|
477
|
+
issuerProfile &&
|
|
478
|
+
(await getSigningAuthorityForUserByName(
|
|
479
|
+
issuerProfile,
|
|
480
|
+
config.signingAuthorityEndpoint,
|
|
481
|
+
config.signingAuthorityName.toLowerCase()
|
|
482
|
+
));
|
|
483
|
+
if (!signingAuthority) {
|
|
484
|
+
throw new TRPCError({
|
|
485
|
+
code: 'FORBIDDEN',
|
|
486
|
+
message: `Signing authority ${config.signingAuthorityName} is not registered for autoboost issuer ${issuer}`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
id: config.id,
|
|
492
|
+
signingAuthorityEndpoint: config.signingAuthorityEndpoint,
|
|
493
|
+
signingAuthorityName: config.signingAuthorityName,
|
|
494
|
+
issuer,
|
|
495
|
+
};
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
const next: StoredContractVersion = {
|
|
499
|
+
...current,
|
|
500
|
+
...Object.fromEntries(
|
|
501
|
+
Object.entries(changes).filter(
|
|
502
|
+
([key, value]) =>
|
|
503
|
+
key !== 'contract' &&
|
|
504
|
+
key !== 'recipients' &&
|
|
505
|
+
key !== 'expiresAt' &&
|
|
506
|
+
value !== undefined
|
|
507
|
+
)
|
|
508
|
+
),
|
|
509
|
+
contract: changes.contract
|
|
510
|
+
? mergeConsentFlowContractPatch(current.contract, changes.contract)
|
|
511
|
+
: current.contract,
|
|
512
|
+
expiresAt:
|
|
513
|
+
changes.expiresAt === null ? undefined : (changes.expiresAt ?? current.expiresAt),
|
|
514
|
+
autoboosts,
|
|
515
|
+
writers: changes.writers ?? current.writers,
|
|
516
|
+
};
|
|
517
|
+
const kind = classifyContractChange(current, next);
|
|
518
|
+
const diff = diffContractChange(current, next);
|
|
519
|
+
if (reason && reason.length > 140) {
|
|
520
|
+
throw new TRPCError({
|
|
521
|
+
code: 'BAD_REQUEST',
|
|
522
|
+
message: 'Reason must be at most 140 characters',
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
if (kind === 'requires_consent' && !reason) {
|
|
526
|
+
throw new TRPCError({
|
|
527
|
+
code: 'BAD_REQUEST',
|
|
528
|
+
message: 'A reason is required when asking users for more access',
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (next.expiresAt && !z.iso.datetime().safeParse(next.expiresAt).success) {
|
|
532
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'expiresAt must be a valid ISO date' });
|
|
533
|
+
}
|
|
534
|
+
if (
|
|
535
|
+
reconsentDeadline &&
|
|
536
|
+
(!z.iso.datetime().safeParse(reconsentDeadline).success ||
|
|
537
|
+
Date.parse(reconsentDeadline) <= Date.now())
|
|
538
|
+
) {
|
|
539
|
+
throw new TRPCError({
|
|
540
|
+
code: 'BAD_REQUEST',
|
|
541
|
+
message: 'reconsentDeadline must be a future ISO date',
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
const effectiveDeadline =
|
|
545
|
+
kind === 'requires_consent'
|
|
546
|
+
? (reconsentDeadline ?? new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString())
|
|
547
|
+
: undefined;
|
|
548
|
+
const preview = buildContractUpdatePreview({ name: next.name, kind, diff });
|
|
549
|
+
const reviewUrl = `${(appBaseUrl ?? domain).replace(/\/+$/, '')}/consent-flow/review?uri=${encodeURIComponent(uri)}`;
|
|
550
|
+
const contractOwnerProfileId = stored.get('ownerProfileId');
|
|
551
|
+
if (typeof contractOwnerProfileId !== 'string') {
|
|
552
|
+
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Contract owner is missing' });
|
|
553
|
+
}
|
|
554
|
+
const currentAutoBoosts = new Map(current.autoboosts.map(config => [config.id, config]));
|
|
555
|
+
const autoBoostsUnchanged =
|
|
556
|
+
autoboosts.length === currentAutoBoosts.size &&
|
|
557
|
+
autoboosts.every(config => {
|
|
558
|
+
const previous = currentAutoBoosts.get(config.id);
|
|
559
|
+
return (
|
|
560
|
+
previous?.signingAuthorityEndpoint === config.signingAuthorityEndpoint &&
|
|
561
|
+
previous.signingAuthorityName === config.signingAuthorityName &&
|
|
562
|
+
(previous.issuer ?? contractOwnerProfileId) === config.issuer
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
if (
|
|
566
|
+
isDeepStrictEqual(current.contract, next.contract) &&
|
|
567
|
+
!diff.details?.length &&
|
|
568
|
+
autoBoostsUnchanged
|
|
569
|
+
) {
|
|
570
|
+
return { version: currentVersion, kind, affectedUsers: 0, preview };
|
|
571
|
+
}
|
|
572
|
+
const affectedUsers = await neogma.queryRunner
|
|
573
|
+
.run(
|
|
574
|
+
`MATCH (profile:Profile)-[:CREATED_BY]->(t:ConsentFlowTerms)-[:CONSENTS_TO]->(c:ConsentFlowContract {id:$id})
|
|
575
|
+
WHERE t.status IN ['live','needs_reconsent','suspended'] RETURN count(DISTINCT profile.profileId) AS count`,
|
|
576
|
+
{ id: contractId }
|
|
577
|
+
)
|
|
578
|
+
.then(r => Number(r.records[0]?.get('count') ?? 0));
|
|
579
|
+
const eventId = `${contractId}:version:${currentVersion + 1}`;
|
|
580
|
+
const notificationData = {
|
|
581
|
+
contractUri: uri,
|
|
582
|
+
contractName: next.name,
|
|
583
|
+
fromVersion: currentVersion,
|
|
584
|
+
toVersion: currentVersion + 1,
|
|
585
|
+
kind,
|
|
586
|
+
...(reason ? { reason } : {}),
|
|
587
|
+
diff,
|
|
588
|
+
...(effectiveDeadline ? { reconsentDeadline: effectiveDeadline } : {}),
|
|
589
|
+
reviewUrl,
|
|
590
|
+
preview,
|
|
591
|
+
};
|
|
592
|
+
const result = { version: currentVersion + 1, kind, affectedUsers, preview };
|
|
593
|
+
if (dryRun) return result;
|
|
594
|
+
const createdAt = new Date().toISOString();
|
|
595
|
+
const versionId = uuid();
|
|
596
|
+
const removedTermPrefixes = diff.removed.map(
|
|
597
|
+
scope =>
|
|
598
|
+
`terms.${scope.area}.${scope.type === 'credential' ? 'credentials.categories' : 'personal'}.${scope.key}`
|
|
599
|
+
);
|
|
600
|
+
const optionalOffValues: { key: string; value: boolean | string | string[] }[] = diff.added
|
|
601
|
+
.filter(scope => !scope.required)
|
|
602
|
+
.flatMap<{ key: string; value: boolean | string | string[] }>(scope => {
|
|
603
|
+
const prefix = `terms.${scope.area}.${scope.type === 'credential' ? 'credentials.categories' : 'personal'}.${scope.key}`;
|
|
604
|
+
if (scope.area === 'write') return [{ key: prefix, value: false }];
|
|
605
|
+
if (scope.type === 'personal') return [];
|
|
606
|
+
return [
|
|
607
|
+
{ key: `${prefix}.sharing`, value: false },
|
|
608
|
+
{ key: `${prefix}.shared`, value: [] as string[] },
|
|
609
|
+
{ key: `${prefix}.shareAll`, value: false },
|
|
610
|
+
];
|
|
611
|
+
});
|
|
612
|
+
const disableReadShareAll =
|
|
613
|
+
kind === 'expanded_optional' &&
|
|
614
|
+
diff.added.some(
|
|
615
|
+
scope => scope.area === 'read' && scope.type === 'credential' && !scope.required
|
|
616
|
+
);
|
|
617
|
+
const preservedReadGrantKeys = Object.keys(next.contract.read.credentials.categories)
|
|
618
|
+
.filter(
|
|
619
|
+
key =>
|
|
620
|
+
Object.prototype.hasOwnProperty.call(
|
|
621
|
+
current.contract.read.credentials.categories,
|
|
622
|
+
key
|
|
623
|
+
) &&
|
|
624
|
+
!diff.added.some(
|
|
625
|
+
scope =>
|
|
626
|
+
scope.area === 'read' && scope.type === 'credential' && scope.key === key
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
.map(key => `terms.read.credentials.categories.${key}.shareAll`);
|
|
630
|
+
const writerIds = [...new Set(next.writers)];
|
|
631
|
+
const contractProperties = flattenObject({
|
|
632
|
+
...Object.fromEntries(
|
|
633
|
+
Object.entries(next).filter(
|
|
634
|
+
([key, value]) =>
|
|
635
|
+
value !== undefined &&
|
|
636
|
+
![
|
|
637
|
+
'id',
|
|
638
|
+
'version',
|
|
639
|
+
'createdAt',
|
|
640
|
+
'reason',
|
|
641
|
+
'kind',
|
|
642
|
+
'contract',
|
|
643
|
+
'autoboosts',
|
|
644
|
+
'writers',
|
|
645
|
+
].includes(key)
|
|
646
|
+
)
|
|
647
|
+
),
|
|
648
|
+
contract: next.contract,
|
|
649
|
+
currentVersion: currentVersion + 1,
|
|
650
|
+
status: 'active',
|
|
651
|
+
updatedAt: createdAt,
|
|
652
|
+
});
|
|
653
|
+
const committed = await neogma.queryRunner.run(
|
|
654
|
+
`MATCH (c:ConsentFlowContract {id:$id})
|
|
655
|
+
SET c._contractVersionLock=coalesce(c._contractVersionLock,0)+1
|
|
656
|
+
WITH c,
|
|
657
|
+
EXISTS { MATCH (c)-[:CREATED_BY]->(:Profile {profileId:$actor}) } AS authorized,
|
|
658
|
+
coalesce(c.currentVersion,1) = $expectedVersion AS currentMatches,
|
|
659
|
+
coalesce(c.status,'active') <> 'retired' AS active
|
|
660
|
+
FOREACH (_ IN CASE WHEN authorized AND currentMatches AND active THEN [] ELSE [1] END |
|
|
661
|
+
REMOVE c._contractVersionLock)
|
|
662
|
+
WITH c WHERE authorized AND currentMatches AND active
|
|
663
|
+
OPTIONAL MATCH (c)-[:CONSENTS_TO]-(t:ConsentFlowTerms)
|
|
664
|
+
WITH c, collect(DISTINCT t) AS terms
|
|
665
|
+
MATCH (c)-[:CREATED_BY]->(owner:Profile)
|
|
666
|
+
OPTIONAL MATCH (c)-[:CONSENTS_TO]-(holderTerms:ConsentFlowTerms)<-[:CREATED_BY]-(holder:Profile)
|
|
667
|
+
WHERE holderTerms.status IN ['live','needs_reconsent','suspended']
|
|
668
|
+
WITH c, terms, owner, collect(DISTINCT holder.profileId) AS holderIds
|
|
669
|
+
OPTIONAL MATCH (c)-[oldAuto:AUTO_RECEIVE]->(:Boost)
|
|
670
|
+
WITH c, terms, owner, holderIds, collect(oldAuto) AS oldAuto
|
|
671
|
+
OPTIONAL MATCH (c)-[oldWriter:CAN_WRITE]->(:Profile)
|
|
672
|
+
WITH c, terms, owner, holderIds, oldAuto, collect(oldWriter) AS oldWriters
|
|
673
|
+
OPTIONAL MATCH (boost:Boost) WHERE boost.id IN $boostIds
|
|
674
|
+
WITH c, terms, owner, holderIds, oldAuto, oldWriters, collect(DISTINCT boost) AS boosts
|
|
675
|
+
FOREACH (_ IN CASE WHEN size(boosts) = size($boostIds) THEN [] ELSE [1] END | REMOVE c._contractVersionLock)
|
|
676
|
+
WITH c, terms, owner, holderIds, oldAuto, oldWriters, boosts
|
|
677
|
+
WHERE size(boosts) = size($boostIds)
|
|
678
|
+
OPTIONAL MATCH (writer:Profile) WHERE writer.profileId IN $writerIds
|
|
679
|
+
WITH c, terms, owner, holderIds, oldAuto, oldWriters, boosts, collect(DISTINCT writer) AS writers
|
|
680
|
+
FOREACH (_ IN CASE WHEN size(writers) = size($writerIds) THEN [] ELSE [1] END | REMOVE c._contractVersionLock)
|
|
681
|
+
WITH c, terms, owner, holderIds, oldAuto, oldWriters, boosts, writers
|
|
682
|
+
WHERE size(writers) = size($writerIds)
|
|
683
|
+
CREATE (v:ConsentFlowContractVersion {id:$versionId, version:$nextVersion, createdAt:$createdAt,
|
|
684
|
+
contract:$contract, autoboosts:$autoboostSnapshot, writers:$writerSnapshot, reason:$reason, kind:$kind,
|
|
685
|
+
name:$name, subtitle:$subtitle, description:$description, reasonForAccessing:$reasonForAccessing,
|
|
686
|
+
image:$image, needsGuardianConsent:$needsGuardianConsent, redirectUrl:$redirectUrl,
|
|
687
|
+
frontDoorBoostUri:$frontDoorBoostUri, expiresAt:$expiresAt})
|
|
688
|
+
CREATE (c)-[:HAS_VERSION]->(v)
|
|
689
|
+
FOREACH (key IN [key IN keys(c)
|
|
690
|
+
WHERE (key STARTS WITH 'contract.' OR key IN ['name','subtitle','description','reasonForAccessing','image','needsGuardianConsent','redirectUrl','frontDoorBoostUri','expiresAt'])
|
|
691
|
+
AND NOT (key IN keys($contractProperties))] | SET c[key]=null)
|
|
692
|
+
SET c.currentVersion=$nextVersion, c.status='active', c += $contractProperties,
|
|
693
|
+
c.latestUpdate=$latestUpdate, c.updatedAt=$createdAt
|
|
694
|
+
FOREACH (relationship IN oldAuto | DELETE relationship)
|
|
695
|
+
FOREACH (relationship IN oldWriters | DELETE relationship)
|
|
696
|
+
FOREACH (config IN $autoBoostConfigs |
|
|
697
|
+
FOREACH (boost IN [target IN boosts WHERE target.id=config.id] |
|
|
698
|
+
CREATE (c)-[:AUTO_RECEIVE {signingAuthorityEndpoint:config.signingAuthorityEndpoint,
|
|
699
|
+
signingAuthorityName:config.signingAuthorityName, issuer:config.issuer}]->(boost)))
|
|
700
|
+
FOREACH (writer IN writers | CREATE (c)-[:CAN_WRITE]->(writer))
|
|
701
|
+
REMOVE c._contractVersionLock
|
|
702
|
+
FOREACH (term IN terms |
|
|
703
|
+
FOREACH (_ IN CASE WHEN term.status IN ['live','needs_reconsent','suspended'] THEN [1] ELSE [] END |
|
|
704
|
+
SET term.lastReviewedVersion=coalesce(term.lastReviewedVersion,term.consentedVersion,1)
|
|
705
|
+
SET term.mutationVersion=coalesce(term.mutationVersion,0)+1
|
|
706
|
+
CREATE (transaction:ConsentFlowTransaction {
|
|
707
|
+
id:randomUUID(), action:'update', date:$createdAt, version:$nextVersion, kind:$kind
|
|
708
|
+
})-[:IS_FOR]->(term))
|
|
709
|
+
FOREACH (_ IN CASE WHEN term.status = 'live' AND $kind <> 'requires_consent' THEN [1] ELSE [] END |
|
|
710
|
+
FOREACH (key IN [key IN keys(term) WHERE any(prefix IN $removedTermPrefixes WHERE key = prefix OR key STARTS WITH prefix + '.')] | SET term[key] = null)
|
|
711
|
+
FOREACH (entry IN $optionalOffValues | SET term[entry.key] = entry.value)
|
|
712
|
+
FOREACH (_ IN CASE WHEN $disableReadShareAll AND coalesce(term['terms.read.credentials.shareAll'],false) THEN [1] ELSE [] END |
|
|
713
|
+
SET term['terms.read.credentials.shareAll']=false
|
|
714
|
+
FOREACH (key IN $preservedReadGrantKeys | SET term[key]=true))
|
|
715
|
+
FOREACH (_ IN CASE WHEN term['guardianApproval.contractUpdatedAt'] = $previousVersionCreatedAt THEN [1] ELSE [] END |
|
|
716
|
+
SET term['guardianApproval.contractUpdatedAt']=$createdAt)
|
|
717
|
+
SET term.consentedVersion=$nextVersion,
|
|
718
|
+
term.newOptionsUntil=CASE WHEN $kind = 'expanded_optional' THEN $optionsUntil ELSE term.newOptionsUntil END,
|
|
719
|
+
term.updatedAt=$createdAt)
|
|
720
|
+
FOREACH (_ IN CASE WHEN $kind = 'requires_consent' AND term.status = 'live' THEN [1] ELSE [] END |
|
|
721
|
+
SET term.status='needs_reconsent', term.reconsentDeadline=coalesce(term.reconsentDeadline,$deadline), term.updatedAt=$createdAt))
|
|
722
|
+
CREATE (event:ConsentFlowEvent {
|
|
723
|
+
id:$eventId, contractId:c.id, fromId:owner.profileId, ownerId:owner.profileId,
|
|
724
|
+
domain:$eventDomain, kind:'contract_updated', payload:$eventPayload,
|
|
725
|
+
cleanupPending:true, createdAt:$createdAt
|
|
726
|
+
})
|
|
727
|
+
FOREACH (holderId IN [id IN holderIds WHERE id <> owner.profileId] |
|
|
728
|
+
CREATE (event)-[:HAS_DELIVERY]->(:ConsentFlowEventDelivery {
|
|
729
|
+
id:event.id + ':' + holderId, toId:holderId, state:'pending',
|
|
730
|
+
nextAttemptAt:$createdAt, eventCreatedAt:$createdAt, attempts:0, role:'target'
|
|
731
|
+
}))
|
|
732
|
+
RETURN c.currentVersion AS version`,
|
|
733
|
+
{
|
|
734
|
+
id: contractId,
|
|
735
|
+
actor: actorProfileId,
|
|
736
|
+
expectedVersion: expectedVersion ?? currentVersion,
|
|
737
|
+
nextVersion: currentVersion + 1,
|
|
738
|
+
versionId,
|
|
739
|
+
createdAt,
|
|
740
|
+
contract: JSON.stringify(next.contract),
|
|
741
|
+
autoboostSnapshot: JSON.stringify(next.autoboosts),
|
|
742
|
+
writerSnapshot: JSON.stringify(next.writers),
|
|
743
|
+
autoBoostConfigs: next.autoboosts,
|
|
744
|
+
boostIds: next.autoboosts.map(config => config.id),
|
|
745
|
+
writerIds,
|
|
746
|
+
reason: reason ?? null,
|
|
747
|
+
kind,
|
|
748
|
+
contractProperties,
|
|
749
|
+
name: next.name,
|
|
750
|
+
subtitle: next.subtitle ?? null,
|
|
751
|
+
description: next.description ?? null,
|
|
752
|
+
reasonForAccessing: next.reasonForAccessing ?? null,
|
|
753
|
+
image: next.image ?? null,
|
|
754
|
+
needsGuardianConsent: next.needsGuardianConsent ?? false,
|
|
755
|
+
redirectUrl: next.redirectUrl ?? null,
|
|
756
|
+
frontDoorBoostUri: next.frontDoorBoostUri ?? null,
|
|
757
|
+
expiresAt: next.expiresAt ?? null,
|
|
758
|
+
latestUpdate: JSON.stringify({
|
|
759
|
+
fromVersion: currentVersion,
|
|
760
|
+
toVersion: currentVersion + 1,
|
|
761
|
+
kind,
|
|
762
|
+
reason,
|
|
763
|
+
diff,
|
|
764
|
+
reconsentDeadline: effectiveDeadline,
|
|
765
|
+
createdAt,
|
|
766
|
+
preview,
|
|
767
|
+
}),
|
|
768
|
+
deadline: effectiveDeadline ?? null,
|
|
769
|
+
optionsUntil:
|
|
770
|
+
kind === 'expanded_optional'
|
|
771
|
+
? new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString()
|
|
772
|
+
: null,
|
|
773
|
+
removedTermPrefixes,
|
|
774
|
+
optionalOffValues,
|
|
775
|
+
disableReadShareAll,
|
|
776
|
+
preservedReadGrantKeys,
|
|
777
|
+
previousVersionCreatedAt: current.createdAt,
|
|
778
|
+
eventId,
|
|
779
|
+
eventDomain: domain,
|
|
780
|
+
eventPayload: JSON.stringify(notificationData),
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
if (!committed.records.length)
|
|
784
|
+
throw new TRPCError({
|
|
785
|
+
code: 'CONFLICT',
|
|
786
|
+
message: 'Contract changed concurrently; retry the update',
|
|
787
|
+
});
|
|
788
|
+
await tryDispatchContractEvent(eventId);
|
|
789
|
+
return result;
|
|
790
|
+
};
|