@learncard/network-brain-service 4.1.3 → 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 +143 -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/workflows.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
isClaimLinkAlreadySetForBoost,
|
|
28
28
|
getClaimLinkSAInfoForBoost,
|
|
29
29
|
getClaimLinkGeneratorProfileId,
|
|
30
|
-
useClaimLinkForBoost,
|
|
30
|
+
useClaimLinkForBoost as claimLinkForBoost,
|
|
31
31
|
} from '@cache/claim-links';
|
|
32
32
|
|
|
33
33
|
import { validateInboxClaimToken } from '@helpers/contact-method.helpers';
|
|
@@ -56,6 +56,7 @@ import { logCredentialClaimed, logCredentialFailed } from '@helpers/activity.hel
|
|
|
56
56
|
import { handleConnectionPromptsForCredentialClaim } from '@helpers/connectionPrompt.helpers';
|
|
57
57
|
import { decryptInboxCredential } from '@helpers/inbox-encryption.helpers';
|
|
58
58
|
import { getBitstringStatusListEntries } from '@learncard/helpers';
|
|
59
|
+
import { neogma } from '@instance';
|
|
59
60
|
import {
|
|
60
61
|
EXHAUSTED,
|
|
61
62
|
exhaustExchangeChallengeForToken,
|
|
@@ -428,7 +429,7 @@ async function handlePresentationForClaim(
|
|
|
428
429
|
).credential as VC;
|
|
429
430
|
|
|
430
431
|
// Mark the challenge as used
|
|
431
|
-
await
|
|
432
|
+
await claimLinkForBoost(exchangeInfo.boostUri, exchangeInfo.challenge);
|
|
432
433
|
|
|
433
434
|
const vp = await issueResponsePresentationWithVcs([vc]);
|
|
434
435
|
|
|
@@ -716,16 +717,60 @@ async function handleInboxClaimPresentation(
|
|
|
716
717
|
deliveryEncryptionFailed = true;
|
|
717
718
|
throw error;
|
|
718
719
|
}
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
720
|
+
const issuerProfile = await getProfileByDid(inboxCredential.issuerDid);
|
|
721
|
+
if (!issuerProfile) throw new Error('Issuer profile not found');
|
|
722
|
+
const boost = inboxCredential.boostUri
|
|
723
|
+
? await getBoostByUri(inboxCredential.boostUri)
|
|
724
|
+
: undefined;
|
|
725
|
+
// Reservation is part of delivery, not best-effort indexing. A conflict
|
|
726
|
+
// must leave escrow pending and must never return the reused credential.
|
|
727
|
+
const delivery = encryptedDelivery;
|
|
728
|
+
const statusEntries = getBitstringStatusListEntries(finalCredential);
|
|
729
|
+
await neogma.getTransaction(null, async tx => {
|
|
730
|
+
const finalized = await finalizeAndWipeInboxCredential(
|
|
731
|
+
inboxCredential.id,
|
|
732
|
+
{
|
|
733
|
+
recipientDid: holderDid,
|
|
734
|
+
credential: delivery,
|
|
735
|
+
},
|
|
736
|
+
{},
|
|
737
|
+
tx
|
|
738
|
+
);
|
|
739
|
+
if (!finalized) throw new Error('Inbox credential is no longer pending');
|
|
740
|
+
const credentialInstance =
|
|
741
|
+
statusEntries.length > 0 || (holderProfile && boost)
|
|
742
|
+
? await storeCredential(
|
|
743
|
+
{
|
|
744
|
+
kind: 'issued-credential',
|
|
745
|
+
credential: delivery,
|
|
746
|
+
statusEntries,
|
|
747
|
+
},
|
|
748
|
+
tx,
|
|
749
|
+
issuerProfile.profileId
|
|
750
|
+
)
|
|
751
|
+
: undefined;
|
|
752
|
+
if (holderProfile) {
|
|
753
|
+
await createClaimedRelationship(
|
|
754
|
+
holderProfile.profileId,
|
|
755
|
+
inboxCredential.id,
|
|
756
|
+
claimToken,
|
|
757
|
+
tx
|
|
758
|
+
);
|
|
759
|
+
if (boost && credentialInstance) {
|
|
760
|
+
await createBoostInstanceOfRelationship(credentialInstance, boost, tx);
|
|
761
|
+
await createSentCredentialRelationship(
|
|
762
|
+
{ type: 'profile', profile: issuerProfile },
|
|
763
|
+
holderProfile,
|
|
764
|
+
credentialInstance,
|
|
765
|
+
undefined,
|
|
766
|
+
undefined,
|
|
767
|
+
undefined,
|
|
768
|
+
tx
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
722
772
|
});
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Record the claim only after finalization succeeds so failed compare-and-swap
|
|
727
|
-
// attempts cannot leave a misleading audit relationship behind.
|
|
728
|
-
if (holderProfile) {
|
|
773
|
+
} else if (holderProfile) {
|
|
729
774
|
await createClaimedRelationship(
|
|
730
775
|
holderProfile.profileId,
|
|
731
776
|
inboxCredential.id,
|
|
@@ -733,38 +778,8 @@ async function handleInboxClaimPresentation(
|
|
|
733
778
|
);
|
|
734
779
|
}
|
|
735
780
|
|
|
736
|
-
// Store credential and create boost relationship if this was a boost issuance
|
|
737
|
-
const boostUri = inboxCredential.boostUri;
|
|
738
|
-
if (!inboxCredential.refreshId && holderProfile && boostUri) {
|
|
739
|
-
try {
|
|
740
|
-
const boost = await getBoostByUri(boostUri);
|
|
741
|
-
const issuerProfile = await getProfileByDid(inboxCredential.issuerDid);
|
|
742
|
-
|
|
743
|
-
if (boost && issuerProfile) {
|
|
744
|
-
// Store the credential in the database
|
|
745
|
-
const credentialInstance = await storeCredential({
|
|
746
|
-
kind: 'issued-credential',
|
|
747
|
-
credential: encryptedDelivery!,
|
|
748
|
-
statusEntries: getBitstringStatusListEntries(finalCredential),
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// Create the boost instance relationship
|
|
752
|
-
await createBoostInstanceOfRelationship(credentialInstance, boost);
|
|
753
|
-
|
|
754
|
-
// Create the sent/received credential relationship
|
|
755
|
-
await createSentCredentialRelationship(
|
|
756
|
-
{ type: 'profile', profile: issuerProfile },
|
|
757
|
-
holderProfile,
|
|
758
|
-
credentialInstance
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
} catch {
|
|
762
|
-
// Escrow is already wiped; preserve delivery even if indexing fails.
|
|
763
|
-
console.error('Failed to index claimed inbox boost', inboxCredential.id);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
781
|
// Log CLAIMED activity - chain to original activityId/integrationId if available
|
|
782
|
+
const boostUri = inboxCredential.boostUri;
|
|
768
783
|
// activityId and integrationId are stored on the inbox credential
|
|
769
784
|
const activityId = inboxCredential.activityId;
|
|
770
785
|
const integrationId = inboxCredential.integrationId;
|
|
@@ -805,11 +820,11 @@ async function handleInboxClaimPresentation(
|
|
|
805
820
|
// Trigger webhook if configured
|
|
806
821
|
if (inboxCredential.webhookUrl) {
|
|
807
822
|
try {
|
|
808
|
-
const
|
|
823
|
+
const webhookLearnCard = await getLearnCard();
|
|
809
824
|
await addNotificationToQueue({
|
|
810
825
|
webhookUrl: inboxCredential.webhookUrl,
|
|
811
826
|
type: LCNNotificationTypeEnumValidator.enum.ISSUANCE_CLAIMED,
|
|
812
|
-
from: { did:
|
|
827
|
+
from: { did: webhookLearnCard.id.did() },
|
|
813
828
|
to: { did: inboxCredential.issuerDid },
|
|
814
829
|
message: getNotificationMessage(
|
|
815
830
|
'issuanceClaimed',
|
|
@@ -871,7 +886,7 @@ async function handleInboxClaimPresentation(
|
|
|
871
886
|
try {
|
|
872
887
|
// Trigger webhook for error if configured
|
|
873
888
|
if (inboxCredential.webhookUrl) {
|
|
874
|
-
const
|
|
889
|
+
const webhookLearnCard = await getLearnCard();
|
|
875
890
|
const issuanceErrorMsg = getNotificationMessage(
|
|
876
891
|
'issuanceError',
|
|
877
892
|
// failedIssuerProfile is the issuer (the webhook recipient),
|
|
@@ -883,7 +898,7 @@ async function handleInboxClaimPresentation(
|
|
|
883
898
|
await addNotificationToQueue({
|
|
884
899
|
webhookUrl: inboxCredential.webhookUrl,
|
|
885
900
|
type: LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR,
|
|
886
|
-
from: { did:
|
|
901
|
+
from: { did: webhookLearnCard.id.did() },
|
|
887
902
|
to: { did: inboxCredential.issuerDid },
|
|
888
903
|
message: {
|
|
889
904
|
title: issuanceErrorMsg.title,
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { Notification } from '../delivery.service';
|
|
3
3
|
|
|
4
|
-
const mocks = vi.hoisted(() => ({
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
send: vi.fn(),
|
|
6
|
+
legacySend: vi.fn(),
|
|
7
|
+
render: vi.fn(),
|
|
8
|
+
stream: vi.fn(),
|
|
9
|
+
}));
|
|
5
10
|
vi.mock('@environment', () => ({ environment: {} }));
|
|
6
11
|
vi.mock('postmark', () => ({
|
|
7
12
|
ServerClient: class {
|
|
13
|
+
getMessageStream = mocks.stream;
|
|
8
14
|
sendEmail = mocks.send;
|
|
9
15
|
sendEmailWithTemplate = mocks.legacySend;
|
|
10
16
|
},
|
|
@@ -25,6 +31,7 @@ beforeEach(() => {
|
|
|
25
31
|
vi.clearAllMocks();
|
|
26
32
|
mocks.render.mockResolvedValue({ html: '<p>Updated</p>', text: 'Updated', subject: 'Updated' });
|
|
27
33
|
mocks.send.mockResolvedValue({});
|
|
34
|
+
mocks.stream.mockResolvedValue({ MessageStreamType: 'Broadcasts' });
|
|
28
35
|
});
|
|
29
36
|
|
|
30
37
|
describe('update email provider attempts', () => {
|
|
@@ -52,3 +59,65 @@ describe('update email provider attempts', () => {
|
|
|
52
59
|
expect(mocks.legacySend).not.toHaveBeenCalled();
|
|
53
60
|
});
|
|
54
61
|
});
|
|
62
|
+
|
|
63
|
+
describe('campaign provider boundary', () => {
|
|
64
|
+
const campaignNotification: Notification = {
|
|
65
|
+
...notification,
|
|
66
|
+
templateId: 'campaign-announcement',
|
|
67
|
+
subject: 'Authored subject',
|
|
68
|
+
messageStream: 'news',
|
|
69
|
+
headers: [
|
|
70
|
+
{
|
|
71
|
+
Name: 'List-Unsubscribe',
|
|
72
|
+
Value: '<https://brain.example/campaigns/unsubscribe?token=test>',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
it('accepts the Postmark Broadcasts stream type and forwards headers and subject', async () => {
|
|
77
|
+
await new PostmarkAdapter('test-key').send(campaignNotification);
|
|
78
|
+
expect(mocks.stream).toHaveBeenCalledWith('news');
|
|
79
|
+
expect(mocks.send).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
Subject: 'Authored subject',
|
|
82
|
+
MessageStream: 'news',
|
|
83
|
+
Headers: campaignNotification.headers,
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
it('validates one stream per wave and prepares without sending', async () => {
|
|
88
|
+
const adapter = new PostmarkAdapter('test-key');
|
|
89
|
+
await adapter.assertBroadcastStream('news');
|
|
90
|
+
const first = await adapter.prepareCampaignEmail(campaignNotification);
|
|
91
|
+
const second = await adapter.prepareCampaignEmail(campaignNotification);
|
|
92
|
+
expect(mocks.stream).toHaveBeenCalledOnce();
|
|
93
|
+
expect(mocks.render).toHaveBeenCalledTimes(2);
|
|
94
|
+
expect(mocks.send).not.toHaveBeenCalled();
|
|
95
|
+
await first();
|
|
96
|
+
await second();
|
|
97
|
+
expect(mocks.send).toHaveBeenCalledTimes(2);
|
|
98
|
+
});
|
|
99
|
+
it('does not cache failed stream checks', async () => {
|
|
100
|
+
const adapter = new PostmarkAdapter('test-key');
|
|
101
|
+
mocks.stream.mockRejectedValueOnce(new Error('provider private detail'));
|
|
102
|
+
await expect(adapter.assertBroadcastStream('news')).rejects.toThrow(
|
|
103
|
+
'stream validation failed'
|
|
104
|
+
);
|
|
105
|
+
await adapter.assertBroadcastStream('news');
|
|
106
|
+
expect(mocks.stream).toHaveBeenCalledTimes(2);
|
|
107
|
+
});
|
|
108
|
+
it('rejects a custom transactional stream', async () => {
|
|
109
|
+
mocks.stream.mockResolvedValue({ MessageStreamType: 'Transactional' });
|
|
110
|
+
await expect(new PostmarkAdapter('test-key').send(campaignNotification)).rejects.toThrow(
|
|
111
|
+
'Broadcast'
|
|
112
|
+
);
|
|
113
|
+
expect(mocks.send).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
it('never falls back to another send after an uncertain acknowledgement', async () => {
|
|
116
|
+
mocks.send.mockRejectedValueOnce(new Error('timeout'));
|
|
117
|
+
await expect(new PostmarkAdapter('test-key').send(campaignNotification)).rejects.toThrow(
|
|
118
|
+
'timeout'
|
|
119
|
+
);
|
|
120
|
+
expect(mocks.send).toHaveBeenCalledOnce();
|
|
121
|
+
expect(mocks.legacySend).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -42,6 +42,7 @@ const LOCAL_TEMPLATE_MAP: Record<string, TemplateId> = {
|
|
|
42
42
|
'guardian-email-otp': 'guardian-email-otp',
|
|
43
43
|
'guardian-rejected-credential': 'guardian-rejected-credential',
|
|
44
44
|
'credential-updated': 'credential-updated',
|
|
45
|
+
'campaign-announcement': 'announcement',
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
export class PostmarkAdapter implements DeliveryService {
|
|
@@ -51,7 +52,65 @@ export class PostmarkAdapter implements DeliveryService {
|
|
|
51
52
|
this.client = new ServerClient(apiKey);
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
private readonly broadcastStreams = new Map<string, Promise<void>>();
|
|
56
|
+
|
|
57
|
+
/** Validate once per adapter/wave; failed checks are never cached. */
|
|
58
|
+
public async assertBroadcastStream(streamId?: string): Promise<void> {
|
|
59
|
+
if (!streamId || streamId.toLowerCase() === 'outbound')
|
|
60
|
+
throw new Error('Campaign email requires a broadcast stream');
|
|
61
|
+
let check = this.broadcastStreams.get(streamId);
|
|
62
|
+
if (!check) {
|
|
63
|
+
check = this.client.getMessageStream(streamId).then(stream => {
|
|
64
|
+
if (stream.MessageStreamType !== 'Broadcasts')
|
|
65
|
+
throw new Error('Campaign email requires a Postmark Broadcast stream');
|
|
66
|
+
});
|
|
67
|
+
this.broadcastStreams.set(streamId, check);
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
await check;
|
|
71
|
+
} catch {
|
|
72
|
+
this.broadcastStreams.delete(streamId);
|
|
73
|
+
throw new Error('Campaign Postmark Broadcast stream validation failed');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Prepare without sending, so callers can render before reserving a recipient. */
|
|
78
|
+
public async prepareCampaignEmail(notification: Notification): Promise<() => Promise<void>> {
|
|
79
|
+
await this.assertBroadcastStream(notification.messageStream);
|
|
80
|
+
const branding = resolveBranding(notification.branding);
|
|
81
|
+
const rendered = await renderEmail(
|
|
82
|
+
'announcement',
|
|
83
|
+
branding,
|
|
84
|
+
notification.templateModel as TemplateDataMap['announcement'],
|
|
85
|
+
notification.locale
|
|
86
|
+
);
|
|
87
|
+
const brandName =
|
|
88
|
+
notification.branding?.brandName || environment.POSTMARK_BRAND_NAME || 'LearnCard';
|
|
89
|
+
const from = notification.branding?.fromDomain
|
|
90
|
+
? buildFromAddress(brandName, `support@${notification.branding.fromDomain}`)
|
|
91
|
+
: buildFromAddress(
|
|
92
|
+
brandName,
|
|
93
|
+
environment.POSTMARK_FROM_EMAIL || 'support@learningeconomy.io'
|
|
94
|
+
);
|
|
95
|
+
return async () => {
|
|
96
|
+
await this.client.sendEmail({
|
|
97
|
+
From: from,
|
|
98
|
+
To: notification.contactMethod.value,
|
|
99
|
+
Subject: notification.subject ?? rendered.subject,
|
|
100
|
+
HtmlBody: rendered.html,
|
|
101
|
+
TextBody: rendered.text,
|
|
102
|
+
MessageStream: notification.messageStream,
|
|
103
|
+
Headers: notification.headers,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
54
108
|
public async send(notification: Notification): Promise<void> {
|
|
109
|
+
if (notification.templateId === 'campaign-announcement') {
|
|
110
|
+
const deliver = await this.prepareCampaignEmail(notification);
|
|
111
|
+
await deliver();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
55
114
|
const defaultFrom = environment.POSTMARK_FROM_EMAIL || 'support@learningeconomy.io';
|
|
56
115
|
const defaultBrandName = environment.POSTMARK_BRAND_NAME || 'LearnCard';
|
|
57
116
|
|
|
@@ -82,12 +141,18 @@ export class PostmarkAdapter implements DeliveryService {
|
|
|
82
141
|
notification.locale
|
|
83
142
|
);
|
|
84
143
|
} catch (renderError) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
144
|
+
if (notification.templateId !== 'campaign-announcement')
|
|
145
|
+
console.error(
|
|
146
|
+
`[PostmarkAdapter] Local render failed for "${notification.templateId}":`,
|
|
147
|
+
renderError
|
|
148
|
+
);
|
|
89
149
|
// This template is owned locally; there is no legacy provider alias.
|
|
90
|
-
if (
|
|
150
|
+
if (
|
|
151
|
+
['credential-updated', 'campaign-announcement'].includes(
|
|
152
|
+
notification.templateId
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
throw renderError;
|
|
91
156
|
}
|
|
92
157
|
|
|
93
158
|
if (rendered) {
|
|
@@ -95,21 +160,28 @@ export class PostmarkAdapter implements DeliveryService {
|
|
|
95
160
|
await this.client.sendEmail({
|
|
96
161
|
From: from,
|
|
97
162
|
To: notification.contactMethod.value,
|
|
98
|
-
Subject: rendered.subject,
|
|
163
|
+
Subject: notification.subject ?? rendered.subject,
|
|
99
164
|
HtmlBody: rendered.html,
|
|
100
165
|
TextBody: rendered.text,
|
|
101
166
|
MessageStream: notification.messageStream,
|
|
167
|
+
Headers: notification.headers,
|
|
102
168
|
});
|
|
103
169
|
|
|
104
170
|
return;
|
|
105
171
|
} catch (sendError) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
172
|
+
if (notification.templateId !== 'campaign-announcement')
|
|
173
|
+
console.error(
|
|
174
|
+
`[PostmarkAdapter] sendEmail API failed for "${notification.templateId}":`,
|
|
175
|
+
sendError
|
|
176
|
+
);
|
|
110
177
|
// A lost provider acknowledgement may already have sent the email.
|
|
111
178
|
// Do not bypass the refresh delivery claim with a second attempt.
|
|
112
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
['credential-updated', 'campaign-announcement'].includes(
|
|
181
|
+
notification.templateId
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
throw sendError;
|
|
113
185
|
}
|
|
114
186
|
}
|
|
115
187
|
}
|
|
@@ -122,6 +194,7 @@ export class PostmarkAdapter implements DeliveryService {
|
|
|
122
194
|
TemplateAlias: notification.templateId,
|
|
123
195
|
TemplateModel: notification.templateModel,
|
|
124
196
|
MessageStream: notification.messageStream,
|
|
197
|
+
Headers: notification.headers,
|
|
125
198
|
});
|
|
126
199
|
} catch (error) {
|
|
127
200
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -12,6 +12,8 @@ export interface Notification {
|
|
|
12
12
|
templateId: string; // e.g., 'universal-inbox-claim'
|
|
13
13
|
templateModel: Record<string, any>; // Data for the template
|
|
14
14
|
messageStream?: string;
|
|
15
|
+
subject?: string;
|
|
16
|
+
headers?: Array<{ Name: string; Value: string }>;
|
|
15
17
|
/** Optional tenant branding. When present, emails are rendered locally via @learncard/email-templates. */
|
|
16
18
|
branding?: Partial<TenantBranding>;
|
|
17
19
|
/** Recipient locale (BCP-47), resolved via resolveRecipientLocale. Defaults to English at render time. */
|
|
@@ -26,6 +28,8 @@ export const NotificationSchema = z.object({
|
|
|
26
28
|
templateId: z.string(),
|
|
27
29
|
templateModel: z.record(z.string(), z.any()),
|
|
28
30
|
messageStream: z.string().optional(),
|
|
31
|
+
subject: z.string().optional(),
|
|
32
|
+
headers: z.array(z.object({ Name: z.string(), Value: z.string() })).optional(),
|
|
29
33
|
branding: z.record(z.string(), z.any()).optional(),
|
|
30
34
|
locale: z.string().optional(),
|
|
31
35
|
});
|
package/src/types/campaign.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CampaignIdValidator, type CampaignStatus } from '@learncard/types';
|
|
2
3
|
|
|
3
4
|
export type CampaignRecord = {
|
|
4
5
|
id: string;
|
|
@@ -15,3 +16,15 @@ export type CampaignRecord = {
|
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
export type CampaignAudienceQuery = { match: string; params: Record<string, unknown> };
|
|
19
|
+
|
|
20
|
+
export const CampaignDispatchJobValidator = z
|
|
21
|
+
.object({
|
|
22
|
+
campaignId: CampaignIdValidator,
|
|
23
|
+
campaignCreatedAt: z.iso.datetime(),
|
|
24
|
+
channel: z.enum(['notify', 'email']).default('notify'),
|
|
25
|
+
afterProfileId: z.string().optional(),
|
|
26
|
+
dispatchId: z.uuid(),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
export type CampaignDispatchJob = z.input<typeof CampaignDispatchJobValidator>;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ConsentFlowTransactionActionValidator,
|
|
6
6
|
ConsentFlowGuardianApprovalValidator,
|
|
7
7
|
ConsentFlowReferralValidator,
|
|
8
|
+
ConsentFlowContractDetailsValidator,
|
|
8
9
|
} from '@learncard/types';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
|
|
@@ -22,6 +23,27 @@ type FlatReferral = {
|
|
|
22
23
|
|
|
23
24
|
type FlatConsentTerms = Partial<Record<`terms.${string}`, string | boolean | string[]>>;
|
|
24
25
|
|
|
26
|
+
export const DbContractVersionValidator = z.object({
|
|
27
|
+
id: z.string(),
|
|
28
|
+
version: z.coerce.number().int().positive(),
|
|
29
|
+
contract: ConsentFlowContractValidator,
|
|
30
|
+
name: z.string(),
|
|
31
|
+
subtitle: z.string().optional(),
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
reasonForAccessing: z.string().optional(),
|
|
34
|
+
image: z.string().optional(),
|
|
35
|
+
needsGuardianConsent: z.boolean().optional(),
|
|
36
|
+
redirectUrl: z.string().optional(),
|
|
37
|
+
frontDoorBoostUri: z.string().optional(),
|
|
38
|
+
expiresAt: z.string().optional(),
|
|
39
|
+
autoboosts: z.array(z.unknown()).default([]),
|
|
40
|
+
writers: z.array(z.string()).default([]),
|
|
41
|
+
reason: z.string().optional(),
|
|
42
|
+
createdAt: z.string(),
|
|
43
|
+
kind: z.enum(['relaxed', 'expanded_optional', 'requires_consent']).optional(),
|
|
44
|
+
});
|
|
45
|
+
export type DbContractVersionType = z.infer<typeof DbContractVersionValidator>;
|
|
46
|
+
|
|
25
47
|
export const DbContractValidator = z.object({
|
|
26
48
|
id: z.string(),
|
|
27
49
|
name: z.string(),
|
|
@@ -32,16 +54,31 @@ export const DbContractValidator = z.object({
|
|
|
32
54
|
redirectUrl: z.string().optional(),
|
|
33
55
|
frontDoorBoostUri: z.string().optional(),
|
|
34
56
|
image: z.string().optional(),
|
|
35
|
-
contract
|
|
57
|
+
// An empty request has no flattened contract.* properties in Neo4j.
|
|
58
|
+
contract: ConsentFlowContractValidator.prefault({}),
|
|
36
59
|
createdAt: z.string(),
|
|
37
60
|
updatedAt: z.string(),
|
|
38
61
|
expiresAt: z.string().optional(),
|
|
39
62
|
audienceVersion: z.coerce.number().int().nonnegative().optional(),
|
|
40
63
|
hasConsented: z.boolean().optional(),
|
|
64
|
+
currentVersion: z.coerce.number().int().positive().optional(),
|
|
65
|
+
status: z.enum(['active', 'retired']).optional(),
|
|
66
|
+
version: z.coerce.number().int().positive().optional(),
|
|
67
|
+
latestUpdate: z.preprocess(value => {
|
|
68
|
+
if (typeof value !== 'string') return value;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(value);
|
|
71
|
+
} catch {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
}, ConsentFlowContractDetailsValidator.shape.latestUpdate.optional()),
|
|
41
75
|
});
|
|
42
76
|
|
|
43
77
|
export type DbContractType = z.infer<typeof DbContractValidator>;
|
|
44
|
-
export type FlatDbContractType = DbContractType
|
|
78
|
+
export type FlatDbContractType = Omit<DbContractType, 'contract' | 'latestUpdate'> &
|
|
79
|
+
Partial<Record<`contract.${string}`, string | boolean>> & {
|
|
80
|
+
latestUpdate?: string;
|
|
81
|
+
};
|
|
45
82
|
|
|
46
83
|
export const DbTransactionValidator = z.object({
|
|
47
84
|
id: z.string(),
|
|
@@ -73,6 +110,10 @@ export const DbTermsValidator = z.object({
|
|
|
73
110
|
mutationVersion: z.coerce.number().int().nonnegative().optional(),
|
|
74
111
|
id: z.string(),
|
|
75
112
|
status: ConsentFlowTermsStatusValidator,
|
|
113
|
+
consentedVersion: z.coerce.number().int().positive().optional(),
|
|
114
|
+
lastReviewedVersion: z.coerce.number().int().positive().optional(),
|
|
115
|
+
reconsentDeadline: z.string().optional(),
|
|
116
|
+
newOptionsUntil: z.string().optional(),
|
|
76
117
|
// Neo4j has no properties to store for entirely empty permission objects.
|
|
77
118
|
terms: ConsentFlowTermsValidator.prefault({}),
|
|
78
119
|
guardianApproval: ConsentFlowGuardianApprovalValidator.optional(),
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
CredentialRefreshSigningModeValidator,
|
|
5
|
+
CredentialRefreshUpdateReasonValidator,
|
|
5
6
|
PublishCredentialRefreshNotificationValidator,
|
|
6
7
|
} from '@learncard/types';
|
|
7
8
|
|
|
@@ -45,6 +46,12 @@ export const CredentialRefreshRecordValidator = z.object({
|
|
|
45
46
|
rootMaterialDigest: z.string().optional(),
|
|
46
47
|
/** Server-keyed fingerprint of the original credentialStatus descriptor */
|
|
47
48
|
credentialStatusDigest: z.string().optional(),
|
|
49
|
+
/** Exact public status descriptor, including object/array shape. JSON null means absent. */
|
|
50
|
+
credentialStatusBinding: z.string().optional(),
|
|
51
|
+
/** Exact signed refresh-service descriptor, including additional services and array order. */
|
|
52
|
+
refreshServiceBinding: z.string().optional(),
|
|
53
|
+
encryptionMode: z.enum(['network', 'holder']).optional(),
|
|
54
|
+
statusEntries: z.string().optional(),
|
|
48
55
|
/** Keyed fingerprint of version 1 subject identifiers; no extra subject DIDs persisted. */
|
|
49
56
|
credentialSubjectDigest: z.string().optional(),
|
|
50
57
|
/** Optional boost permanently associated with the initial credential bind. */
|
|
@@ -54,6 +61,7 @@ export const CredentialRefreshRecordValidator = z.object({
|
|
|
54
61
|
idempotencyKey: z.string().optional(),
|
|
55
62
|
/** Issuer-authored, privacy-safe update summary */
|
|
56
63
|
updateSummary: z.string().optional(),
|
|
64
|
+
updateReason: CredentialRefreshUpdateReasonValidator.optional(),
|
|
57
65
|
/**
|
|
58
66
|
* Bounded, normalized credential display title used only for the email update
|
|
59
67
|
* notice. Never rendered in in-app/push payloads. Sourced from the unsigned
|
|
@@ -108,6 +116,7 @@ export const CredentialRefreshVersionNodeValidator = z.object({
|
|
|
108
116
|
etag: z.string().optional(),
|
|
109
117
|
signingMode: CredentialRefreshSigningModeValidator,
|
|
110
118
|
updateSummary: z.string().optional(),
|
|
119
|
+
updateReason: CredentialRefreshUpdateReasonValidator.optional(),
|
|
111
120
|
/** Bounded credential display title for the email update notice only. */
|
|
112
121
|
credentialDisplayName: z.string().optional(),
|
|
113
122
|
/** Caller-provided key for replaying this exact historical publication */
|
|
@@ -148,7 +157,8 @@ export const AdvanceCredentialRefreshHeadParamsValidator = z.object({
|
|
|
148
157
|
etag: z.string().optional(),
|
|
149
158
|
materialDigest: z.string().optional(),
|
|
150
159
|
signingMode: CredentialRefreshSigningModeValidator,
|
|
151
|
-
updateSummary: z.string().optional(),
|
|
160
|
+
updateSummary: z.string().max(280).optional(),
|
|
161
|
+
updateReason: CredentialRefreshUpdateReasonValidator.optional(),
|
|
152
162
|
/** Bounded credential display title persisted for the email update notice only. */
|
|
153
163
|
credentialDisplayName: z.string().optional(),
|
|
154
164
|
idempotencyKey: z.string().optional(),
|
package/src/types/credential.ts
CHANGED
|
@@ -12,5 +12,7 @@ export const CredentialValidator = z.object({
|
|
|
12
12
|
id: z.string(),
|
|
13
13
|
credential: z.string(),
|
|
14
14
|
statusEntries: z.string().optional(),
|
|
15
|
+
statusOwnerProfileId: z.string().optional(),
|
|
16
|
+
statusReservationMode: z.enum(['strict', 'lenient']).optional(),
|
|
15
17
|
});
|
|
16
18
|
export type CredentialType = z.infer<typeof CredentialValidator>;
|