@learncard/network-brain-service 4.1.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/CHANGELOG.md +164 -0
- package/README.md +27 -1
- package/bootstrapLambda.test.ts +70 -32
- package/config/config.dev.json +1 -0
- package/config/config.example.json +2 -1
- package/config/config.production.json +1 -0
- package/config/config.scouts.dev.json +1 -0
- package/config/config.scouts.production.json +1 -0
- package/consentFlowDeadlineLambda.test.ts +108 -0
- package/consentFlowDeadlineLambda.ts +18 -0
- package/consentFlowDeadlineLambdaApp.ts +14 -0
- package/dist/accesslayer/boost/create.d.ts +8 -2
- package/dist/accesslayer/boost/create.d.ts.map +1 -1
- package/dist/accesslayer/campaign/audience.d.ts +4 -4
- package/dist/accesslayer/campaign/audience.d.ts.map +1 -1
- package/dist/accesslayer/campaign/create.d.ts +1 -1
- package/dist/accesslayer/campaign/create.d.ts.map +1 -1
- package/dist/accesslayer/campaign/delivery.d.ts +5 -0
- package/dist/accesslayer/campaign/delivery.d.ts.map +1 -0
- package/dist/accesslayer/campaign/dispatch.d.ts +5 -0
- package/dist/accesslayer/campaign/dispatch.d.ts.map +1 -0
- package/dist/accesslayer/campaign/dispatch.test.d.ts +2 -0
- package/dist/accesslayer/campaign/dispatch.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/email.d.ts +10 -0
- package/dist/accesslayer/campaign/email.d.ts.map +1 -0
- package/dist/accesslayer/campaign/email.test.d.ts +2 -0
- package/dist/accesslayer/campaign/email.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/preferences.d.ts +5 -0
- package/dist/accesslayer/campaign/preferences.d.ts.map +1 -0
- package/dist/accesslayer/campaign/read.d.ts.map +1 -1
- package/dist/accesslayer/campaign/test.d.ts +5 -0
- package/dist/accesslayer/campaign/test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/test.test.d.ts +2 -0
- package/dist/accesslayer/campaign/test.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/update.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/delete.d.ts +1 -1
- package/dist/accesslayer/consentflowcontract/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +2 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +19 -4
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +6 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/versions.d.ts +68 -0
- package/dist/accesslayer/consentflowcontract/versions.d.ts.map +1 -0
- package/dist/accesslayer/credential/create.d.ts +3 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/delete.d.ts +1 -0
- package/dist/accesslayer/credential/delete.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +15 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/read.d.ts +2 -0
- package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/create.d.ts +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +7 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -1
- package/dist/app.d.ts +11 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +61 -1
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/campaign-unsubscribe.d.ts +7 -0
- package/dist/campaign-unsubscribe.d.ts.map +1 -0
- package/dist/campaign-unsubscribe.test.d.ts +2 -0
- package/dist/campaign-unsubscribe.test.d.ts.map +1 -0
- package/dist/config/environment.d.ts +42 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/stageConfig.d.ts +4 -0
- package/dist/config/stageConfig.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/boost.helpers.d.ts +4 -1
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/campaign-app-origin.helpers.d.ts +2 -0
- package/dist/helpers/campaign-app-origin.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-email.helpers.d.ts +13 -0
- package/dist/helpers/campaign-email.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-notifications.helpers.d.ts +7 -0
- package/dist/helpers/campaign-notifications.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-queue.helpers.d.ts +7 -0
- package/dist/helpers/campaign-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/campaign-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/campaign-unsubscribe-page.d.ts +2 -0
- package/dist/helpers/campaign-unsubscribe-page.d.ts.map +1 -0
- package/dist/helpers/campaign-unsubscribe.helpers.d.ts +7 -0
- package/dist/helpers/campaign-unsubscribe.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-webhook.helpers.test.d.ts +2 -0
- package/dist/helpers/campaign-webhook.helpers.test.d.ts.map +1 -0
- package/dist/helpers/consentflow-deadline.helpers.d.ts +5 -0
- package/dist/helpers/consentflow-deadline.helpers.d.ts.map +1 -0
- package/dist/helpers/consentflow-legacy-migration.helpers.d.ts +2 -0
- package/dist/helpers/consentflow-legacy-migration.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -1
- package/dist/helpers/contract.helpers.d.ts +1 -1
- package/dist/helpers/contract.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-display.helpers.d.ts +5 -0
- package/dist/helpers/credential-display.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-display.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-display.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-refresh-materiality.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-materiality.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-refresh.helpers.d.ts +30 -1
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-status-mutation.helpers.d.ts +5 -0
- package/dist/helpers/credential-status-mutation.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-status-mutation.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-status-mutation.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential.helpers.d.ts +1 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-refresh.helpers.d.ts +4 -2
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts +1 -0
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/learnCard.helpers.d.ts +1 -0
- package/dist/helpers/learnCard.helpers.d.ts.map +1 -1
- package/dist/helpers/managed-status.helpers.d.ts +7 -0
- package/dist/helpers/managed-status.helpers.d.ts.map +1 -0
- package/dist/helpers/managed-status.helpers.test.d.ts +2 -0
- package/dist/helpers/managed-status.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts +2 -0
- package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notification-webhook-fetch.helpers.d.ts +13 -0
- package/dist/helpers/notification-webhook-fetch.helpers.d.ts.map +1 -0
- package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts +2 -0
- package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +13 -5
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.test.d.ts +2 -0
- package/dist/helpers/refresh-send-intent.helpers.test.d.ts.map +1 -0
- package/dist/helpers/send-contract.helpers.d.ts +11 -0
- package/dist/helpers/send-contract.helpers.d.ts.map +1 -0
- package/dist/helpers/status-list.helpers.d.ts +12 -1
- package/dist/helpers/status-list.helpers.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowContractVersion.d.ts +12 -0
- package/dist/models/ConsentFlowContractVersion.d.ts.map +1 -0
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/Credential.d.ts +2 -0
- package/dist/models/Credential.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -0
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +1 -1
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/campaign-constraints.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/routes/activity.d.ts +15 -5
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts +15 -5
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +12 -2
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/bench.d.ts +11 -1
- package/dist/routes/bench.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +70 -15
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/campaigns.d.ts +183 -51
- package/dist/routes/campaigns.d.ts.map +1 -1
- package/dist/routes/claim-hooks.d.ts +11 -1
- package/dist/routes/claim-hooks.d.ts.map +1 -1
- package/dist/routes/contact-methods.d.ts +11 -1
- package/dist/routes/contact-methods.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +374 -8
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-input-privacy.test.d.ts +2 -0
- package/dist/routes/credential-input-privacy.test.d.ts.map +1 -0
- package/dist/routes/credential-refreshes.d.ts +210 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credential-share-previews.d.ts +11 -1
- package/dist/routes/credential-share-previews.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +132 -3
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/did-metadata.d.ts +11 -1
- package/dist/routes/did-metadata.d.ts.map +1 -1
- package/dist/routes/federation.d.ts +11 -1
- package/dist/routes/federation.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +54 -19
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +100 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/integrations.d.ts +14 -4
- package/dist/routes/integrations.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +11 -1
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts +11 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts +11 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +22 -2
- package/dist/routes/public-share-links.d.ts.map +1 -1
- package/dist/routes/share-links.d.ts +36 -16
- package/dist/routes/share-links.d.ts.map +1 -1
- package/dist/routes/skill-frameworks.d.ts +11 -1
- package/dist/routes/skill-frameworks.d.ts.map +1 -1
- package/dist/routes/skills.d.ts +11 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/storage.d.ts +37 -23
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/test.d.ts +79 -3
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/utilities.d.ts +11 -1
- package/dist/routes/utilities.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts +11 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts +3 -0
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/delivery.service.d.ts +10 -0
- package/dist/services/delivery/delivery.service.d.ts.map +1 -1
- package/dist/types/activity.d.ts +3 -3
- package/dist/types/app-manifest.d.ts +10 -10
- package/dist/types/campaign.d.ts +13 -1
- package/dist/types/campaign.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +175 -7
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +27 -0
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/credential.d.ts +5 -0
- package/dist/types/credential.d.ts.map +1 -1
- package/dist/types/integration.d.ts +3 -3
- package/lambda.ts +44 -14
- package/lambdaApp.ts +20 -0
- package/package.json +20 -19
- package/project.json +5 -0
- package/scripts/migrate-consent-flow-versions.ts +9 -0
- package/scripts/report-send-template-exposure.ts +47 -0
- package/scripts/verify-python-contracts.ts +16 -1
- package/serverless-local.yml +15 -2
- package/serverless.function-env.cjs +4 -0
- package/serverless.local.yml +12 -0
- package/serverless.yml +48 -3
- package/src/accesslayer/boost/create.ts +62 -7
- package/src/accesslayer/campaign/README.md +275 -6
- package/src/accesslayer/campaign/audience.ts +39 -7
- package/src/accesslayer/campaign/create.ts +4 -2
- package/src/accesslayer/campaign/delivery.ts +66 -0
- package/src/accesslayer/campaign/dispatch.test.ts +59 -0
- package/src/accesslayer/campaign/dispatch.ts +172 -0
- package/src/accesslayer/campaign/email.test.ts +73 -0
- package/src/accesslayer/campaign/email.ts +91 -0
- package/src/accesslayer/campaign/preferences.ts +53 -0
- package/src/accesslayer/campaign/read.ts +3 -1
- package/src/accesslayer/campaign/state.ts +1 -1
- package/src/accesslayer/campaign/test.test.ts +31 -0
- package/src/accesslayer/campaign/test.ts +75 -0
- package/src/accesslayer/campaign/update.ts +15 -4
- package/src/accesslayer/consentflowcontract/create.ts +20 -1
- package/src/accesslayer/consentflowcontract/delete.ts +121 -3
- package/src/accesslayer/consentflowcontract/read.ts +15 -9
- package/src/accesslayer/consentflowcontract/relationships/create.ts +70 -36
- package/src/accesslayer/consentflowcontract/relationships/read.ts +305 -73
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +1 -1
- package/src/accesslayer/consentflowcontract/relationships/update.ts +269 -98
- package/src/accesslayer/consentflowcontract/versions.ts +790 -0
- package/src/accesslayer/credential/create.ts +33 -9
- package/src/accesslayer/credential/delete.ts +45 -0
- package/src/accesslayer/credential/relationships/create.ts +102 -47
- package/src/accesslayer/credential/relationships/update.ts +8 -116
- package/src/accesslayer/credential-refresh/read.ts +26 -2
- package/src/accesslayer/credential-refresh/update.ts +6 -2
- package/src/accesslayer/inbox-credential/create.ts +7 -1
- package/src/accesslayer/inbox-credential/refresh.ts +14 -4
- package/src/campaign-unsubscribe.test.ts +158 -0
- package/src/campaign-unsubscribe.ts +86 -0
- package/src/config/environment.test.ts +52 -0
- package/src/config/environment.ts +58 -0
- package/src/config/serverlessFunctionEnv.test.ts +59 -0
- package/src/credential-refresh.ts +1 -0
- package/src/docker-entry.ts +26 -1
- package/src/helpers/boost-storage.helpers.test.ts +107 -41
- package/src/helpers/boost.helpers.ts +88 -40
- package/src/helpers/campaign-app-origin.helpers.ts +25 -0
- package/src/helpers/campaign-email.helpers.ts +113 -0
- package/src/helpers/campaign-notifications.helpers.ts +127 -0
- package/src/helpers/campaign-queue.helpers.test.ts +442 -0
- package/src/helpers/campaign-queue.helpers.ts +122 -0
- package/src/helpers/campaign-unsubscribe-page.ts +107 -0
- package/src/helpers/campaign-unsubscribe.helpers.ts +56 -0
- package/src/helpers/campaign-webhook.helpers.test.ts +137 -0
- package/src/helpers/consentflow-deadline.helpers.ts +22 -0
- package/src/helpers/consentflow-legacy-migration.helpers.ts +179 -0
- package/src/helpers/contract-events.helpers.ts +35 -3
- package/src/helpers/contract-requests.helpers.ts +1 -1
- package/src/helpers/contract.helpers.ts +5 -1
- package/src/helpers/credential-display.helpers.test.ts +68 -0
- package/src/helpers/credential-display.helpers.ts +15 -0
- package/src/helpers/credential-refresh-email.helpers.ts +5 -12
- package/src/helpers/credential-refresh-materiality.helpers.test.ts +138 -1
- package/src/helpers/credential-refresh-materiality.helpers.ts +62 -1
- package/src/helpers/credential-refresh-proof.helpers.test.ts +1 -1
- package/src/helpers/credential-refresh-proof.helpers.ts +2 -2
- package/src/helpers/credential-refresh.helpers.ts +1166 -376
- package/src/helpers/credential-status-mutation.helpers.test.ts +155 -0
- package/src/helpers/credential-status-mutation.helpers.ts +166 -0
- package/src/helpers/credential.helpers.ts +19 -11
- package/src/helpers/finalize-inbox.helpers.ts +1 -1
- package/src/helpers/inbox-refresh.helpers.ts +34 -8
- package/src/helpers/inbox.helpers.ts +2 -0
- package/src/helpers/learnCard.helpers.ts +23 -0
- package/src/helpers/managed-status.helpers.test.ts +101 -0
- package/src/helpers/managed-status.helpers.ts +147 -0
- package/src/helpers/notification-delivery-boundary.helpers.test.ts +114 -0
- package/src/helpers/notification-webhook-fetch.helpers.test.ts +228 -0
- package/src/helpers/notification-webhook-fetch.helpers.ts +166 -0
- package/src/helpers/notificationQueue.helpers.ts +5 -0
- package/src/helpers/notifications.helpers.ts +19 -12
- package/src/helpers/refresh-send-intent.helpers.test.ts +60 -0
- package/src/helpers/refresh-send-intent.helpers.ts +81 -18
- package/src/helpers/send-contract.helpers.ts +36 -0
- package/src/helpers/status-list.helpers.test.ts +29 -12
- package/src/helpers/status-list.helpers.ts +329 -20
- package/src/models/ConsentFlowContract.ts +3 -0
- package/src/models/ConsentFlowContractVersion.ts +50 -0
- package/src/models/ConsentFlowTerms.ts +4 -0
- package/src/models/Credential.ts +2 -0
- package/src/models/CredentialRefresh.ts +2 -0
- package/src/models/Profile.ts +2 -0
- package/src/models/campaign-constraints.ts +13 -8
- package/src/models/index.ts +2 -0
- package/src/openapi.ts +21 -0
- package/src/routes/app-store.ts +2 -1
- package/src/routes/boosts.ts +219 -137
- package/src/routes/campaigns.ts +98 -19
- package/src/routes/contracts.ts +433 -115
- package/src/routes/credential-input-privacy.test.ts +92 -0
- package/src/routes/credential-refreshes.ts +273 -2
- package/src/routes/federation.ts +7 -2
- package/src/routes/inbox.ts +6 -1
- package/src/routes/index.ts +31 -2
- package/src/routes/workflows.ts +61 -46
- package/src/services/delivery/adapters/postmark.adapter.test.ts +70 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +84 -11
- package/src/services/delivery/delivery.service.ts +4 -0
- package/src/types/campaign.ts +14 -1
- package/src/types/consentflowcontract.ts +43 -2
- package/src/types/credential-refresh.ts +11 -1
- package/src/types/credential.ts +2 -0
- package/test/boost-status-cleanup.spec.ts +123 -0
- package/test/campaigns-email.unit.spec.ts +268 -0
- package/test/campaigns.spec.ts +804 -30
- package/test/consent-autoboost-idempotency.spec.ts +432 -0
- package/test/consent-data-boundaries.spec.ts +322 -26
- package/test/consent-legacy-migration.spec.ts +315 -0
- package/test/consent-recipients.spec.ts +7 -7
- package/test/consent-version-updates.spec.ts +761 -0
- package/test/consentflow.spec.ts +698 -226
- package/test/contract-requests.spec.ts +55 -9
- package/test/credential-refresh-allocation.spec.ts +212 -14
- package/test/credential-refresh-email.spec.ts +6 -10
- package/test/credential-refresh-endpoint.spec.ts +3 -0
- package/test/credential-refresh-publication.spec.ts +52 -15
- package/test/credential-storage.neo4j.spec.ts +33 -6
- package/test/helpers/legacy-autoboost.ts +48 -0
- package/test/holder-mode-send.spec.ts +710 -0
- package/test/inbox-refresh.spec.ts +105 -0
- package/test/inbox-security.e2e.ts +18 -22
- package/test/managed-status-revocation.spec.ts +970 -0
- package/test/notifications-worker.spec.ts +31 -25
- package/test/refresh-send.spec.ts +334 -5
- package/test/smart-resume-consent.spec.ts +1 -1
- package/test/verification/campaigns.ts +40 -0
- package/vitest.campaigns.config.ts +2 -0
- package/vitest.config.ts +14 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as notificationTransport from '@helpers/notification-webhook-fetch.helpers';
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
3
|
import { createServer } from 'node:http';
|
|
3
4
|
import * as runtime from '@environment';
|
|
@@ -122,6 +123,46 @@ describe('generic contract requests and correlated events', () => {
|
|
|
122
123
|
} as unknown as Awaited<ReturnType<typeof learnCardHelpers.getDidWebLearnCard>>);
|
|
123
124
|
};
|
|
124
125
|
|
|
126
|
+
it('delivers required-update alert copy without exposing the owner’s private profile fields', async () => {
|
|
127
|
+
const uri = await create();
|
|
128
|
+
await accept(uri);
|
|
129
|
+
await neogma.queryRunner.run(
|
|
130
|
+
`MATCH (owner:Profile {profileId:$profileId})
|
|
131
|
+
SET owner.email = 'owner-private@example.com', owner.dob = '1990-01-01',
|
|
132
|
+
owner.country = 'US', owner.notificationsWebhook = 'https://owner.example/private-hook'`,
|
|
133
|
+
{ profileId: owner.profileId }
|
|
134
|
+
);
|
|
135
|
+
delivered = [];
|
|
136
|
+
const reason = 'We need transcripts to personalize your progress report.';
|
|
137
|
+
const result = await owner.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
138
|
+
uri,
|
|
139
|
+
changes: {
|
|
140
|
+
contract: {
|
|
141
|
+
read: {
|
|
142
|
+
credentials: {
|
|
143
|
+
categories: {
|
|
144
|
+
...normalContract.read.credentials.categories,
|
|
145
|
+
Transcript: { required: true },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
reason,
|
|
152
|
+
});
|
|
153
|
+
await flush(uri);
|
|
154
|
+
const notice = delivered.find(
|
|
155
|
+
notification =>
|
|
156
|
+
notification.type === 'CONSENT_FLOW_CONTRACT_UPDATED' &&
|
|
157
|
+
notification.to.profileId === learner.profileId
|
|
158
|
+
);
|
|
159
|
+
expect(notice?.message).toEqual({ title: result.preview.headline, body: reason });
|
|
160
|
+
expect(notice?.from.profileId).toBe(owner.profileId);
|
|
161
|
+
for (const key of ['email', 'dob', 'country', 'notificationsWebhook']) {
|
|
162
|
+
expect(notice!.from).not.toHaveProperty(key);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
125
166
|
it.each(['owner', 'writer', 'recipient'] as const)(
|
|
126
167
|
'allows %s requests without granting writer data access',
|
|
127
168
|
async role => {
|
|
@@ -859,12 +900,17 @@ describe('generic contract requests and correlated events', () => {
|
|
|
859
900
|
const history = await learner.clients.fullAuth.contracts.getTermsTransactionHistory({
|
|
860
901
|
uri: termsUri,
|
|
861
902
|
});
|
|
862
|
-
expect(
|
|
903
|
+
expect(
|
|
904
|
+
history.records.filter(transaction => transaction.action === 'consent')
|
|
905
|
+
).toHaveLength(1);
|
|
906
|
+
expect(
|
|
907
|
+
history.records.filter(transaction => transaction.action === 'reconsent')
|
|
908
|
+
).toHaveLength(1);
|
|
863
909
|
expect(history.records.every(t => t.referral?.requestId === original.requestId)).toBe(true);
|
|
864
910
|
expect(
|
|
865
911
|
delivered.find(n => n.to.profileId === owner.profileId)?.data?.metadata
|
|
866
912
|
).toMatchObject({
|
|
867
|
-
event: '
|
|
913
|
+
event: 'consent_updated',
|
|
868
914
|
requestId: original.requestId,
|
|
869
915
|
externalReferenceId: original.externalReferenceId,
|
|
870
916
|
});
|
|
@@ -939,8 +985,8 @@ describe('generic contract requests and correlated events', () => {
|
|
|
939
985
|
expect((await getContractTermsByUri(termsUri))!.terms.referral).toBeUndefined();
|
|
940
986
|
expect(await status(uri)).toMatchObject({ status: 'pending' });
|
|
941
987
|
delivered.length = 0;
|
|
942
|
-
await learner.clients.fullAuth.contracts.
|
|
943
|
-
|
|
988
|
+
await learner.clients.fullAuth.contracts.consentToContract({
|
|
989
|
+
contractUri: uri,
|
|
944
990
|
terms: normalFullTerms,
|
|
945
991
|
expiresAt: '',
|
|
946
992
|
audienceVersion: 2,
|
|
@@ -1052,7 +1098,7 @@ describe('generic contract requests and correlated events', () => {
|
|
|
1052
1098
|
const uri = await create([]);
|
|
1053
1099
|
useDirectWebhook(webhook);
|
|
1054
1100
|
const fetchSpy = vi
|
|
1055
|
-
.spyOn(
|
|
1101
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
1056
1102
|
.mockResolvedValue(new Response('{}', { status: httpStatus ?? 200 }));
|
|
1057
1103
|
await expect(accept(uri)).resolves.toMatchObject({ termsUri: expect.any(String) });
|
|
1058
1104
|
const row = (await eventRows(uri))[0]!;
|
|
@@ -1075,7 +1121,7 @@ describe('generic contract requests and correlated events', () => {
|
|
|
1075
1121
|
const uri = await create([]);
|
|
1076
1122
|
useDirectWebhook('https://synthetic.example/events');
|
|
1077
1123
|
const fetchSpy = vi
|
|
1078
|
-
.spyOn(
|
|
1124
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
1079
1125
|
.mockResolvedValue(new Response('{"success":false}', { status: httpStatus }));
|
|
1080
1126
|
await accept(uri);
|
|
1081
1127
|
const row = (await eventRows(uri))[0]!;
|
|
@@ -1104,7 +1150,7 @@ describe('generic contract requests and correlated events', () => {
|
|
|
1104
1150
|
vi.mocked(learnCardHelpers.getDidWebLearnCard).mockRejectedValue(
|
|
1105
1151
|
new Error('synthetic signing outage')
|
|
1106
1152
|
);
|
|
1107
|
-
const fetchSpy = vi.spyOn(
|
|
1153
|
+
const fetchSpy = vi.spyOn(notificationTransport, 'fetchNotificationWebhook');
|
|
1108
1154
|
await accept(uri);
|
|
1109
1155
|
const row = (await eventRows(uri))[0]!;
|
|
1110
1156
|
expect(row.get('deliveries')[0].properties.state).toBe('pending');
|
|
@@ -1411,7 +1457,7 @@ describe('generic contract requests and correlated events', () => {
|
|
|
1411
1457
|
const queued = structuredClone(delivered[0]!);
|
|
1412
1458
|
useDirectWebhook('https://synthetic.example/events');
|
|
1413
1459
|
const fetchSpy = vi
|
|
1414
|
-
.spyOn(
|
|
1460
|
+
.spyOn(notificationTransport, 'fetchNotificationWebhook')
|
|
1415
1461
|
.mockResolvedValue(new Response('{}', { status: 403 }));
|
|
1416
1462
|
await expect(deliverQueuedNotification(JSON.stringify(queued))).resolves.toBeUndefined();
|
|
1417
1463
|
await expect(notifications.sendNotification(structuredClone(queued))).resolves.toBe(false);
|
|
@@ -1544,7 +1590,7 @@ describe('generic contract requests and correlated events', () => {
|
|
|
1544
1590
|
vi.spyOn(runtime, 'getNotificationRuntimeEnvironment').mockReturnValue({
|
|
1545
1591
|
...runtime.getNotificationRuntimeEnvironment(),
|
|
1546
1592
|
NODE_ENV: 'development',
|
|
1547
|
-
IS_OFFLINE:
|
|
1593
|
+
IS_OFFLINE: true,
|
|
1548
1594
|
NOTIFICATIONS_QUEUE_URL: undefined,
|
|
1549
1595
|
IS_E2E_TEST: false,
|
|
1550
1596
|
NOTIFICATIONS_SERVICE_WEBHOOK_URL: `http://127.0.0.1:${address.port}/events`,
|
|
@@ -7,7 +7,14 @@ import {
|
|
|
7
7
|
UnsignedVC,
|
|
8
8
|
} from '@learncard/types';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Profile,
|
|
12
|
+
Credential,
|
|
13
|
+
Boost,
|
|
14
|
+
ConsentFlowContract,
|
|
15
|
+
ConsentFlowTerms,
|
|
16
|
+
ConsentFlowTransaction,
|
|
17
|
+
} from '@models';
|
|
11
18
|
import { neogma } from '@instance';
|
|
12
19
|
|
|
13
20
|
import { getClient, getUser } from './helpers/getClient';
|
|
@@ -18,6 +25,12 @@ import { getCredentialByUri } from '@accesslayer/credential/read';
|
|
|
18
25
|
import { getCredentialRefresh, getCredentialRefreshHead } from '@accesslayer/credential-refresh';
|
|
19
26
|
import { getDidWeb } from '@helpers/did.helpers';
|
|
20
27
|
import { testUnsignedBoost } from './helpers/send';
|
|
28
|
+
import { normalContract, normalFullTerms } from './helpers/contract';
|
|
29
|
+
import {
|
|
30
|
+
getContractDetailsByUri,
|
|
31
|
+
getContractTermsForProfile,
|
|
32
|
+
} from '@accesslayer/consentflowcontract/relationships/read';
|
|
33
|
+
import { sendRefreshableCredential } from '@helpers/credential-refresh.helpers';
|
|
21
34
|
|
|
22
35
|
const noAuthClient = getClient();
|
|
23
36
|
|
|
@@ -107,6 +120,60 @@ const allocateAndSign = async (): Promise<{ allocation: AllocationResult; creden
|
|
|
107
120
|
};
|
|
108
121
|
|
|
109
122
|
describe('Credential Refresh Allocation', () => {
|
|
123
|
+
it('returns a content-free receipt to the pending holder, never issuer or outsider', async () => {
|
|
124
|
+
const { allocation, credential } = await allocateAndSign();
|
|
125
|
+
const uri = await issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
126
|
+
refreshId: allocation.refreshId,
|
|
127
|
+
credential,
|
|
128
|
+
});
|
|
129
|
+
expect((await getCredentialRefresh(allocation.refreshId))?.state).toBe('awaiting_claim');
|
|
130
|
+
const receipt = await holder.clients.fullAuth.credentialRefresh.getReceipt({ uri });
|
|
131
|
+
expect(receipt).toEqual({
|
|
132
|
+
refreshId: allocation.refreshId,
|
|
133
|
+
credentialId: credential.id,
|
|
134
|
+
issuerDid: issuer.learnCard.id.did(),
|
|
135
|
+
holderDid: holder.learnCard.id.did(),
|
|
136
|
+
refreshService: allocation.refreshService,
|
|
137
|
+
encryptionMode: 'network',
|
|
138
|
+
});
|
|
139
|
+
await expect(
|
|
140
|
+
issuer.clients.fullAuth.credentialRefresh.getReceipt({ uri })
|
|
141
|
+
).rejects.toThrow();
|
|
142
|
+
await expect(
|
|
143
|
+
outsider.clients.fullAuth.credentialRefresh.getReceipt({ uri })
|
|
144
|
+
).rejects.toThrow();
|
|
145
|
+
await expect(noAuthClient.credentialRefresh.getReceipt({ uri })).rejects.toThrow();
|
|
146
|
+
await expect(
|
|
147
|
+
holder.clients.fullAuth.credentialRefresh.getReceipt({
|
|
148
|
+
uri: uri.replace('localhost%3A3000', 'foreign.example'),
|
|
149
|
+
})
|
|
150
|
+
).rejects.toThrow();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not authorize a receipt after the pending delivery edge is removed', async () => {
|
|
154
|
+
const { allocation, credential } = await allocateAndSign();
|
|
155
|
+
const uri = await issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
156
|
+
refreshId: allocation.refreshId,
|
|
157
|
+
credential,
|
|
158
|
+
});
|
|
159
|
+
await runQuery('MATCH ()-[sent:CREDENTIAL_SENT]->() DELETE sent');
|
|
160
|
+
await expect(
|
|
161
|
+
holder.clients.fullAuth.credentialRefresh.getReceipt({ uri })
|
|
162
|
+
).rejects.toThrow();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('fails closed when a legacy delivery lacks its exact public status binding', async () => {
|
|
166
|
+
const { allocation, credential } = await allocateAndSign();
|
|
167
|
+
const uri = await issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
168
|
+
refreshId: allocation.refreshId,
|
|
169
|
+
credential,
|
|
170
|
+
});
|
|
171
|
+
await runQuery('MATCH (r:CredentialRefresh) REMOVE r.credentialStatusBinding');
|
|
172
|
+
await expect(holder.clients.fullAuth.credentialRefresh.getReceipt({ uri })).rejects.toThrow(
|
|
173
|
+
'Delivery binding unavailable'
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
110
177
|
beforeAll(async () => {
|
|
111
178
|
process.env.CREDENTIAL_REFRESH_ENABLED = 'true';
|
|
112
179
|
|
|
@@ -136,6 +203,9 @@ describe('Credential Refresh Allocation', () => {
|
|
|
136
203
|
});
|
|
137
204
|
|
|
138
205
|
beforeEach(async () => {
|
|
206
|
+
await ConsentFlowTransaction.delete({ detach: true, where: {} });
|
|
207
|
+
await ConsentFlowTerms.delete({ detach: true, where: {} });
|
|
208
|
+
await ConsentFlowContract.delete({ detach: true, where: {} });
|
|
139
209
|
await runQuery('MATCH (r:CredentialRefresh) DETACH DELETE r');
|
|
140
210
|
await runQuery('MATCH (c:Credential) DETACH DELETE c');
|
|
141
211
|
await Boost.delete({ detach: true, where: {} });
|
|
@@ -342,6 +412,67 @@ describe('Credential Refresh Allocation', () => {
|
|
|
342
412
|
expect(await countCredentialNodes()).toEqual(0);
|
|
343
413
|
expect(await countSentRelationships()).toEqual(0);
|
|
344
414
|
});
|
|
415
|
+
it('rejects a captured consent snapshot when the contract becomes pending before root binding', async () => {
|
|
416
|
+
const contractUri = await issuer.clients.fullAuth.contracts.createConsentFlowContract({
|
|
417
|
+
contract: normalContract,
|
|
418
|
+
name: 'Refresh consent contract',
|
|
419
|
+
});
|
|
420
|
+
await holder.clients.fullAuth.contracts.consentToContract({
|
|
421
|
+
contractUri,
|
|
422
|
+
terms: normalFullTerms,
|
|
423
|
+
});
|
|
424
|
+
const holderProfile = await holder.clients.fullAuth.profile.getProfile();
|
|
425
|
+
const issuerProfile = await issuer.clients.fullAuth.profile.getProfile();
|
|
426
|
+
const contract = await getContractDetailsByUri(contractUri);
|
|
427
|
+
const capturedTerms = await getContractTermsForProfile(
|
|
428
|
+
holderProfile!,
|
|
429
|
+
contract!.contract
|
|
430
|
+
);
|
|
431
|
+
expect(capturedTerms).toBeDefined();
|
|
432
|
+
|
|
433
|
+
const { allocation, credential } = await allocateAndSign();
|
|
434
|
+
const changedContract = {
|
|
435
|
+
...normalContract,
|
|
436
|
+
read: {
|
|
437
|
+
...normalContract.read,
|
|
438
|
+
credentials: {
|
|
439
|
+
...normalContract.read.credentials,
|
|
440
|
+
categories: {
|
|
441
|
+
...normalContract.read.credentials.categories,
|
|
442
|
+
Transcript: { required: true },
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
await issuer.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
448
|
+
uri: contractUri,
|
|
449
|
+
changes: { contract: changedContract },
|
|
450
|
+
reason: 'We need transcripts to personalize your progress report.',
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await expect(
|
|
454
|
+
sendRefreshableCredential({
|
|
455
|
+
issuerProfile: { ...issuerProfile!, did: issuer.learnCard.id.did() },
|
|
456
|
+
refreshId: allocation.refreshId,
|
|
457
|
+
credential,
|
|
458
|
+
contractTerms: capturedTerms!,
|
|
459
|
+
domain: DOMAIN,
|
|
460
|
+
})
|
|
461
|
+
).rejects.toMatchObject({
|
|
462
|
+
code: 'FORBIDDEN',
|
|
463
|
+
message: 'User has not yet approved the latest version of this contract',
|
|
464
|
+
});
|
|
465
|
+
expect(await countCredentialNodes()).toBe(0);
|
|
466
|
+
expect(await countSentRelationships()).toBe(0);
|
|
467
|
+
expect(
|
|
468
|
+
(
|
|
469
|
+
await holder.clients.fullAuth.contracts.getConsentStatus({
|
|
470
|
+
uri: contractUri,
|
|
471
|
+
profileId: HOLDER_PROFILE_ID,
|
|
472
|
+
})
|
|
473
|
+
).status
|
|
474
|
+
).toBe('needs_reconsent');
|
|
475
|
+
});
|
|
345
476
|
|
|
346
477
|
it('stores only the holder-encrypted JWE and binds version 1', async () => {
|
|
347
478
|
const { allocation, credential } = await allocateAndSign();
|
|
@@ -642,24 +773,91 @@ describe('Credential Refresh Allocation', () => {
|
|
|
642
773
|
expect(associations.records[0]?.get('boostIds')).toHaveLength(1);
|
|
643
774
|
});
|
|
644
775
|
|
|
645
|
-
|
|
776
|
+
const synchronizeInitialRootReads = () => {
|
|
777
|
+
const originalRun = neogma.queryRunner.run.bind(neogma.queryRunner);
|
|
778
|
+
let arrivals = 0;
|
|
779
|
+
let release!: () => void;
|
|
780
|
+
const bothReadsReady = new Promise<void>(resolve => {
|
|
781
|
+
release = resolve;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
return vi.spyOn(neogma.queryRunner, 'run').mockImplementation(async (query, params) => {
|
|
785
|
+
const isInitialRootRead =
|
|
786
|
+
params !== undefined &&
|
|
787
|
+
Object.keys(params).length === 1 &&
|
|
788
|
+
'refreshId' in params;
|
|
789
|
+
const result = await originalRun(query, params);
|
|
790
|
+
|
|
791
|
+
if (isInitialRootRead && result.records.length === 0 && arrivals < 2) {
|
|
792
|
+
arrivals += 1;
|
|
793
|
+
if (arrivals === 2) release();
|
|
794
|
+
await bothReadsReady;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return result;
|
|
798
|
+
});
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
it('returns the shared result for concurrent identical initial sends', async () => {
|
|
646
802
|
const { allocation, credential } = await allocateAndSign();
|
|
803
|
+
const queryRunnerSpy = synchronizeInitialRootReads();
|
|
647
804
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
805
|
+
try {
|
|
806
|
+
const results = await Promise.all([
|
|
807
|
+
issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
808
|
+
refreshId: allocation.refreshId,
|
|
809
|
+
credential,
|
|
810
|
+
}),
|
|
811
|
+
issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
812
|
+
refreshId: allocation.refreshId,
|
|
813
|
+
credential,
|
|
814
|
+
}),
|
|
815
|
+
]);
|
|
816
|
+
|
|
817
|
+
expect(results[0]).toEqual(results[1]);
|
|
818
|
+
expect(await countCredentialNodes()).toEqual(1);
|
|
819
|
+
expect(await countSentRelationships()).toEqual(1);
|
|
820
|
+
expect(addNotificationToQueueSpy).toHaveBeenCalledTimes(1);
|
|
821
|
+
} finally {
|
|
822
|
+
queryRunnerSpy.mockRestore();
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('rejects concurrent initial sends with different credential material', async () => {
|
|
827
|
+
const allocation = await allocate();
|
|
828
|
+
const [credential, differentCredential] = await Promise.all([
|
|
829
|
+
signAs(issuer, buildUnsignedCredential(allocation)),
|
|
830
|
+
signAs(
|
|
831
|
+
issuer,
|
|
832
|
+
buildUnsignedCredential(allocation, { name: 'Different Transcript' })
|
|
833
|
+
),
|
|
657
834
|
]);
|
|
835
|
+
const queryRunnerSpy = synchronizeInitialRootReads();
|
|
658
836
|
|
|
659
|
-
|
|
660
|
-
|
|
837
|
+
try {
|
|
838
|
+
const results = await Promise.allSettled([
|
|
839
|
+
issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
840
|
+
refreshId: allocation.refreshId,
|
|
841
|
+
credential,
|
|
842
|
+
}),
|
|
843
|
+
issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
844
|
+
refreshId: allocation.refreshId,
|
|
845
|
+
credential: differentCredential,
|
|
846
|
+
}),
|
|
847
|
+
]);
|
|
848
|
+
|
|
849
|
+
expect(results.filter(result => result.status === 'fulfilled')).toHaveLength(1);
|
|
850
|
+
expect(results.filter(result => result.status === 'rejected')).toHaveLength(1);
|
|
851
|
+
expect(results.find(result => result.status === 'rejected')).toMatchObject({
|
|
852
|
+
reason: { code: 'CONFLICT' },
|
|
853
|
+
});
|
|
854
|
+
expect(await countCredentialNodes()).toEqual(1);
|
|
855
|
+
expect(await countSentRelationships()).toEqual(1);
|
|
856
|
+
expect(addNotificationToQueueSpy).toHaveBeenCalledTimes(1);
|
|
857
|
+
} finally {
|
|
858
|
+
queryRunnerSpy.mockRestore();
|
|
859
|
+
}
|
|
661
860
|
});
|
|
662
|
-
|
|
663
861
|
it('rejects an initial-send retry whose credential material differs', async () => {
|
|
664
862
|
const { allocation, credential } = await allocateAndSign();
|
|
665
863
|
|
|
@@ -251,7 +251,7 @@ describe('credential refresh update emails', () => {
|
|
|
251
251
|
expect(emailDeliveries()[0].contactMethod.value).toBe(HOLDER_VERIFIED_EMAIL);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
it('emails the holder verified email
|
|
254
|
+
it('emails the holder verified email without credential-derived titles', async () => {
|
|
255
255
|
await addEmail(HOLDER_PROFILE_ID, HOLDER_VERIFIED_EMAIL, { primary: true });
|
|
256
256
|
await addEmail(HOLDER_PROFILE_ID, HOLDER_UNVERIFIED_EMAIL, { verified: false });
|
|
257
257
|
|
|
@@ -298,12 +298,12 @@ describe('credential refresh update emails', () => {
|
|
|
298
298
|
expect(delivery.templateId).toEqual('credential-updated');
|
|
299
299
|
expect(delivery.templateModel).toEqual({
|
|
300
300
|
issuer: { name: expect.any(String) },
|
|
301
|
-
credential: { name: SECRET_TITLE },
|
|
302
301
|
});
|
|
303
302
|
|
|
304
303
|
// No grades, description/body content, or issuer summary may reach the template.
|
|
305
304
|
const serialized = JSON.stringify(delivery);
|
|
306
305
|
expect(serialized).not.toContain(SECRET_GRADE_MARKER);
|
|
306
|
+
expect(serialized).not.toContain(SECRET_TITLE);
|
|
307
307
|
expect(serialized).not.toContain(SECRET_SUBJECT_MARKER);
|
|
308
308
|
expect(serialized).not.toContain(SECRET_SUMMARY_MARKER);
|
|
309
309
|
expect(serialized).not.toContain(HOLDER_UNVERIFIED_EMAIL);
|
|
@@ -382,9 +382,7 @@ describe('credential refresh update emails', () => {
|
|
|
382
382
|
});
|
|
383
383
|
|
|
384
384
|
expect(emailDeliveries()).toHaveLength(1);
|
|
385
|
-
expect(emailDeliveries()[0]!.templateModel).
|
|
386
|
-
credential: { name: SECRET_TITLE },
|
|
387
|
-
});
|
|
385
|
+
expect(emailDeliveries()[0]!.templateModel).not.toHaveProperty('credential');
|
|
388
386
|
|
|
389
387
|
// A replayed accept does not email twice.
|
|
390
388
|
await holder.clients.fullAuth.credential.acceptCredential({ uri });
|
|
@@ -616,9 +614,7 @@ describe('credential refresh update emails', () => {
|
|
|
616
614
|
expect(JSON.stringify(refreshEvents[0])).not.toContain(SECRET_TITLE);
|
|
617
615
|
|
|
618
616
|
// Email still carries the title.
|
|
619
|
-
expect(emailDeliveries()[0]!.templateModel).toMatchObject({
|
|
620
|
-
credential: { name: SECRET_TITLE },
|
|
621
|
-
});
|
|
617
|
+
expect(emailDeliveries()[0]!.templateModel).toMatchObject({});
|
|
622
618
|
});
|
|
623
619
|
|
|
624
620
|
it('renders generic copy and persists no title when the publication has none', async () => {
|
|
@@ -657,7 +653,7 @@ describe('credential refresh update emails', () => {
|
|
|
657
653
|
const aggregate = await getCredentialRefresh(allocation.refreshId);
|
|
658
654
|
const head = await getCredentialRefreshHead(allocation.refreshId);
|
|
659
655
|
|
|
660
|
-
expect(aggregate?.credentialDisplayName).
|
|
661
|
-
expect(head?.credentialDisplayName).
|
|
656
|
+
expect(aggregate?.credentialDisplayName).toBeUndefined();
|
|
657
|
+
expect(head?.credentialDisplayName).toBeUndefined();
|
|
662
658
|
});
|
|
663
659
|
});
|
|
@@ -588,18 +588,21 @@ describe('Credential Refresh Endpoint', () => {
|
|
|
588
588
|
const first = await preAuthSourceLimitedApp.inject({
|
|
589
589
|
method: 'GET',
|
|
590
590
|
url: '/refresh/rate-limited-source-a',
|
|
591
|
+
remoteAddress: '192.0.2.234',
|
|
591
592
|
});
|
|
592
593
|
expect(first.statusCode).toBe(401);
|
|
593
594
|
|
|
594
595
|
const second = await preAuthSourceLimitedApp.inject({
|
|
595
596
|
method: 'GET',
|
|
596
597
|
url: '/refresh/rate-limited-source-b',
|
|
598
|
+
remoteAddress: '192.0.2.234',
|
|
597
599
|
});
|
|
598
600
|
expect(second.statusCode).toBe(401);
|
|
599
601
|
|
|
600
602
|
const limited = await preAuthSourceLimitedApp.inject({
|
|
601
603
|
method: 'GET',
|
|
602
604
|
url: '/refresh/rate-limited-source-c',
|
|
605
|
+
remoteAddress: '192.0.2.234',
|
|
603
606
|
});
|
|
604
607
|
expect(limited.statusCode).toBe(429);
|
|
605
608
|
expect(limited.json().jwe).toBeUndefined();
|
|
@@ -16,6 +16,7 @@ import { getClient, getUser } from './helpers/getClient';
|
|
|
16
16
|
import { addNotificationToQueueSpy } from './helpers/spies';
|
|
17
17
|
import * as Notifications from '@helpers/notifications.helpers';
|
|
18
18
|
import { getLearnCard, SeedLearnCard } from '@helpers/learnCard.helpers';
|
|
19
|
+
import { computeCredentialMaterialDigest } from '@helpers/credential-refresh-materiality.helpers';
|
|
19
20
|
import {
|
|
20
21
|
getCredentialRefresh,
|
|
21
22
|
getCredentialRefreshHead,
|
|
@@ -634,6 +635,45 @@ describe('Credential Refresh Publication', () => {
|
|
|
634
635
|
expect(await countCredentialNodes()).toEqual(1);
|
|
635
636
|
});
|
|
636
637
|
|
|
638
|
+
it('retries initial binding and suppresses unchanged publication after digest key rotation', async () => {
|
|
639
|
+
try {
|
|
640
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'old-publication-test-key');
|
|
641
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v1');
|
|
642
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS', '{}');
|
|
643
|
+
const { allocation, credential } = await sendOriginal();
|
|
644
|
+
await setCredentialRefreshState(allocation.refreshId, 'active');
|
|
645
|
+
|
|
646
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_SECRET', 'new-publication-test-key');
|
|
647
|
+
vi.stubEnv('CREDENTIAL_REFRESH_DIGEST_KEY_VERSION', 'v2');
|
|
648
|
+
vi.stubEnv(
|
|
649
|
+
'CREDENTIAL_REFRESH_DIGEST_PREVIOUS_KEYS',
|
|
650
|
+
JSON.stringify({
|
|
651
|
+
v1: 'old-publication-test-key',
|
|
652
|
+
})
|
|
653
|
+
);
|
|
654
|
+
await issuer.clients.fullAuth.credentialRefresh.sendRefreshableCredential({
|
|
655
|
+
refreshId: allocation.refreshId,
|
|
656
|
+
credential,
|
|
657
|
+
});
|
|
658
|
+
expect(await countCredentialNodes()).toEqual(1);
|
|
659
|
+
|
|
660
|
+
const timestampOnly = await signAs(
|
|
661
|
+
issuer,
|
|
662
|
+
buildUnsignedCredential(allocation, { validFrom: '2026-02-01T00:00:00Z' })
|
|
663
|
+
);
|
|
664
|
+
addNotificationToQueueSpy.mockClear();
|
|
665
|
+
const result = await publishIssuerSigned(allocation.refreshId, timestampOnly);
|
|
666
|
+
expect(result.version).toEqual(2);
|
|
667
|
+
expect(result.notification).toEqual('suppressed');
|
|
668
|
+
expect(addNotificationToQueueSpy).not.toHaveBeenCalled();
|
|
669
|
+
expect((await getCredentialRefresh(allocation.refreshId))?.materialDigest).toBe(
|
|
670
|
+
computeCredentialMaterialDigest(timestampOnly as Record<string, unknown>)
|
|
671
|
+
);
|
|
672
|
+
} finally {
|
|
673
|
+
vi.unstubAllEnvs();
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
637
677
|
it('suppresses the first update when only non-material timestamps changed', async () => {
|
|
638
678
|
const { allocation } = await sendOriginal();
|
|
639
679
|
await setCredentialRefreshState(allocation.refreshId, 'active');
|
|
@@ -649,13 +689,11 @@ describe('Credential Refresh Publication', () => {
|
|
|
649
689
|
});
|
|
650
690
|
|
|
651
691
|
it('rejects removal or replacement of the original credentialStatus descriptor', async () => {
|
|
652
|
-
const credentialStatus =
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
statusListCredential: 'https://status.example.com/lists/42',
|
|
658
|
-
};
|
|
692
|
+
const [credentialStatus] = await issuer.clients.fullAuth.boost.allocateCredentialStatus(
|
|
693
|
+
{
|
|
694
|
+
statusPurposes: ['revocation'],
|
|
695
|
+
}
|
|
696
|
+
);
|
|
659
697
|
const { allocation } = await sendOriginal({ credentialStatus });
|
|
660
698
|
|
|
661
699
|
const removed = await signAs(
|
|
@@ -984,13 +1022,12 @@ describe('Credential Refresh Publication', () => {
|
|
|
984
1022
|
});
|
|
985
1023
|
|
|
986
1024
|
it('preserves an existing credentialStatus and allocates no new status-list entry', async () => {
|
|
987
|
-
const credentialStatus =
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
};
|
|
1025
|
+
const [credentialStatus] = await issuer.clients.fullAuth.boost.allocateCredentialStatus(
|
|
1026
|
+
{
|
|
1027
|
+
statusPurposes: ['revocation'],
|
|
1028
|
+
}
|
|
1029
|
+
);
|
|
1030
|
+
const initialListCount = await countStatusLists();
|
|
994
1031
|
const { allocation } = await sendOriginal({ credentialStatus });
|
|
995
1032
|
await registerIssuerSigningAuthority();
|
|
996
1033
|
|
|
@@ -1005,7 +1042,7 @@ describe('Credential Refresh Publication', () => {
|
|
|
1005
1042
|
);
|
|
1006
1043
|
|
|
1007
1044
|
// No status list was created or allocated for the refresh version.
|
|
1008
|
-
expect(await countStatusLists()).toEqual(
|
|
1045
|
+
expect(await countStatusLists()).toEqual(initialListCount);
|
|
1009
1046
|
});
|
|
1010
1047
|
|
|
1011
1048
|
it('does not add a credentialStatus when the body has none', async () => {
|
|
@@ -43,16 +43,17 @@ describe('credential status storage in Neo4j', () => {
|
|
|
43
43
|
credentialStatus: entry,
|
|
44
44
|
});
|
|
45
45
|
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
46
|
-
const stored = await storeCredential(
|
|
47
|
-
kind: 'issued-credential',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const stored = await storeCredential(
|
|
47
|
+
{ kind: 'issued-credential', credential: jwe, statusEntries: [entry] },
|
|
48
|
+
undefined,
|
|
49
|
+
'storage-issuer'
|
|
50
|
+
);
|
|
51
51
|
credentialIds.push(stored.id);
|
|
52
52
|
|
|
53
53
|
const row = await getCredentialById(stored.id);
|
|
54
54
|
expect(row?.credential).toBe(JSON.stringify(jwe));
|
|
55
55
|
expect(JSON.parse(row?.statusEntries ?? 'null')).toEqual([entry]);
|
|
56
|
+
expect(row?.statusOwnerProfileId).toBe('storage-issuer');
|
|
56
57
|
expect(await isStatusBitSet(entry)).toBe(false);
|
|
57
58
|
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(true);
|
|
58
59
|
expect(await isStatusBitSet(entry)).toBe(true);
|
|
@@ -94,12 +95,18 @@ describe('credential status storage in Neo4j', () => {
|
|
|
94
95
|
credentialStatus: entry,
|
|
95
96
|
});
|
|
96
97
|
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
97
|
-
const wire = {
|
|
98
|
+
const wire = {
|
|
99
|
+
...jwe,
|
|
100
|
+
kind: 'issued-credential',
|
|
101
|
+
statusEntries: [entry],
|
|
102
|
+
statusOwnerProfileId: 'storage-issuer',
|
|
103
|
+
};
|
|
98
104
|
const stored = await storeCredential(wire);
|
|
99
105
|
credentialIds.push(stored.id);
|
|
100
106
|
const row = await getCredentialById(stored.id);
|
|
101
107
|
expect(row?.credential).toBe(JSON.stringify(wire));
|
|
102
108
|
expect(row?.statusEntries).toBeUndefined();
|
|
109
|
+
expect(row?.statusOwnerProfileId).toBeUndefined();
|
|
103
110
|
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
|
|
104
111
|
expect(await isStatusBitSet(entry)).toBe(false);
|
|
105
112
|
});
|
|
@@ -119,4 +126,24 @@ describe('credential status storage in Neo4j', () => {
|
|
|
119
126
|
expect(row?.statusEntries).toBeUndefined();
|
|
120
127
|
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
|
|
121
128
|
});
|
|
129
|
+
|
|
130
|
+
it('rejects a trusted descriptor when the supplied owner does not own its list', async () => {
|
|
131
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
132
|
+
statusPurposes: ['revocation'],
|
|
133
|
+
});
|
|
134
|
+
if (!entry) throw new Error('Expected allocated revocation entry');
|
|
135
|
+
const signed = await issuer.learnCard.invoke.issueCredential({
|
|
136
|
+
...(await issuer.learnCard.invoke.getTestVc()),
|
|
137
|
+
credentialStatus: entry,
|
|
138
|
+
});
|
|
139
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
140
|
+
await expect(
|
|
141
|
+
storeCredential(
|
|
142
|
+
{ kind: 'issued-credential', credential: jwe, statusEntries: [entry] },
|
|
143
|
+
undefined,
|
|
144
|
+
'another-issuer'
|
|
145
|
+
)
|
|
146
|
+
).rejects.toThrow('Invalid local credential status coordinates');
|
|
147
|
+
expect(await isStatusBitSet(entry)).toBe(false);
|
|
148
|
+
});
|
|
122
149
|
});
|