@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
package/src/routes/contracts.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { z } from 'zod';
|
|
|
19
19
|
import { t, profileRoute, openRoute, guardianGatedRoute } from '@routes';
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
+
CONTRACT_APPROVAL_REQUIRED,
|
|
22
23
|
ConsentFlowContractValidator,
|
|
23
24
|
ConsentFlowContractDetailsValidator,
|
|
24
25
|
ConsentFlowContractQueryValidator,
|
|
@@ -38,16 +39,26 @@ import {
|
|
|
38
39
|
JWEValidator,
|
|
39
40
|
UnsignedVC,
|
|
40
41
|
AutoBoostConfigValidator,
|
|
42
|
+
ConsentFlowContractChangesValidator,
|
|
43
|
+
ConsentFlowContractUpdateResultValidator,
|
|
44
|
+
ConsentFlowContractVersionValidator,
|
|
45
|
+
ConsentFlowConsentStatusValidator,
|
|
41
46
|
LCNNotificationTypeEnumValidator,
|
|
42
47
|
LCNProfileValidator,
|
|
43
48
|
} from '@learncard/types';
|
|
44
49
|
import { isVC2Format } from '@learncard/helpers';
|
|
45
50
|
import { injectObv3AlignmentsIntoCredentialForBoost } from '@services/skills-provider/inject';
|
|
51
|
+
import {
|
|
52
|
+
getConsentFlowContractVersion,
|
|
53
|
+
getConsentFlowContractVersions,
|
|
54
|
+
updateConsentFlowContractVersion,
|
|
55
|
+
} from '@accesslayer/consentflowcontract/versions';
|
|
46
56
|
import { createConsentFlowContract } from '@accesslayer/consentflowcontract/create';
|
|
47
57
|
import {
|
|
48
58
|
getAutoBoostsForContract,
|
|
49
59
|
getConsentFlowContractsForProfile,
|
|
50
60
|
getConsentedContractsForProfile,
|
|
61
|
+
encodeConsentedDataCursor,
|
|
51
62
|
getConsentedDataBetweenProfiles,
|
|
52
63
|
getConsentedDataForContract,
|
|
53
64
|
getConsentedDataForProfile,
|
|
@@ -77,6 +88,7 @@ import { removeRequestedForRelationship } from '@accesslayer/consentflowcontract
|
|
|
77
88
|
import { areTermsValid } from '@helpers/contract.helpers';
|
|
78
89
|
import { updateDidForProfile, getProfileIdFromString } from '@helpers/did.helpers';
|
|
79
90
|
import {
|
|
91
|
+
reconsentTerms,
|
|
80
92
|
syncCredentialsToContract,
|
|
81
93
|
updateTerms,
|
|
82
94
|
pruneDeletedUrisFromConsentTerms,
|
|
@@ -106,10 +118,6 @@ import {
|
|
|
106
118
|
sanitizeProfileForTier,
|
|
107
119
|
stripSensitiveProfileListFields,
|
|
108
120
|
} from '@helpers/profile-privacy.helpers';
|
|
109
|
-
import {
|
|
110
|
-
addAutoBoostsToContractDb,
|
|
111
|
-
removeAutoBoostsFromContractDb,
|
|
112
|
-
} from '@accesslayer/consentflowcontract/relationships/manageAutoboosts';
|
|
113
121
|
import { addNotificationToQueue } from '@helpers/notifications.helpers';
|
|
114
122
|
import { getNotificationMessage } from '@helpers/notificationMessages';
|
|
115
123
|
import { resolveRecipientLocale } from '@helpers/getRecipientLocale.helpers';
|
|
@@ -133,6 +141,29 @@ const resolveRecipientProfileId = async (identifier: string, domain: string): Pr
|
|
|
133
141
|
return profileId;
|
|
134
142
|
};
|
|
135
143
|
|
|
144
|
+
const LATEST_CONTRACT_APPROVAL_REQUIRED =
|
|
145
|
+
'User has not yet approved the latest version of this contract';
|
|
146
|
+
|
|
147
|
+
const assertLatestContractApproval = (
|
|
148
|
+
terms: { status?: string; consentedVersion?: number } | null | undefined,
|
|
149
|
+
currentVersion: number
|
|
150
|
+
) => {
|
|
151
|
+
if (!terms) return;
|
|
152
|
+
if (
|
|
153
|
+
terms.status === 'needs_reconsent' ||
|
|
154
|
+
terms.status === 'suspended' ||
|
|
155
|
+
(terms.status === 'live' &&
|
|
156
|
+
terms.consentedVersion !== undefined &&
|
|
157
|
+
terms.consentedVersion < currentVersion)
|
|
158
|
+
) {
|
|
159
|
+
throw new TRPCError({
|
|
160
|
+
code: 'FORBIDDEN',
|
|
161
|
+
message: LATEST_CONTRACT_APPROVAL_REQUIRED,
|
|
162
|
+
cause: { code: CONTRACT_APPROVAL_REQUIRED },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
136
167
|
export const contractsRouter = t.router({
|
|
137
168
|
createConsentFlowContract: profileRoute
|
|
138
169
|
.meta({
|
|
@@ -360,7 +391,7 @@ export const contractsRouter = t.router({
|
|
|
360
391
|
method: 'GET',
|
|
361
392
|
path: '/consent-flow-contract',
|
|
362
393
|
tags: ['Contracts'],
|
|
363
|
-
summary: 'Get Consent Flow
|
|
394
|
+
summary: 'Get Consent Flow Contract',
|
|
364
395
|
description: 'Gets Consent Flow Contract Details',
|
|
365
396
|
},
|
|
366
397
|
requiredScope: 'contracts:read',
|
|
@@ -368,9 +399,7 @@ export const contractsRouter = t.router({
|
|
|
368
399
|
.input(z.object({ uri: z.string() }))
|
|
369
400
|
.output(ConsentFlowContractDetailsValidator)
|
|
370
401
|
.query(async ({ input, ctx }) => {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const decodedUri = decodeURIComponent(uri);
|
|
402
|
+
const decodedUri = decodeURIComponent(input.uri);
|
|
374
403
|
const result = await getContractDetailsByUri(decodedUri);
|
|
375
404
|
|
|
376
405
|
if (!result) {
|
|
@@ -400,6 +429,141 @@ export const contractsRouter = t.router({
|
|
|
400
429
|
publicContractRecipient(ctx.domain, recipient)
|
|
401
430
|
),
|
|
402
431
|
audienceVersion: Number(result.contract.audienceVersion ?? 0),
|
|
432
|
+
version: Number(result.contract.currentVersion ?? 1),
|
|
433
|
+
status: result.contract.status === 'retired' ? 'retired' : 'active',
|
|
434
|
+
latestUpdate: result.contract.latestUpdate,
|
|
435
|
+
};
|
|
436
|
+
}),
|
|
437
|
+
|
|
438
|
+
updateConsentFlowContract: profileRoute
|
|
439
|
+
.meta({
|
|
440
|
+
requiredScope: 'contracts:write',
|
|
441
|
+
openapi: {
|
|
442
|
+
protect: true,
|
|
443
|
+
method: 'POST',
|
|
444
|
+
path: '/consent-flow-contract/update',
|
|
445
|
+
tags: ['Contracts'],
|
|
446
|
+
summary: 'Update Consent Flow Contract',
|
|
447
|
+
description: 'Creates a versioned update to an owned consent flow contract',
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
.input(
|
|
451
|
+
z.object({
|
|
452
|
+
uri: z.string(),
|
|
453
|
+
changes: ConsentFlowContractChangesValidator,
|
|
454
|
+
reason: z.string().max(140).optional(),
|
|
455
|
+
reconsentDeadline: z.string().datetime().optional(),
|
|
456
|
+
dryRun: z.boolean().optional(),
|
|
457
|
+
expectedVersion: z.number().int().positive().optional(),
|
|
458
|
+
})
|
|
459
|
+
)
|
|
460
|
+
.output(ConsentFlowContractUpdateResultValidator)
|
|
461
|
+
.mutation(async ({ ctx, input }) => {
|
|
462
|
+
const contract = await getContractByUri(decodeURIComponent(input.uri));
|
|
463
|
+
if (!contract)
|
|
464
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
|
|
465
|
+
if (!(await isProfileConsentFlowContractAdmin(ctx.user.profile, contract))) {
|
|
466
|
+
throw new TRPCError({
|
|
467
|
+
code: 'UNAUTHORIZED',
|
|
468
|
+
message: 'Profile does not own contract',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
const engineChanges = {
|
|
472
|
+
...input.changes,
|
|
473
|
+
autoboosts: undefined as
|
|
474
|
+
| {
|
|
475
|
+
id: string;
|
|
476
|
+
signingAuthorityEndpoint: string;
|
|
477
|
+
signingAuthorityName: string;
|
|
478
|
+
}[]
|
|
479
|
+
| undefined,
|
|
480
|
+
};
|
|
481
|
+
if (input.changes.autoboosts) {
|
|
482
|
+
engineChanges.autoboosts = await Promise.all(
|
|
483
|
+
input.changes.autoboosts.map(async config => {
|
|
484
|
+
const boost = await getBoostByUri(config.boostUri);
|
|
485
|
+
if (!boost) {
|
|
486
|
+
throw new TRPCError({
|
|
487
|
+
code: 'NOT_FOUND',
|
|
488
|
+
message: `Could not find boost: ${config.boostUri}`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (isDraftBoost(boost)) {
|
|
492
|
+
throw new TRPCError({
|
|
493
|
+
code: 'FORBIDDEN',
|
|
494
|
+
message:
|
|
495
|
+
'Draft Boosts cannot be used for automatic credential delivery',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const name = config.signingAuthority.name.toLowerCase();
|
|
499
|
+
return {
|
|
500
|
+
id: boost.id,
|
|
501
|
+
signingAuthorityEndpoint: config.signingAuthority.endpoint,
|
|
502
|
+
signingAuthorityName: name,
|
|
503
|
+
};
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const appHost =
|
|
508
|
+
environment.CLIENT_APP_DOMAIN_NAME ??
|
|
509
|
+
`localhost:${environment.CLIENT_APP_PORT || 3000}`;
|
|
510
|
+
const scheme = environment.IS_OFFLINE ? 'http' : 'https';
|
|
511
|
+
const appBaseUrl =
|
|
512
|
+
ctx.tenant?.emailBranding?.appUrl?.replace(/\/+$/, '') ?? `${scheme}://${appHost}`;
|
|
513
|
+
return updateConsentFlowContractVersion(
|
|
514
|
+
{ ...input, changes: engineChanges },
|
|
515
|
+
ctx.user.profile.profileId,
|
|
516
|
+
ctx.domain,
|
|
517
|
+
appBaseUrl
|
|
518
|
+
);
|
|
519
|
+
}),
|
|
520
|
+
|
|
521
|
+
getContractVersions: openRoute
|
|
522
|
+
.meta({ requiredScope: 'contracts:read' })
|
|
523
|
+
.input(z.object({ uri: z.string() }))
|
|
524
|
+
.output(z.array(ConsentFlowContractVersionValidator))
|
|
525
|
+
.query(async ({ input }) => getConsentFlowContractVersions(input.uri)),
|
|
526
|
+
|
|
527
|
+
getConsentStatus: profileRoute
|
|
528
|
+
.meta({ requiredScope: 'contracts:read' })
|
|
529
|
+
.input(z.object({ uri: z.string(), profileId: z.string() }))
|
|
530
|
+
.output(ConsentFlowConsentStatusValidator)
|
|
531
|
+
.query(async ({ input, ctx }) => {
|
|
532
|
+
const profileId = await getProfileIdFromString(input.profileId, ctx.domain);
|
|
533
|
+
if (!profileId)
|
|
534
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found' });
|
|
535
|
+
const contract = await getContractByUri(decodeURIComponent(input.uri));
|
|
536
|
+
if (!contract)
|
|
537
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
|
|
538
|
+
const isOwner = await isProfileConsentFlowContractAdmin(ctx.user.profile, contract);
|
|
539
|
+
if (profileId !== ctx.user.profile.profileId && !isOwner) {
|
|
540
|
+
throw new TRPCError({
|
|
541
|
+
code: 'UNAUTHORIZED',
|
|
542
|
+
message: 'Profile cannot inspect this consent',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const terms = await getContractTermsForProfile(profileId, contract);
|
|
546
|
+
const currentVersion = Number(contract.currentVersion ?? 1);
|
|
547
|
+
const needsReconsent =
|
|
548
|
+
terms?.status === 'needs_reconsent' ||
|
|
549
|
+
terms?.status === 'suspended' ||
|
|
550
|
+
(terms?.consentedVersion !== undefined && terms.consentedVersion < currentVersion);
|
|
551
|
+
const appHost =
|
|
552
|
+
environment.CLIENT_APP_DOMAIN_NAME ??
|
|
553
|
+
`localhost:${environment.CLIENT_APP_PORT || 3000}`;
|
|
554
|
+
const scheme = environment.IS_OFFLINE ? 'http' : 'https';
|
|
555
|
+
const appBaseUrl =
|
|
556
|
+
ctx.tenant?.emailBranding?.appUrl?.replace(/\/+$/, '') ?? `${scheme}://${appHost}`;
|
|
557
|
+
const reviewUrl =
|
|
558
|
+
terms && needsReconsent
|
|
559
|
+
? `${appBaseUrl}/consent-flow/review?uri=${encodeURIComponent(input.uri)}`
|
|
560
|
+
: undefined;
|
|
561
|
+
return {
|
|
562
|
+
status: terms?.status ?? 'not_consented',
|
|
563
|
+
...(terms?.consentedVersion ? { consentedVersion: terms.consentedVersion } : {}),
|
|
564
|
+
currentVersion,
|
|
565
|
+
needsReconsent,
|
|
566
|
+
...(reviewUrl ? { reviewUrl } : {}),
|
|
403
567
|
};
|
|
404
568
|
}),
|
|
405
569
|
|
|
@@ -456,6 +620,9 @@ export const contractsRouter = t.router({
|
|
|
456
620
|
uri: constructUri('contract', contract.id, ctx.domain),
|
|
457
621
|
...(contract.expiresAt ? { expiresAt: contract.expiresAt } : {}),
|
|
458
622
|
autoBoosts,
|
|
623
|
+
version: Number(contract.currentVersion ?? 1),
|
|
624
|
+
status: contract.status === 'retired' ? 'retired' : 'active',
|
|
625
|
+
latestUpdate: contract.latestUpdate,
|
|
459
626
|
})),
|
|
460
627
|
};
|
|
461
628
|
}),
|
|
@@ -492,7 +659,16 @@ export const contractsRouter = t.router({
|
|
|
492
659
|
message: 'Profile does not own contract',
|
|
493
660
|
});
|
|
494
661
|
}
|
|
495
|
-
|
|
662
|
+
const appHost =
|
|
663
|
+
environment.CLIENT_APP_DOMAIN_NAME ??
|
|
664
|
+
`localhost:${environment.CLIENT_APP_PORT || 3000}`;
|
|
665
|
+
const scheme = environment.IS_OFFLINE ? 'http' : 'https';
|
|
666
|
+
const appBaseUrl =
|
|
667
|
+
ctx.tenant?.emailBranding?.appUrl?.replace(/\/+$/, '') ?? `${scheme}://${appHost}`;
|
|
668
|
+
await Promise.all([
|
|
669
|
+
deleteConsentFlowContract(contract, ctx.domain, appBaseUrl),
|
|
670
|
+
deleteStorageForUri(uri),
|
|
671
|
+
]);
|
|
496
672
|
|
|
497
673
|
return true;
|
|
498
674
|
}),
|
|
@@ -538,21 +714,16 @@ export const contractsRouter = t.router({
|
|
|
538
714
|
|
|
539
715
|
const contractId = getIdFromUri(uri);
|
|
540
716
|
|
|
541
|
-
const
|
|
717
|
+
const page = await getConsentedDataForContract(contractId, profile.profileId, {
|
|
542
718
|
query,
|
|
543
|
-
limit
|
|
719
|
+
limit,
|
|
544
720
|
cursor,
|
|
545
721
|
});
|
|
546
722
|
|
|
547
|
-
const data = results.slice(0, limit);
|
|
548
|
-
|
|
549
|
-
const hasMore = results.length > limit;
|
|
550
|
-
const nextCursor = data.at(-1)?.date;
|
|
551
|
-
|
|
552
723
|
return {
|
|
553
|
-
hasMore,
|
|
554
|
-
cursor: nextCursor,
|
|
555
|
-
records:
|
|
724
|
+
hasMore: page.hasMore,
|
|
725
|
+
cursor: page.nextCursor,
|
|
726
|
+
records: page.records,
|
|
556
727
|
};
|
|
557
728
|
}),
|
|
558
729
|
|
|
@@ -594,17 +765,14 @@ export const contractsRouter = t.router({
|
|
|
594
765
|
const results = await getConsentedDataBetweenProfiles(
|
|
595
766
|
profile.profileId,
|
|
596
767
|
otherProfile.profileId,
|
|
597
|
-
{ query, limit
|
|
768
|
+
{ query, limit, cursor, domain: ctx.domain }
|
|
598
769
|
);
|
|
599
770
|
|
|
600
|
-
const data = results.
|
|
601
|
-
|
|
602
|
-
const hasMore = results.length > limit;
|
|
603
|
-
const nextCursor = data.at(-1)?.date;
|
|
771
|
+
const data = results.records;
|
|
604
772
|
|
|
605
773
|
return {
|
|
606
|
-
hasMore,
|
|
607
|
-
cursor: nextCursor,
|
|
774
|
+
hasMore: results.hasMore,
|
|
775
|
+
cursor: results.nextCursor,
|
|
608
776
|
records: data,
|
|
609
777
|
};
|
|
610
778
|
}),
|
|
@@ -634,21 +802,16 @@ export const contractsRouter = t.router({
|
|
|
634
802
|
|
|
635
803
|
const { query, limit, cursor } = input;
|
|
636
804
|
|
|
637
|
-
const
|
|
805
|
+
const page = await getConsentedDataForProfile(profile.profileId, {
|
|
638
806
|
query,
|
|
639
|
-
limit
|
|
807
|
+
limit,
|
|
640
808
|
cursor,
|
|
641
809
|
});
|
|
642
810
|
|
|
643
|
-
const data = results.slice(0, limit);
|
|
644
|
-
|
|
645
|
-
const hasMore = results.length > limit;
|
|
646
|
-
const nextCursor = data.at(-1)?.date;
|
|
647
|
-
|
|
648
811
|
return {
|
|
649
|
-
hasMore,
|
|
650
|
-
cursor: nextCursor,
|
|
651
|
-
records:
|
|
812
|
+
hasMore: page.hasMore,
|
|
813
|
+
cursor: page.nextCursor,
|
|
814
|
+
records: page.records,
|
|
652
815
|
};
|
|
653
816
|
}),
|
|
654
817
|
|
|
@@ -706,9 +869,22 @@ export const contractsRouter = t.router({
|
|
|
706
869
|
message: 'Could not find contract',
|
|
707
870
|
});
|
|
708
871
|
}
|
|
872
|
+
if (contractDetails.contract.status === 'retired') {
|
|
873
|
+
throw new TRPCError({
|
|
874
|
+
code: 'FORBIDDEN',
|
|
875
|
+
message: 'This contract is no longer active',
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
|
|
879
|
+
assertLatestContractApproval(
|
|
880
|
+
terms,
|
|
881
|
+
Number(contractDetails.contract.currentVersion ?? 1)
|
|
882
|
+
);
|
|
709
883
|
|
|
710
|
-
|
|
711
|
-
|
|
884
|
+
if (
|
|
885
|
+
!(await hasProfileConsentedToContract(otherProfile, contractDetails.contract)) ||
|
|
886
|
+
!terms
|
|
887
|
+
) {
|
|
712
888
|
throw new TRPCError({
|
|
713
889
|
code: 'FORBIDDEN',
|
|
714
890
|
message: 'Target profile has not consented to this contract',
|
|
@@ -740,13 +916,6 @@ export const contractsRouter = t.router({
|
|
|
740
916
|
}
|
|
741
917
|
|
|
742
918
|
// Check if this boost category is allowed in the contract's terms
|
|
743
|
-
const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
|
|
744
|
-
if (!terms) {
|
|
745
|
-
throw new TRPCError({
|
|
746
|
-
code: 'FORBIDDEN',
|
|
747
|
-
message: 'Target profile has not consented to this contract',
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
919
|
|
|
751
920
|
// Check if the boost category is included in the contract terms
|
|
752
921
|
const boostCategory = boost.category;
|
|
@@ -849,9 +1018,23 @@ export const contractsRouter = t.router({
|
|
|
849
1018
|
if (!contractDetails) {
|
|
850
1019
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
|
|
851
1020
|
}
|
|
1021
|
+
if (contractDetails.contract.status === 'retired') {
|
|
1022
|
+
throw new TRPCError({
|
|
1023
|
+
code: 'FORBIDDEN',
|
|
1024
|
+
message: 'This contract is no longer active',
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
|
|
1028
|
+
assertLatestContractApproval(
|
|
1029
|
+
terms,
|
|
1030
|
+
Number(contractDetails.contract.currentVersion ?? 1)
|
|
1031
|
+
);
|
|
852
1032
|
|
|
853
1033
|
// Verify consent
|
|
854
|
-
if (
|
|
1034
|
+
if (
|
|
1035
|
+
!(await hasProfileConsentedToContract(otherProfile, contractDetails.contract)) ||
|
|
1036
|
+
!terms
|
|
1037
|
+
) {
|
|
855
1038
|
throw new TRPCError({
|
|
856
1039
|
code: 'FORBIDDEN',
|
|
857
1040
|
message: 'Target profile has not consented to this contract',
|
|
@@ -874,15 +1057,6 @@ export const contractsRouter = t.router({
|
|
|
874
1057
|
});
|
|
875
1058
|
}
|
|
876
1059
|
|
|
877
|
-
// Get contract terms
|
|
878
|
-
const terms = await getContractTermsForProfile(otherProfile, contractDetails.contract);
|
|
879
|
-
if (!terms) {
|
|
880
|
-
throw new TRPCError({
|
|
881
|
-
code: 'FORBIDDEN',
|
|
882
|
-
message: 'Target profile has not consented to this contract',
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
|
|
886
1060
|
// Check category allowed
|
|
887
1061
|
const boostCategory = boost.category;
|
|
888
1062
|
if (boostCategory) {
|
|
@@ -1027,6 +1201,12 @@ export const contractsRouter = t.router({
|
|
|
1027
1201
|
if (!contractDetails) {
|
|
1028
1202
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
|
|
1029
1203
|
}
|
|
1204
|
+
if (contractDetails.contract.status === 'retired') {
|
|
1205
|
+
throw new TRPCError({
|
|
1206
|
+
code: 'FORBIDDEN',
|
|
1207
|
+
message: 'This contract is no longer active',
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1030
1210
|
|
|
1031
1211
|
if (
|
|
1032
1212
|
contractDetails.contract.expiresAt?.trim() &&
|
|
@@ -1049,11 +1229,15 @@ export const contractsRouter = t.router({
|
|
|
1049
1229
|
profile,
|
|
1050
1230
|
contractDetails.contract
|
|
1051
1231
|
);
|
|
1232
|
+
assertLatestContractApproval(
|
|
1233
|
+
previousTerms,
|
|
1234
|
+
Number(contractDetails.contract.currentVersion ?? 1)
|
|
1235
|
+
);
|
|
1052
1236
|
const retrySmartResume =
|
|
1053
1237
|
isSmartResume &&
|
|
1054
1238
|
Boolean(previousTerms?.smartResumeFingerprint) &&
|
|
1055
1239
|
(previousTerms?.status === 'live' ||
|
|
1056
|
-
(previousTerms?.status === '
|
|
1240
|
+
(previousTerms?.status === 'consumed' && previousTerms.oneTime));
|
|
1057
1241
|
|
|
1058
1242
|
if (
|
|
1059
1243
|
!retrySmartResume &&
|
|
@@ -1107,7 +1291,7 @@ export const contractsRouter = t.router({
|
|
|
1107
1291
|
// Identical submissions still replay the completed upload instead.
|
|
1108
1292
|
const newDecisionAfterOneTime =
|
|
1109
1293
|
retrySmartResume &&
|
|
1110
|
-
previousTerms?.status === '
|
|
1294
|
+
previousTerms?.status === 'consumed' &&
|
|
1111
1295
|
previousTerms.oneTime &&
|
|
1112
1296
|
previousTerms.smartResumePublicationStatus === 'succeeded' &&
|
|
1113
1297
|
fingerprint !== previousTerms.smartResumeFingerprint;
|
|
@@ -1194,12 +1378,16 @@ export const contractsRouter = t.router({
|
|
|
1194
1378
|
|
|
1195
1379
|
const contracts = results.slice(0, limit);
|
|
1196
1380
|
|
|
1197
|
-
const hasMore = results.length > limit;
|
|
1198
|
-
const nextCursor = contracts.at(-1)?.terms.updatedAt;
|
|
1199
|
-
|
|
1200
1381
|
return {
|
|
1201
|
-
hasMore,
|
|
1202
|
-
cursor:
|
|
1382
|
+
hasMore: results.length > limit,
|
|
1383
|
+
cursor: contracts.length
|
|
1384
|
+
? encodeConsentedDataCursor(
|
|
1385
|
+
contracts.at(-1)!.terms.updatedAt ??
|
|
1386
|
+
contracts.at(-1)!.terms.createdAt ??
|
|
1387
|
+
'',
|
|
1388
|
+
contracts.at(-1)!.terms.id
|
|
1389
|
+
)
|
|
1390
|
+
: undefined,
|
|
1203
1391
|
records: contracts.map(record => ({
|
|
1204
1392
|
contract: {
|
|
1205
1393
|
contract: record.contract.contract,
|
|
@@ -1220,6 +1408,9 @@ export const contractsRouter = t.router({
|
|
|
1220
1408
|
: {}),
|
|
1221
1409
|
autoBoosts: record.autoBoosts,
|
|
1222
1410
|
audienceVersion: Number(record.contract.audienceVersion ?? 0),
|
|
1411
|
+
version: Number(record.contract.currentVersion ?? 1),
|
|
1412
|
+
status: record.contract.status === 'retired' ? 'retired' : 'active',
|
|
1413
|
+
latestUpdate: record.contract.latestUpdate,
|
|
1223
1414
|
recipients: record.recipients.map(recipient =>
|
|
1224
1415
|
publicContractRecipient(ctx.domain, recipient)
|
|
1225
1416
|
),
|
|
@@ -1231,6 +1422,19 @@ export const contractsRouter = t.router({
|
|
|
1231
1422
|
...(record.terms.oneTime ? { oneTime: record.terms.oneTime } : {}),
|
|
1232
1423
|
consenter: updateDidForProfile(ctx.domain, profile),
|
|
1233
1424
|
status: record.terms.status,
|
|
1425
|
+
consentedVersion: Number(record.terms.consentedVersion ?? 1),
|
|
1426
|
+
lastReviewedVersion: Number(
|
|
1427
|
+
record.terms.lastReviewedVersion ?? record.terms.consentedVersion ?? 1
|
|
1428
|
+
),
|
|
1429
|
+
...(record.cumulativeUpdate
|
|
1430
|
+
? { cumulativeUpdate: record.cumulativeUpdate }
|
|
1431
|
+
: {}),
|
|
1432
|
+
...(record.terms.reconsentDeadline
|
|
1433
|
+
? { reconsentDeadline: record.terms.reconsentDeadline }
|
|
1434
|
+
: {}),
|
|
1435
|
+
...(record.terms.newOptionsUntil
|
|
1436
|
+
? { newOptionsUntil: record.terms.newOptionsUntil }
|
|
1437
|
+
: {}),
|
|
1234
1438
|
})),
|
|
1235
1439
|
};
|
|
1236
1440
|
}),
|
|
@@ -1251,6 +1455,7 @@ export const contractsRouter = t.router({
|
|
|
1251
1455
|
z.object({
|
|
1252
1456
|
uri: z.string(),
|
|
1253
1457
|
terms: ConsentFlowTermsValidator,
|
|
1458
|
+
acceptVersion: z.number().int().positive().optional(),
|
|
1254
1459
|
expiresAt: z.string().optional(),
|
|
1255
1460
|
oneTime: z.boolean().optional(),
|
|
1256
1461
|
audienceVersion: z.number().int().nonnegative().optional(),
|
|
@@ -1269,7 +1474,7 @@ export const contractsRouter = t.router({
|
|
|
1269
1474
|
});
|
|
1270
1475
|
}
|
|
1271
1476
|
|
|
1272
|
-
const { uri, terms, expiresAt, oneTime, audienceVersion } = input;
|
|
1477
|
+
const { uri, terms, acceptVersion, expiresAt, oneTime, audienceVersion } = input;
|
|
1273
1478
|
|
|
1274
1479
|
const decodedUri = decodeURIComponent(uri);
|
|
1275
1480
|
const relationship = await getContractTermsByUri(decodedUri);
|
|
@@ -1287,6 +1492,12 @@ export const contractsRouter = t.router({
|
|
|
1287
1492
|
message: 'Profile does not own terms',
|
|
1288
1493
|
});
|
|
1289
1494
|
}
|
|
1495
|
+
if (relationship.contract.status === 'retired') {
|
|
1496
|
+
throw new TRPCError({
|
|
1497
|
+
code: 'FORBIDDEN',
|
|
1498
|
+
message: 'This contract is no longer active',
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1290
1501
|
|
|
1291
1502
|
if (!guardianIdentity && (await hasGuardianApprovalHistory(relationship.terms))) {
|
|
1292
1503
|
throw new TRPCError({
|
|
@@ -1295,14 +1506,32 @@ export const contractsRouter = t.router({
|
|
|
1295
1506
|
});
|
|
1296
1507
|
}
|
|
1297
1508
|
|
|
1298
|
-
if (!
|
|
1509
|
+
if (!acceptVersion) {
|
|
1510
|
+
assertLatestContractApproval(
|
|
1511
|
+
relationship.terms,
|
|
1512
|
+
Number(relationship.contract.currentVersion ?? 1)
|
|
1513
|
+
);
|
|
1514
|
+
if (relationship.terms.status !== 'live') {
|
|
1515
|
+
throw new TRPCError({
|
|
1516
|
+
code: 'FORBIDDEN',
|
|
1517
|
+
message: 'Only live contract terms can be updated',
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
const acceptedContractVersion = await getConsentFlowContractVersion(
|
|
1522
|
+
relationship.contract.id,
|
|
1523
|
+
acceptVersion ?? Number(relationship.contract.currentVersion ?? 1)
|
|
1524
|
+
);
|
|
1525
|
+
if (!acceptedContractVersion) {
|
|
1526
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Unknown contract version' });
|
|
1527
|
+
}
|
|
1528
|
+
if (!areTermsValid(terms, acceptedContractVersion.contract)) {
|
|
1299
1529
|
throw new TRPCError({
|
|
1300
1530
|
code: 'BAD_REQUEST',
|
|
1301
1531
|
message: 'New Terms are invalid for Contract',
|
|
1302
1532
|
});
|
|
1303
1533
|
}
|
|
1304
1534
|
|
|
1305
|
-
// Convert deniedWriters DIDs to profile IDs if needed
|
|
1306
1535
|
if (terms.deniedWriters?.length) {
|
|
1307
1536
|
terms.deniedWriters = await resolveAndValidateDeniedWriters(
|
|
1308
1537
|
relationship.contract,
|
|
@@ -1311,27 +1540,28 @@ export const contractsRouter = t.router({
|
|
|
1311
1540
|
);
|
|
1312
1541
|
}
|
|
1313
1542
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1543
|
+
const changes = {
|
|
1544
|
+
terms,
|
|
1545
|
+
expiresAt,
|
|
1546
|
+
oneTime,
|
|
1547
|
+
audienceVersion,
|
|
1548
|
+
...(guardianIdentity
|
|
1549
|
+
? {
|
|
1550
|
+
guardianApproval: {
|
|
1551
|
+
guardianProfileId: guardianIdentity.profileId,
|
|
1552
|
+
guardianDid: guardianIdentity.did,
|
|
1553
|
+
approvedAt: new Date().toISOString(),
|
|
1554
|
+
contractUpdatedAt: relationship.contract.updatedAt,
|
|
1555
|
+
},
|
|
1556
|
+
}
|
|
1557
|
+
: {}),
|
|
1558
|
+
};
|
|
1559
|
+
if (acceptVersion) {
|
|
1560
|
+
await reconsentTerms(relationship, { ...changes, acceptVersion }, ctx.domain);
|
|
1561
|
+
} else {
|
|
1562
|
+
await updateTerms(relationship, changes, ctx.domain);
|
|
1563
|
+
}
|
|
1564
|
+
await deleteStorageForUri(uri);
|
|
1335
1565
|
|
|
1336
1566
|
return true;
|
|
1337
1567
|
}),
|
|
@@ -1386,7 +1616,15 @@ export const contractsRouter = t.router({
|
|
|
1386
1616
|
const contracts = results.slice(0, pageSize);
|
|
1387
1617
|
allContracts.push(...contracts);
|
|
1388
1618
|
|
|
1389
|
-
cursor =
|
|
1619
|
+
cursor =
|
|
1620
|
+
results.length > pageSize && contracts.length
|
|
1621
|
+
? encodeConsentedDataCursor(
|
|
1622
|
+
contracts.at(-1)!.terms.updatedAt ??
|
|
1623
|
+
contracts.at(-1)!.terms.createdAt ??
|
|
1624
|
+
'',
|
|
1625
|
+
contracts.at(-1)!.terms.id
|
|
1626
|
+
)
|
|
1627
|
+
: undefined;
|
|
1390
1628
|
} while (cursor);
|
|
1391
1629
|
|
|
1392
1630
|
for (const contract of allContracts) {
|
|
@@ -1502,8 +1740,10 @@ export const contractsRouter = t.router({
|
|
|
1502
1740
|
const transactions = results.slice(0, limit);
|
|
1503
1741
|
|
|
1504
1742
|
const hasMore = results.length > limit;
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1743
|
+
const lastTransaction = transactions.at(-1);
|
|
1744
|
+
const nextCursor = lastTransaction
|
|
1745
|
+
? encodeConsentedDataCursor(lastTransaction.date, lastTransaction.id)
|
|
1746
|
+
: undefined;
|
|
1507
1747
|
return {
|
|
1508
1748
|
hasMore,
|
|
1509
1749
|
cursor: nextCursor,
|
|
@@ -1547,6 +1787,7 @@ export const contractsRouter = t.router({
|
|
|
1547
1787
|
if (!contract) {
|
|
1548
1788
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Could not find contract' });
|
|
1549
1789
|
}
|
|
1790
|
+
if (contract.status === 'retired') return false;
|
|
1550
1791
|
|
|
1551
1792
|
if (contract.expiresAt && new Date() > new Date(contract.expiresAt)) return false;
|
|
1552
1793
|
|
|
@@ -1555,6 +1796,11 @@ export const contractsRouter = t.router({
|
|
|
1555
1796
|
if (!terms) return false;
|
|
1556
1797
|
|
|
1557
1798
|
if (terms.status !== 'live') return false;
|
|
1799
|
+
if (
|
|
1800
|
+
terms.consentedVersion !== undefined &&
|
|
1801
|
+
terms.consentedVersion < Number(contract.currentVersion ?? 1)
|
|
1802
|
+
)
|
|
1803
|
+
return false;
|
|
1558
1804
|
|
|
1559
1805
|
if (terms.expiresAt && new Date() > new Date(terms.expiresAt)) return false;
|
|
1560
1806
|
|
|
@@ -1601,12 +1847,22 @@ export const contractsRouter = t.router({
|
|
|
1601
1847
|
message: 'Profile does not own these terms',
|
|
1602
1848
|
});
|
|
1603
1849
|
}
|
|
1850
|
+
if (relationship.contract.status === 'retired') {
|
|
1851
|
+
throw new TRPCError({
|
|
1852
|
+
code: 'FORBIDDEN',
|
|
1853
|
+
message: 'This contract is no longer active',
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1604
1856
|
|
|
1857
|
+
assertLatestContractApproval(
|
|
1858
|
+
relationship.terms,
|
|
1859
|
+
Number(relationship.contract.currentVersion ?? 1)
|
|
1860
|
+
);
|
|
1605
1861
|
// Check if the terms are still live
|
|
1606
1862
|
if (relationship.terms.status !== 'live') {
|
|
1607
1863
|
throw new TRPCError({
|
|
1608
1864
|
code: 'FORBIDDEN',
|
|
1609
|
-
message: 'Cannot sync credentials to
|
|
1865
|
+
message: 'Cannot sync credentials to terms that are not live',
|
|
1610
1866
|
});
|
|
1611
1867
|
}
|
|
1612
1868
|
|
|
@@ -1787,9 +2043,9 @@ export const contractsRouter = t.router({
|
|
|
1787
2043
|
method: 'POST',
|
|
1788
2044
|
path: '/consent-flow-contracts/autoboosts/add',
|
|
1789
2045
|
tags: ['Contracts'],
|
|
1790
|
-
summary: '
|
|
2046
|
+
summary: 'Deprecated: add autoboosts to a contract',
|
|
1791
2047
|
description:
|
|
1792
|
-
'
|
|
2048
|
+
'Deprecated compatibility route. Only the contract owner can add autoboosts through a versioned update; additions require reapproval.',
|
|
1793
2049
|
},
|
|
1794
2050
|
requiredScope: 'contracts:write autoboosts:write',
|
|
1795
2051
|
})
|
|
@@ -1805,30 +2061,78 @@ export const contractsRouter = t.router({
|
|
|
1805
2061
|
const { contractUri, autoboosts } = input;
|
|
1806
2062
|
|
|
1807
2063
|
const contract = await getContractByUri(contractUri);
|
|
1808
|
-
|
|
1809
|
-
if (!contract) {
|
|
2064
|
+
if (!contract)
|
|
1810
2065
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
const writers = await getWritersForContract(contract);
|
|
1814
|
-
const isAuthorized = writers.some(writer => writer.profileId === profile.profileId);
|
|
1815
2066
|
|
|
1816
|
-
if (!
|
|
2067
|
+
if (!(await isProfileConsentFlowContractAdmin(profile, contract))) {
|
|
1817
2068
|
throw new TRPCError({
|
|
1818
2069
|
code: 'UNAUTHORIZED',
|
|
1819
2070
|
message: 'You do not have permission to modify autoboosts for this contract.',
|
|
1820
2071
|
});
|
|
1821
2072
|
}
|
|
1822
2073
|
|
|
1823
|
-
const
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2074
|
+
const version = await getConsentFlowContractVersion(
|
|
2075
|
+
contract.id,
|
|
2076
|
+
Number(contract.currentVersion ?? 1)
|
|
2077
|
+
);
|
|
2078
|
+
if (!version)
|
|
2079
|
+
throw new TRPCError({
|
|
2080
|
+
code: 'PRECONDITION_FAILED',
|
|
2081
|
+
message: 'Contract version snapshot is missing',
|
|
2082
|
+
});
|
|
2083
|
+
const existingBoostIds = new Set(version.autoboosts.map(boost => boost.id));
|
|
2084
|
+
const additions = await Promise.all(
|
|
2085
|
+
autoboosts.map(async ab => {
|
|
2086
|
+
const boost = await getBoostByUri(ab.boostUri);
|
|
2087
|
+
if (!boost)
|
|
2088
|
+
throw new TRPCError({
|
|
2089
|
+
code: 'NOT_FOUND',
|
|
2090
|
+
message: `Could not find boost: ${ab.boostUri}`,
|
|
2091
|
+
});
|
|
2092
|
+
if (!(await canProfileIssueBoost(profile, boost))) {
|
|
2093
|
+
throw new TRPCError({
|
|
2094
|
+
code: 'UNAUTHORIZED',
|
|
2095
|
+
message: `Profile does not have permissions to issue boost: ${ab.boostUri}`,
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
if (isDraftBoost(boost))
|
|
2099
|
+
throw new TRPCError({
|
|
2100
|
+
code: 'FORBIDDEN',
|
|
2101
|
+
message:
|
|
2102
|
+
'Draft Boosts cannot be used for automatic credential delivery',
|
|
2103
|
+
});
|
|
2104
|
+
const signingAuthorityName = ab.signingAuthority.name.toLowerCase();
|
|
2105
|
+
const signingAuthority = await getSigningAuthorityForUserByName(
|
|
2106
|
+
profile,
|
|
2107
|
+
ab.signingAuthority.endpoint,
|
|
2108
|
+
signingAuthorityName
|
|
2109
|
+
);
|
|
2110
|
+
if (!signingAuthority)
|
|
2111
|
+
throw new TRPCError({
|
|
2112
|
+
code: 'FORBIDDEN',
|
|
2113
|
+
message: `Signing authority "${signingAuthorityName}" at endpoint "${ab.signingAuthority.endpoint}" not found for this profile`,
|
|
2114
|
+
});
|
|
2115
|
+
return {
|
|
2116
|
+
id: boost.id,
|
|
2117
|
+
signingAuthorityEndpoint: ab.signingAuthority.endpoint,
|
|
2118
|
+
signingAuthorityName,
|
|
2119
|
+
};
|
|
2120
|
+
})
|
|
2121
|
+
);
|
|
2122
|
+
const uniqueAdditions = additions.filter(
|
|
2123
|
+
(boost, index) =>
|
|
2124
|
+
!existingBoostIds.has(boost.id) &&
|
|
2125
|
+
additions.findIndex(candidate => candidate.id === boost.id) === index
|
|
2126
|
+
);
|
|
2127
|
+
await updateConsentFlowContractVersion(
|
|
2128
|
+
{
|
|
2129
|
+
uri: contractUri,
|
|
2130
|
+
changes: { autoboosts: [...version.autoboosts, ...uniqueAdditions] },
|
|
2131
|
+
reason: 'The contract added automatic credential delivery options.',
|
|
1828
2132
|
},
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2133
|
+
profile.profileId,
|
|
2134
|
+
ctx.domain
|
|
2135
|
+
);
|
|
1832
2136
|
|
|
1833
2137
|
return true;
|
|
1834
2138
|
}),
|
|
@@ -1842,7 +2146,7 @@ export const contractsRouter = t.router({
|
|
|
1842
2146
|
tags: ['Contracts'],
|
|
1843
2147
|
summary: 'Remove autoboosts from a contract',
|
|
1844
2148
|
description:
|
|
1845
|
-
'Removes one or more autoboosts from an existing consent flow contract, identified by their boost URIs.
|
|
2149
|
+
'Removes one or more autoboosts from an existing consent flow contract, identified by their boost URIs. Only the contract owner may change this configuration.',
|
|
1846
2150
|
},
|
|
1847
2151
|
requiredScope: 'contracts:write autoboosts:write',
|
|
1848
2152
|
})
|
|
@@ -1863,18 +2167,32 @@ export const contractsRouter = t.router({
|
|
|
1863
2167
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Contract not found' });
|
|
1864
2168
|
}
|
|
1865
2169
|
|
|
1866
|
-
|
|
1867
|
-
const isAuthorized = writers.some(writer => writer.profileId === profile.profileId);
|
|
1868
|
-
|
|
1869
|
-
if (!isAuthorized) {
|
|
2170
|
+
if (!(await isProfileConsentFlowContractAdmin(profile, contract))) {
|
|
1870
2171
|
throw new TRPCError({
|
|
1871
2172
|
code: 'UNAUTHORIZED',
|
|
1872
2173
|
message: 'You do not have permission to modify autoboosts for this contract.',
|
|
1873
2174
|
});
|
|
1874
2175
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2176
|
+
const version = await getConsentFlowContractVersion(
|
|
2177
|
+
contract.id,
|
|
2178
|
+
Number(contract.currentVersion ?? 1)
|
|
2179
|
+
);
|
|
2180
|
+
if (!version)
|
|
2181
|
+
throw new TRPCError({
|
|
2182
|
+
code: 'PRECONDITION_FAILED',
|
|
2183
|
+
message: 'Contract version snapshot is missing',
|
|
2184
|
+
});
|
|
2185
|
+
const removedIds = new Set(boostUris.map(uri => getIdFromUri(uri)));
|
|
2186
|
+
await updateConsentFlowContractVersion(
|
|
2187
|
+
{
|
|
2188
|
+
uri: contractUri,
|
|
2189
|
+
changes: {
|
|
2190
|
+
autoboosts: version.autoboosts.filter(boost => !removedIds.has(boost.id)),
|
|
2191
|
+
},
|
|
2192
|
+
},
|
|
2193
|
+
profile.profileId,
|
|
2194
|
+
ctx.domain
|
|
2195
|
+
);
|
|
1878
2196
|
return true;
|
|
1879
2197
|
}),
|
|
1880
2198
|
|