@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as notificationTransport from '@helpers/notification-webhook-fetch.helpers';
|
|
1
2
|
import type { Context, SQSBatchResponse, SQSEvent, SQSRecord } from 'aws-lambda';
|
|
2
3
|
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
|
|
@@ -93,12 +94,7 @@ const invokeWorker = async (...records: SQSRecord[]): Promise<SQSBatchResponse>
|
|
|
93
94
|
return result as SQSBatchResponse;
|
|
94
95
|
};
|
|
95
96
|
|
|
96
|
-
const fetchResponse = (stored: boolean): Response =>
|
|
97
|
-
({
|
|
98
|
-
ok: true,
|
|
99
|
-
status: 200,
|
|
100
|
-
text: async () => JSON.stringify(stored),
|
|
101
|
-
} as Response);
|
|
97
|
+
const fetchResponse = (stored: boolean): Response => new Response(JSON.stringify(stored));
|
|
102
98
|
|
|
103
99
|
const getSenderDeliveryState = async (): Promise<{
|
|
104
100
|
delivered: boolean;
|
|
@@ -139,18 +135,20 @@ describe('notifications SQS worker', () => {
|
|
|
139
135
|
profileB = (await getProfileByProfileId('userb'))!;
|
|
140
136
|
vi.spyOn(LearnCardHelpers, 'getDidWebLearnCard').mockResolvedValue({
|
|
141
137
|
invoke: { getDidAuthVp: async () => 'test-did-auth-vp' },
|
|
142
|
-
} as
|
|
138
|
+
} as unknown as Awaited<ReturnType<typeof LearnCardHelpers.getDidWebLearnCard>>);
|
|
143
139
|
});
|
|
144
140
|
|
|
145
141
|
it('returns independent batch failures for downstream false and transport errors', async () => {
|
|
146
|
-
const fetchSpy = vi
|
|
147
|
-
|
|
142
|
+
const fetchSpy = vi
|
|
143
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
144
|
+
.mockImplementation(async (_url, init) => {
|
|
145
|
+
const body = JSON.parse(String(init?.body)) as LCNNotification;
|
|
148
146
|
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
if (body.message?.title === 'not-stored') return fetchResponse(false);
|
|
148
|
+
if (body.message?.title === 'transport-error') throw new Error('network failure');
|
|
151
149
|
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
return fetchResponse(true);
|
|
151
|
+
});
|
|
154
152
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
155
153
|
|
|
156
154
|
try {
|
|
@@ -191,7 +189,9 @@ describe('notifications SQS worker', () => {
|
|
|
191
189
|
attemptedAt: new Date().toISOString(),
|
|
192
190
|
}
|
|
193
191
|
);
|
|
194
|
-
const fetchSpy = vi
|
|
192
|
+
const fetchSpy = vi
|
|
193
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
194
|
+
.mockResolvedValue(fetchResponse(true));
|
|
195
195
|
|
|
196
196
|
try {
|
|
197
197
|
await expect(
|
|
@@ -222,11 +222,13 @@ describe('notifications SQS worker', () => {
|
|
|
222
222
|
sender: profileA,
|
|
223
223
|
triggerId: 'credential:worker-unknown-ack',
|
|
224
224
|
});
|
|
225
|
-
const fetchSpy = vi
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
225
|
+
const fetchSpy = vi
|
|
226
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
227
|
+
.mockResolvedValue({
|
|
228
|
+
ok: true,
|
|
229
|
+
status: 200,
|
|
230
|
+
text: async () => JSON.stringify({ status: 'ok' }),
|
|
231
|
+
} as Response);
|
|
230
232
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
231
233
|
|
|
232
234
|
try {
|
|
@@ -251,11 +253,13 @@ describe('notifications SQS worker', () => {
|
|
|
251
253
|
});
|
|
252
254
|
|
|
253
255
|
it('preserves compatible unrecognized 2xx handling for legacy queue records', async () => {
|
|
254
|
-
const fetchSpy = vi
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
const fetchSpy = vi
|
|
257
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
258
|
+
.mockResolvedValue({
|
|
259
|
+
ok: true,
|
|
260
|
+
status: 200,
|
|
261
|
+
text: async () => JSON.stringify({ status: 'ok' }),
|
|
262
|
+
} as Response);
|
|
259
263
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
260
264
|
|
|
261
265
|
try {
|
|
@@ -281,7 +285,9 @@ describe('notifications SQS worker', () => {
|
|
|
281
285
|
counterpartProfileId: profileB.profileId,
|
|
282
286
|
})
|
|
283
287
|
);
|
|
284
|
-
const fetchSpy = vi
|
|
288
|
+
const fetchSpy = vi
|
|
289
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
290
|
+
.mockResolvedValue(fetchResponse(true));
|
|
285
291
|
const originalRun = neogma.queryRunner.run.bind(neogma.queryRunner);
|
|
286
292
|
const queryRunnerSpy = vi
|
|
287
293
|
.spyOn(neogma.queryRunner, 'run')
|
|
@@ -43,6 +43,7 @@ import { getCredentialRefresh, getCredentialRefreshHead } from '@accesslayer/cre
|
|
|
43
43
|
import { testUnsignedBoost } from './helpers/send';
|
|
44
44
|
import {
|
|
45
45
|
claimRefreshSendIntent,
|
|
46
|
+
computeRefreshSendRequestDigest,
|
|
46
47
|
getRefreshSendIntent,
|
|
47
48
|
recordRefreshSendIntent,
|
|
48
49
|
recordRefreshSendIntentPending,
|
|
@@ -215,7 +216,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
215
216
|
});
|
|
216
217
|
|
|
217
218
|
describe('inline refresh preparation', () => {
|
|
218
|
-
it('
|
|
219
|
+
it('keeps only public display and contract linkage, then reuses one boost', async () => {
|
|
219
220
|
const contractUri = await issuer.clients.fullAuth.contracts.createConsentFlowContract({
|
|
220
221
|
contract: {
|
|
221
222
|
read: { anonymize: false },
|
|
@@ -242,8 +243,10 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
242
243
|
});
|
|
243
244
|
const baseline = await getMutationBaseline();
|
|
244
245
|
const prepared = await issuer.clients.fullAuth.boost.prepareRefreshableSend({
|
|
246
|
+
statusBinding: 'server',
|
|
245
247
|
recipient: HOLDER_PROFILE_ID,
|
|
246
248
|
contractUri,
|
|
249
|
+
display: { name: 'Prepared Boost', category: 'TestBoosts' },
|
|
247
250
|
template: {
|
|
248
251
|
credential: testUnsignedBoost,
|
|
249
252
|
name: 'Prepared Boost',
|
|
@@ -258,11 +261,12 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
258
261
|
expect(boost).toMatchObject({
|
|
259
262
|
name: 'Prepared Boost',
|
|
260
263
|
category: 'TestBoosts',
|
|
261
|
-
claimPermissions: { canView: true },
|
|
262
264
|
});
|
|
265
|
+
expect(boost.boost.display).toEqual({ name: 'Prepared Boost', category: 'TestBoosts' });
|
|
266
|
+
expect(boost.boost.credentialSubject).toEqual({ id: 'did:example:123' });
|
|
263
267
|
expect(
|
|
264
268
|
await issuer.clients.fullAuth.boost.getBoostSkills({ uri: prepared.boostUri })
|
|
265
|
-
).toEqual([
|
|
269
|
+
).toEqual([]);
|
|
266
270
|
const links = await runQuery(
|
|
267
271
|
'MATCH (:ConsentFlowContract)-[:RELATED_TO]->(b:Boost {name: $name}) RETURN count(b) AS count',
|
|
268
272
|
{ name: 'Prepared Boost' }
|
|
@@ -281,6 +285,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
281
285
|
{
|
|
282
286
|
...testUnsignedBoost,
|
|
283
287
|
id: prepared.credentialId,
|
|
288
|
+
credentialStatus: prepared.statusEntries,
|
|
284
289
|
issuer: issuer.learnCard.id.did(),
|
|
285
290
|
credentialSubject: {
|
|
286
291
|
...testUnsignedBoost.credentialSubject,
|
|
@@ -390,6 +395,66 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
390
395
|
});
|
|
391
396
|
|
|
392
397
|
describe('whole-call idempotency (idempotencyKey)', () => {
|
|
398
|
+
it.each(['prepared', 'delivered', 'stale'] as const)(
|
|
399
|
+
'retries a %s intent across retained digest key rotation',
|
|
400
|
+
async state => {
|
|
401
|
+
try {
|
|
402
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'old-send-test-key');
|
|
403
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v1');
|
|
404
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
|
|
405
|
+
const input = {
|
|
406
|
+
recipient: HOLDER_PROFILE_ID,
|
|
407
|
+
template: inlineTemplate(),
|
|
408
|
+
idempotencyKey: `rotation-${state}`,
|
|
409
|
+
};
|
|
410
|
+
const send = () =>
|
|
411
|
+
state === 'delivered'
|
|
412
|
+
? issuer.clients.fullAuth.boost.send({
|
|
413
|
+
...input,
|
|
414
|
+
type: 'boost',
|
|
415
|
+
refresh: true,
|
|
416
|
+
})
|
|
417
|
+
: issuer.clients.fullAuth.boost.prepareRefreshableSend(input);
|
|
418
|
+
const first = await send();
|
|
419
|
+
const original = await getRefreshSendIntent(
|
|
420
|
+
ISSUER_PROFILE_ID,
|
|
421
|
+
input.idempotencyKey
|
|
422
|
+
);
|
|
423
|
+
if (state === 'stale') {
|
|
424
|
+
await neogma.queryRunner.run(
|
|
425
|
+
`MATCH (i:RefreshSendIntent {intentKey: $key})
|
|
426
|
+
SET i.state = 'preparing', i.updatedAt = '2000-01-01T00:00:00.000Z'`,
|
|
427
|
+
{ key: original!.intentKey }
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'new-send-test-key');
|
|
431
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
|
|
432
|
+
vi.stubEnv(
|
|
433
|
+
'CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS',
|
|
434
|
+
JSON.stringify({ v1: 'old-send-test-key' })
|
|
435
|
+
);
|
|
436
|
+
expect(await send()).toEqual(first);
|
|
437
|
+
const retried = await getRefreshSendIntent(
|
|
438
|
+
ISSUER_PROFILE_ID,
|
|
439
|
+
input.idempotencyKey
|
|
440
|
+
);
|
|
441
|
+
expect(retried?.requestDigest).toBe(original?.requestDigest);
|
|
442
|
+
if (state === 'stale')
|
|
443
|
+
expect(retried?.claimToken).not.toBe(original?.claimToken);
|
|
444
|
+
await expect(
|
|
445
|
+
issuer.clients.fullAuth.boost.prepareRefreshableSend({
|
|
446
|
+
...input,
|
|
447
|
+
recipient: OUTSIDER_PROFILE_ID,
|
|
448
|
+
})
|
|
449
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
450
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
|
|
451
|
+
await expect(send()).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
452
|
+
} finally {
|
|
453
|
+
vi.unstubAllEnvs();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
|
|
393
458
|
const inlineTemplate = () => ({
|
|
394
459
|
credential: testUnsignedVcV2,
|
|
395
460
|
name: 'Idempotent Refreshable',
|
|
@@ -468,6 +533,16 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
468
533
|
};
|
|
469
534
|
|
|
470
535
|
const first = (await issuer.clients.fullAuth.boost.send(input)) as SendResult;
|
|
536
|
+
// Direct signing-authority requests retain their pre-capability fingerprint.
|
|
537
|
+
expect(
|
|
538
|
+
(await getRefreshSendIntent(ISSUER_PROFILE_ID, input.idempotencyKey))?.requestDigest
|
|
539
|
+
).toBe(
|
|
540
|
+
computeRefreshSendRequestDigest({
|
|
541
|
+
recipientProfileId: HOLDER_PROFILE_ID,
|
|
542
|
+
holderDid: getDidWeb(DOMAIN, HOLDER_PROFILE_ID),
|
|
543
|
+
template: input.template,
|
|
544
|
+
})
|
|
545
|
+
);
|
|
471
546
|
const second = (await issuer.clients.fullAuth.boost.send(input)) as SendResult;
|
|
472
547
|
|
|
473
548
|
expect(second).toEqual(first);
|
|
@@ -481,6 +556,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
481
556
|
it('completes an abandoned prepare on the next send with the same key', async () => {
|
|
482
557
|
// Simulates the SDK failing after prepare (e.g. while signing).
|
|
483
558
|
const prepared = await issuer.clients.fullAuth.boost.prepareRefreshableSend({
|
|
559
|
+
statusBinding: 'server',
|
|
484
560
|
recipient: HOLDER_PROFILE_ID,
|
|
485
561
|
template: inlineTemplate(),
|
|
486
562
|
idempotencyKey: 'abandoned-1',
|
|
@@ -491,6 +567,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
491
567
|
{
|
|
492
568
|
...testUnsignedVcV2,
|
|
493
569
|
id: prepared.credentialId,
|
|
570
|
+
credentialStatus: prepared.statusEntries,
|
|
494
571
|
issuer: issuer.learnCard.id.did(),
|
|
495
572
|
validFrom: '2026-01-01T00:00:00Z',
|
|
496
573
|
credentialSubject: { id: prepared.holderDid },
|
|
@@ -515,6 +592,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
515
592
|
// A later prepare with the same key reports the completed result: the SDK
|
|
516
593
|
// returns it without signing again.
|
|
517
594
|
const again = await issuer.clients.fullAuth.boost.prepareRefreshableSend({
|
|
595
|
+
statusBinding: 'server',
|
|
518
596
|
recipient: HOLDER_PROFILE_ID,
|
|
519
597
|
template: inlineTemplate(),
|
|
520
598
|
idempotencyKey: 'abandoned-1',
|
|
@@ -527,6 +605,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
527
605
|
async materialChange => {
|
|
528
606
|
const idempotencyKey = 'pre-signed-post-bind';
|
|
529
607
|
const prepared = await issuer.clients.fullAuth.boost.prepareRefreshableSend({
|
|
608
|
+
statusBinding: 'server',
|
|
530
609
|
recipient: HOLDER_PROFILE_ID,
|
|
531
610
|
template: inlineTemplate(),
|
|
532
611
|
idempotencyKey,
|
|
@@ -535,6 +614,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
535
614
|
{
|
|
536
615
|
...testUnsignedVcV2,
|
|
537
616
|
id: prepared.credentialId,
|
|
617
|
+
credentialStatus: prepared.statusEntries,
|
|
538
618
|
issuer: issuer.learnCard.id.did(),
|
|
539
619
|
validFrom: '2026-01-01T00:00:00Z',
|
|
540
620
|
credentialSubject: { id: prepared.holderDid },
|
|
@@ -614,7 +694,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
614
694
|
expect(await countNodes('CredentialActivity')).toBe(1);
|
|
615
695
|
});
|
|
616
696
|
|
|
617
|
-
it.each(['
|
|
697
|
+
it.each(['integrationId'] as const)(
|
|
618
698
|
'rejects changing %s after preparation without allocating again',
|
|
619
699
|
async field => {
|
|
620
700
|
const input = {
|
|
@@ -642,7 +722,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
642
722
|
}
|
|
643
723
|
);
|
|
644
724
|
|
|
645
|
-
it.each(['
|
|
725
|
+
it.each(['integrationId'] as const)(
|
|
646
726
|
'rejects changing %s after delivery instead of returning the old receipt',
|
|
647
727
|
async field => {
|
|
648
728
|
const input = {
|
|
@@ -676,6 +756,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
676
756
|
const intent = await getRefreshSendIntent(ISSUER_PROFILE_ID, input.idempotencyKey);
|
|
677
757
|
const receipt = first.refresh!;
|
|
678
758
|
const reordered = {
|
|
759
|
+
statusManagement: first.statusManagement,
|
|
679
760
|
refresh: {
|
|
680
761
|
holderDid: receipt.holderDid,
|
|
681
762
|
issuerDid: receipt.issuerDid,
|
|
@@ -1346,10 +1427,257 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
1346
1427
|
|
|
1347
1428
|
expect(result.refresh).toBeDefined();
|
|
1348
1429
|
expect(storedTemplate.refreshService).toBeUndefined();
|
|
1430
|
+
expect(storedTemplate.display).toEqual({});
|
|
1431
|
+
expect(storedTemplate.name).toBe('Credential');
|
|
1432
|
+
expect(storedTemplate.credentialSubject).toEqual({ id: 'did:example:123' });
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
it('rejects reusing a display-only anchor via templateUri without a template, instead of a server error', async () => {
|
|
1436
|
+
const { signed } = await buildHandoff();
|
|
1437
|
+
const created = (await issuer.clients.fullAuth.boost.send({
|
|
1438
|
+
type: 'boost',
|
|
1439
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1440
|
+
signedCredential: signed,
|
|
1441
|
+
refresh: true,
|
|
1442
|
+
})) as SendResult;
|
|
1443
|
+
|
|
1444
|
+
await expect(
|
|
1445
|
+
issuer.clients.fullAuth.boost.send({
|
|
1446
|
+
type: 'boost',
|
|
1447
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1448
|
+
templateUri: created.uri,
|
|
1449
|
+
refresh: true,
|
|
1450
|
+
})
|
|
1451
|
+
).rejects.toMatchObject({
|
|
1452
|
+
code: 'BAD_REQUEST',
|
|
1453
|
+
message: expect.stringContaining(
|
|
1454
|
+
'Display-only anchors require an issuance credential'
|
|
1455
|
+
),
|
|
1456
|
+
});
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('rejects reusing a display-only anchor in an ordinary signing-authority send', async () => {
|
|
1460
|
+
const { signed } = await buildHandoff();
|
|
1461
|
+
const created = (await issuer.clients.fullAuth.boost.send({
|
|
1462
|
+
type: 'boost',
|
|
1463
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1464
|
+
signedCredential: signed,
|
|
1465
|
+
refresh: true,
|
|
1466
|
+
})) as SendResult;
|
|
1467
|
+
|
|
1468
|
+
await expect(
|
|
1469
|
+
issuer.clients.fullAuth.boost.send({
|
|
1470
|
+
type: 'boost',
|
|
1471
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1472
|
+
templateUri: created.uri,
|
|
1473
|
+
})
|
|
1474
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
1349
1475
|
});
|
|
1350
1476
|
});
|
|
1351
1477
|
|
|
1352
1478
|
describe('normal sends are unchanged', () => {
|
|
1479
|
+
it.each([undefined, 'private-display-key'])(
|
|
1480
|
+
'stores only public display for inline refresh (%s)',
|
|
1481
|
+
async idempotencyKey => {
|
|
1482
|
+
const display = {
|
|
1483
|
+
name: 'Public credential',
|
|
1484
|
+
category: 'Achievement',
|
|
1485
|
+
image: 'https://example.com/icon.png',
|
|
1486
|
+
};
|
|
1487
|
+
const result = await issuer.clients.fullAuth.boost.send({
|
|
1488
|
+
type: 'boost',
|
|
1489
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1490
|
+
template: { credential: testUnsignedBoost, name: 'PRIVATE TEMPLATE TITLE' },
|
|
1491
|
+
display,
|
|
1492
|
+
refresh: true,
|
|
1493
|
+
idempotencyKey,
|
|
1494
|
+
});
|
|
1495
|
+
const stored = await issuer.clients.fullAuth.boost.getBoost({ uri: result.uri });
|
|
1496
|
+
expect(stored.name).toBe(display.name);
|
|
1497
|
+
expect(stored.boost).toEqual({
|
|
1498
|
+
'@context': ['https://www.w3.org/ns/credentials/v2'],
|
|
1499
|
+
type: ['VerifiableCredential'],
|
|
1500
|
+
issuer: getDidWeb(DOMAIN, ISSUER_PROFILE_ID),
|
|
1501
|
+
credentialSubject: { id: 'did:example:123' },
|
|
1502
|
+
name: display.name,
|
|
1503
|
+
display,
|
|
1504
|
+
displayOnlyAnchor: true,
|
|
1505
|
+
});
|
|
1506
|
+
expect(JSON.stringify(stored)).not.toContain('PRIVATE TEMPLATE TITLE');
|
|
1507
|
+
const signedInput = signingAuthorityMocks.issueCredential.mock.calls[0]?.[1];
|
|
1508
|
+
expect(signedInput.name).toBe(testUnsignedBoost.name);
|
|
1509
|
+
}
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
it.each([undefined, 'omit-display-fallback-key'])(
|
|
1513
|
+
'ignores the inline template name but copies its category when display is omitted (%s)',
|
|
1514
|
+
async idempotencyKey => {
|
|
1515
|
+
const result = await issuer.clients.fullAuth.boost.send({
|
|
1516
|
+
type: 'boost',
|
|
1517
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1518
|
+
template: {
|
|
1519
|
+
credential: testUnsignedBoost,
|
|
1520
|
+
name: 'Private Template Name',
|
|
1521
|
+
category: 'TemplateCategory',
|
|
1522
|
+
},
|
|
1523
|
+
refresh: true,
|
|
1524
|
+
idempotencyKey,
|
|
1525
|
+
});
|
|
1526
|
+
const stored = await issuer.clients.fullAuth.boost.getBoost({ uri: result.uri });
|
|
1527
|
+
expect(stored.name).toBe('Credential');
|
|
1528
|
+
// Unlike name/image, category is copied from the template: consent-flow
|
|
1529
|
+
// write authorization keys off a Boost's category and treats an absent
|
|
1530
|
+
// one as unrestricted, so dropping it would let an unconsented category
|
|
1531
|
+
// through as an unrestricted write merely by omitting `display`.
|
|
1532
|
+
expect(stored.category).toBe('TemplateCategory');
|
|
1533
|
+
expect(stored.boost).toEqual({
|
|
1534
|
+
'@context': ['https://www.w3.org/ns/credentials/v2'],
|
|
1535
|
+
type: ['VerifiableCredential'],
|
|
1536
|
+
issuer: getDidWeb(DOMAIN, ISSUER_PROFILE_ID),
|
|
1537
|
+
credentialSubject: { id: 'did:example:123' },
|
|
1538
|
+
name: 'Credential',
|
|
1539
|
+
display: { category: 'TemplateCategory' },
|
|
1540
|
+
displayOnlyAnchor: true,
|
|
1541
|
+
});
|
|
1542
|
+
expect(JSON.stringify(stored)).not.toContain('Private Template Name');
|
|
1543
|
+
}
|
|
1544
|
+
);
|
|
1545
|
+
|
|
1546
|
+
it.each([undefined, 'category-not-consented-key'])(
|
|
1547
|
+
'never records a contract-flow write for an inline template category the recipient did not consent to (%s)',
|
|
1548
|
+
async idempotencyKey => {
|
|
1549
|
+
const contractUri =
|
|
1550
|
+
await issuer.clients.fullAuth.contracts.createConsentFlowContract({
|
|
1551
|
+
contract: {
|
|
1552
|
+
read: { anonymize: false },
|
|
1553
|
+
write: {
|
|
1554
|
+
credentials: {
|
|
1555
|
+
categories: {
|
|
1556
|
+
AllowedCategory: { required: false },
|
|
1557
|
+
RestrictedCategory: { required: false },
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
},
|
|
1561
|
+
},
|
|
1562
|
+
name: 'Category Enforcement Contract',
|
|
1563
|
+
writers: [ISSUER_PROFILE_ID],
|
|
1564
|
+
});
|
|
1565
|
+
await holder.clients.fullAuth.contracts.consentToContract({
|
|
1566
|
+
contractUri,
|
|
1567
|
+
terms: {
|
|
1568
|
+
read: { anonymize: false },
|
|
1569
|
+
write: {
|
|
1570
|
+
credentials: {
|
|
1571
|
+
categories: { AllowedCategory: true, RestrictedCategory: false },
|
|
1572
|
+
},
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
const result = await issuer.clients.fullAuth.boost.send({
|
|
1578
|
+
type: 'boost',
|
|
1579
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1580
|
+
template: { credential: testUnsignedBoost, category: 'RestrictedCategory' },
|
|
1581
|
+
contractUri,
|
|
1582
|
+
refresh: true,
|
|
1583
|
+
idempotencyKey,
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
const stored = await issuer.clients.fullAuth.boost.getBoost({ uri: result.uri });
|
|
1587
|
+
// The category must reach the anchor for the consent check below to see
|
|
1588
|
+
// it at all; this is what the previous test already locks in.
|
|
1589
|
+
expect(stored.category).toBe('RestrictedCategory');
|
|
1590
|
+
|
|
1591
|
+
const transactions = await runQuery(
|
|
1592
|
+
`MATCH (root:Credential {refreshVersionKey: $key})-[:ISSUED_VIA_TRANSACTION]->(:ConsentFlowTransaction)-[:IS_FOR]->(:ConsentFlowTerms)
|
|
1593
|
+
RETURN count(*) AS count`,
|
|
1594
|
+
{ key: `${result.refresh!.refreshId}:1` }
|
|
1595
|
+
);
|
|
1596
|
+
expect(toNum(transactions.records[0].get('count'))).toBe(0);
|
|
1597
|
+
}
|
|
1598
|
+
);
|
|
1599
|
+
|
|
1600
|
+
it.each([undefined, 'category-consented-key'])(
|
|
1601
|
+
'records a contract-flow write for an inline template category the recipient consented to (%s)',
|
|
1602
|
+
async idempotencyKey => {
|
|
1603
|
+
const contractUri =
|
|
1604
|
+
await issuer.clients.fullAuth.contracts.createConsentFlowContract({
|
|
1605
|
+
contract: {
|
|
1606
|
+
read: { anonymize: false },
|
|
1607
|
+
write: {
|
|
1608
|
+
credentials: {
|
|
1609
|
+
categories: { AllowedCategory: { required: false } },
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1613
|
+
name: 'Category Enforcement Allowed Contract',
|
|
1614
|
+
writers: [ISSUER_PROFILE_ID],
|
|
1615
|
+
});
|
|
1616
|
+
await holder.clients.fullAuth.contracts.consentToContract({
|
|
1617
|
+
contractUri,
|
|
1618
|
+
terms: {
|
|
1619
|
+
read: { anonymize: false },
|
|
1620
|
+
write: { credentials: { categories: { AllowedCategory: true } } },
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
const result = await issuer.clients.fullAuth.boost.send({
|
|
1625
|
+
type: 'boost',
|
|
1626
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1627
|
+
template: { credential: testUnsignedBoost, category: 'AllowedCategory' },
|
|
1628
|
+
contractUri,
|
|
1629
|
+
refresh: true,
|
|
1630
|
+
idempotencyKey,
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
const transactions = await runQuery(
|
|
1634
|
+
`MATCH (root:Credential {refreshVersionKey: $key})-[:ISSUED_VIA_TRANSACTION]->(:ConsentFlowTransaction)-[:IS_FOR]->(:ConsentFlowTerms)
|
|
1635
|
+
RETURN count(*) AS count`,
|
|
1636
|
+
{ key: `${result.refresh!.refreshId}:1` }
|
|
1637
|
+
);
|
|
1638
|
+
expect(toNum(transactions.records[0].get('count'))).toBe(1);
|
|
1639
|
+
}
|
|
1640
|
+
);
|
|
1641
|
+
|
|
1642
|
+
it('allocates a suspension status entry for a delegated signing-authority send', async () => {
|
|
1643
|
+
const result = (await issuer.clients.fullAuth.boost.send({
|
|
1644
|
+
type: 'boost',
|
|
1645
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1646
|
+
template: { credential: testUnsignedBoost },
|
|
1647
|
+
refresh: true,
|
|
1648
|
+
statusPurposes: ['suspension'],
|
|
1649
|
+
})) as SendResult;
|
|
1650
|
+
|
|
1651
|
+
expect(result.refresh!.credentialStatus).toEqual(
|
|
1652
|
+
expect.arrayContaining([
|
|
1653
|
+
expect.objectContaining({ statusPurpose: 'revocation' }),
|
|
1654
|
+
expect.objectContaining({ statusPurpose: 'suspension' }),
|
|
1655
|
+
])
|
|
1656
|
+
);
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
it('sends an ordinary reusable Boost whose credential has its own display object, not a display-only anchor', async () => {
|
|
1660
|
+
// Real app-created BoostCredentials have their own top-level `display`
|
|
1661
|
+
// object (appearance fields like backgroundColor); this must never be
|
|
1662
|
+
// mistaken for the display-only anchor marker.
|
|
1663
|
+
const boostUri = await issuer.clients.fullAuth.boost.createBoost({
|
|
1664
|
+
credential: {
|
|
1665
|
+
...testUnsignedBoost,
|
|
1666
|
+
display: { backgroundColor: '#fff', backgroundImage: '' },
|
|
1667
|
+
},
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
const result = (await issuer.clients.fullAuth.boost.send({
|
|
1671
|
+
type: 'boost',
|
|
1672
|
+
recipient: HOLDER_PROFILE_ID,
|
|
1673
|
+
templateUri: boostUri,
|
|
1674
|
+
refresh: true,
|
|
1675
|
+
})) as SendResult;
|
|
1676
|
+
|
|
1677
|
+
expect(result.refresh).toBeDefined();
|
|
1678
|
+
expect(result.credentialUri).toBeDefined();
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1353
1681
|
it('returns no refresh receipt when refresh is omitted', async () => {
|
|
1354
1682
|
const boostUri = await issuer.clients.fullAuth.boost.createBoost({
|
|
1355
1683
|
credential: testUnsignedBoost,
|
|
@@ -1723,6 +2051,7 @@ describe('Unified send with managed refresh (LC-2198)', () => {
|
|
|
1723
2051
|
type: ['VerifiableCredential'],
|
|
1724
2052
|
issuer: receipt.issuerDid,
|
|
1725
2053
|
name: 'SA Updated Badge',
|
|
2054
|
+
credentialStatus: receipt.credentialStatus,
|
|
1726
2055
|
credentialSubject: { id: receipt.holderDid },
|
|
1727
2056
|
refreshService: receipt.refreshService,
|
|
1728
2057
|
validFrom: new Date().toISOString(),
|
|
@@ -192,7 +192,7 @@ describe('SmartResume consent before publication', () => {
|
|
|
192
192
|
upload.mockRejectedValueOnce(new Error('Synthetic outage'));
|
|
193
193
|
await expect(accept({ oneTime: true })).rejects.toMatchObject({ code: 'BAD_GATEWAY' });
|
|
194
194
|
const result = await accept({ oneTime: true });
|
|
195
|
-
expect((await savedTerms())?.status).toBe('
|
|
195
|
+
expect((await savedTerms())?.status).toBe('consumed');
|
|
196
196
|
expect(
|
|
197
197
|
(
|
|
198
198
|
await learner.clients.fullAuth.contracts.getTermsTransactionHistory({
|
|
@@ -30,6 +30,15 @@ const script: VerificationScript = async v => {
|
|
|
30
30
|
.rejects.toThrow(/Campaign administrator access required/);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
await v.step(null, 'campaign-dispatch-authorization', async () => {
|
|
34
|
+
await v
|
|
35
|
+
.expect(learner.invoke.campaignsSendTestToMe(campaignId))
|
|
36
|
+
.rejects.toThrow(/Campaign administrator access required/);
|
|
37
|
+
await v
|
|
38
|
+
.expect(learner.invoke.campaignsDispatch(campaignId))
|
|
39
|
+
.rejects.toThrow(/Campaign administrator access required/);
|
|
40
|
+
});
|
|
41
|
+
|
|
33
42
|
await v.step(null, 'campaign-sdk-user-isolation', async () => {
|
|
34
43
|
v.expect(await learner.invoke.getMyCampaigns()).toEqual([]);
|
|
35
44
|
v.expect(await other.invoke.getMyCampaigns()).toEqual([]);
|
|
@@ -39,6 +48,37 @@ const script: VerificationScript = async v => {
|
|
|
39
48
|
.rejects.toThrow(/Campaign not found/);
|
|
40
49
|
});
|
|
41
50
|
|
|
51
|
+
const page = await v.newPage('announcement-preferences', { signInAs: v.actors.learner });
|
|
52
|
+
await v.step(page, 'announcement-preferences-ui-persistence', async () => {
|
|
53
|
+
await page.goto('/privacy-and-data');
|
|
54
|
+
const email = page.getByRole('switch', { name: 'Email', exact: true });
|
|
55
|
+
const push = page.getByRole('switch', { name: 'Push notifications', exact: true });
|
|
56
|
+
await v.expect(email).toHaveAttribute('aria-checked', 'true');
|
|
57
|
+
await email.click();
|
|
58
|
+
await v.expect(email).toHaveAttribute('aria-checked', 'false');
|
|
59
|
+
await push.click();
|
|
60
|
+
await v.expect(push).toHaveAttribute('aria-checked', 'false');
|
|
61
|
+
await page.reload();
|
|
62
|
+
await v.expect(email).toHaveAttribute('aria-checked', 'false');
|
|
63
|
+
await v.expect(push).toHaveAttribute('aria-checked', 'false');
|
|
64
|
+
v.expect(await learner.invoke.getAnnouncementPreferences()).toEqual({
|
|
65
|
+
announcementEmailOptOut: true,
|
|
66
|
+
announcementPushOptOut: true,
|
|
67
|
+
});
|
|
68
|
+
v.expect(await other.invoke.getAnnouncementPreferences()).toEqual({
|
|
69
|
+
announcementEmailOptOut: false,
|
|
70
|
+
announcementPushOptOut: false,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
const anonymous = await v.newPage('unsubscribe-public');
|
|
74
|
+
await v.step(anonymous, 'unsubscribe-public-error-retry', async () => {
|
|
75
|
+
await anonymous.goto('/unsubscribe?token=invalid');
|
|
76
|
+
await v.expect(anonymous.getByRole('alert')).toBeVisible();
|
|
77
|
+
await anonymous.getByRole('button', { name: 'Try Again' }).click();
|
|
78
|
+
await v.expect(anonymous.getByRole('alert')).toBeVisible();
|
|
79
|
+
await v.expect(anonymous).toHaveURL(/\/unsubscribe\?token=invalid/);
|
|
80
|
+
});
|
|
81
|
+
|
|
42
82
|
return {
|
|
43
83
|
authorization: 'passed',
|
|
44
84
|
userIsolation: 'passed',
|
|
@@ -11,9 +11,11 @@ export default createVitestConfig(serviceIntegrationPreset, {
|
|
|
11
11
|
alias: { '@instance': require.resolve('./test/helpers/mock-instance.ts') },
|
|
12
12
|
env: {
|
|
13
13
|
IS_E2E_TEST: 'true',
|
|
14
|
+
IS_OFFLINE: 'true',
|
|
14
15
|
DOMAIN_NAME: 'localhost%3A3000',
|
|
15
16
|
SEED: 'a'.repeat(64),
|
|
16
17
|
CAMPAIGN_ADMIN_PROFILE_IDS: 'campaign-admin',
|
|
18
|
+
CAMPAIGN_UNSUBSCRIBE_SECRET: 'campaign-test-only-secret-at-least-32-bytes',
|
|
17
19
|
TRACE_CONSOLE: 'false',
|
|
18
20
|
NEO4J_SKIP_INDICES: 'true',
|
|
19
21
|
},
|