@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
|
@@ -6,10 +6,14 @@ import { isIssuedCredential } from '@helpers/issuedCredentialStatus.helpers';
|
|
|
6
6
|
import type { IssuedCredential } from 'types/credential';
|
|
7
7
|
|
|
8
8
|
import { Credential, CredentialInstance } from '@models';
|
|
9
|
+
import { neogma } from '@instance';
|
|
10
|
+
import { reserveCredentialStatusEntries } from '@helpers/status-list.helpers';
|
|
9
11
|
|
|
10
12
|
export const storeCredential = async (
|
|
11
13
|
input: UnsignedVC | VC | JWE | IssuedCredential,
|
|
12
|
-
transaction?: Transaction
|
|
14
|
+
transaction?: Transaction,
|
|
15
|
+
statusOwnerProfileId?: string,
|
|
16
|
+
options: { lenient?: boolean } = {}
|
|
13
17
|
): Promise<CredentialInstance> => {
|
|
14
18
|
const id = uuid();
|
|
15
19
|
|
|
@@ -21,6 +25,7 @@ export const storeCredential = async (
|
|
|
21
25
|
? BitstringStatusListEntryValidator.array().parse(input.statusEntries)
|
|
22
26
|
: undefined;
|
|
23
27
|
const encrypted = isEncrypted(credential);
|
|
28
|
+
const lenient = options.lenient === true && !issued && !encrypted;
|
|
24
29
|
if (
|
|
25
30
|
!statusEntries?.length &&
|
|
26
31
|
(encrypted || !getBitstringStatusListEntries(credential).length)
|
|
@@ -30,12 +35,31 @@ export const storeCredential = async (
|
|
|
30
35
|
{ credentialId: id }
|
|
31
36
|
);
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const create = (session?: Transaction, validatedOwner?: string) =>
|
|
39
|
+
Credential.createOne(
|
|
40
|
+
{
|
|
41
|
+
id,
|
|
42
|
+
credential: JSON.stringify(credential),
|
|
43
|
+
...(statusEntries?.length ? { statusEntries: JSON.stringify(statusEntries) } : {}),
|
|
44
|
+
...(validatedOwner
|
|
45
|
+
? {
|
|
46
|
+
statusOwnerProfileId: validatedOwner,
|
|
47
|
+
statusReservationMode: lenient
|
|
48
|
+
? ('lenient' as const)
|
|
49
|
+
: ('strict' as const),
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
},
|
|
53
|
+
session ? { session } : undefined
|
|
54
|
+
);
|
|
55
|
+
const entries = statusEntries ?? getBitstringStatusListEntries(credential);
|
|
56
|
+
if (!statusOwnerProfileId || entries.length === 0) return create(transaction);
|
|
57
|
+
return neogma.getTransaction(transaction ?? null, async tx => {
|
|
58
|
+
await reserveCredentialStatusEntries(entries, statusOwnerProfileId, id, tx, {
|
|
59
|
+
lenient,
|
|
60
|
+
...(!encrypted ? { credential } : {}),
|
|
61
|
+
});
|
|
62
|
+
// Inbox deliveries need a validated owner even without a CREDENTIAL_SENT edge.
|
|
63
|
+
return create(tx, statusOwnerProfileId);
|
|
64
|
+
});
|
|
41
65
|
};
|
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
import { CredentialInstance } from '@models';
|
|
2
|
+
import { neogma } from '@instance';
|
|
2
3
|
|
|
3
4
|
export const deleteCredential = async (credential: CredentialInstance): Promise<void> => {
|
|
4
5
|
await credential.delete({ detach: true });
|
|
5
6
|
};
|
|
7
|
+
|
|
8
|
+
/** Release failed deliveries with no relationships beyond Boost instance membership. */
|
|
9
|
+
export const releaseUnlinkedCredential = async (id: string): Promise<void> => {
|
|
10
|
+
await neogma.getTransaction(null, async tx => {
|
|
11
|
+
const eligible = await tx.run(
|
|
12
|
+
`MATCH (credential:Credential {id: $id})
|
|
13
|
+
SET credential._statusMutationLock = true REMOVE credential._statusMutationLock
|
|
14
|
+
WITH credential
|
|
15
|
+
WHERE NOT EXISTS {
|
|
16
|
+
MATCH (credential)-[relationship]-()
|
|
17
|
+
WHERE type(relationship) <> 'INSTANCE_OF'
|
|
18
|
+
}
|
|
19
|
+
RETURN credential.id AS id`,
|
|
20
|
+
{ id }
|
|
21
|
+
);
|
|
22
|
+
if (eligible.records.length === 0) return;
|
|
23
|
+
const lists = await tx.run(
|
|
24
|
+
`MATCH (list:BitstringStatusList)-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding)
|
|
25
|
+
WHERE $id IN coalesce(binding.credentialIds, [binding.credentialId])
|
|
26
|
+
RETURN DISTINCT list.statusListCredential AS url ORDER BY url`,
|
|
27
|
+
{ id }
|
|
28
|
+
);
|
|
29
|
+
// Match reservation lock order and re-read membership only after acquiring each list lock.
|
|
30
|
+
for (const row of [...lists.records].sort((a, b) =>
|
|
31
|
+
String(a.get('url')).localeCompare(String(b.get('url')))
|
|
32
|
+
)) {
|
|
33
|
+
const url = String(row.get('url'));
|
|
34
|
+
await tx.run(
|
|
35
|
+
`MATCH (list:BitstringStatusList {statusListCredential: $url})
|
|
36
|
+
SET list._statusBindingLock = true REMOVE list._statusBindingLock`,
|
|
37
|
+
{ url }
|
|
38
|
+
);
|
|
39
|
+
await tx.run(
|
|
40
|
+
`MATCH (:BitstringStatusList {statusListCredential: $url})-[:HAS_STATUS_BINDING]->(binding:CredentialStatusBinding)
|
|
41
|
+
WHERE binding.refreshId IS NULL AND $id IN coalesce(binding.credentialIds, [binding.credentialId])
|
|
42
|
+
WITH binding, [member IN coalesce(binding.credentialIds, [binding.credentialId]) WHERE member <> $id] AS remaining
|
|
43
|
+
SET binding.credentialIds = remaining, binding.credentialId = head(remaining)
|
|
44
|
+
FOREACH (_ IN CASE WHEN size(remaining) = 0 THEN [1] ELSE [] END | DETACH DELETE binding)`,
|
|
45
|
+
{ id, url }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
await tx.run('MATCH (credential:Credential {id: $id}) DETACH DELETE credential', { id });
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
CredentialInstance,
|
|
8
8
|
Credential,
|
|
9
9
|
Boost,
|
|
10
|
+
BoostInstance,
|
|
10
11
|
Role,
|
|
11
12
|
Profile,
|
|
12
|
-
ConsentFlowTerms,
|
|
13
13
|
ConsentFlowTransaction,
|
|
14
14
|
AppStoreListing,
|
|
15
15
|
} from '@models';
|
|
@@ -19,6 +19,8 @@ import { CredentialIssuer, getIssuerOwnerProfile, isAppStoreListingIssuer } from
|
|
|
19
19
|
import { clearDidWebCacheForChildProfileManagers } from '@accesslayer/boost/relationships/update';
|
|
20
20
|
import { getBoostIdForCredentialInstance } from '@accesslayer/credential/relationships/read';
|
|
21
21
|
import { DbTermsType } from 'types/consentflowcontract';
|
|
22
|
+
import { TRPCError } from '@trpc/server';
|
|
23
|
+
import { runAudienceMutation } from '@accesslayer/consentflowcontract/relationships/recipients';
|
|
22
24
|
|
|
23
25
|
export const createSentCredentialRelationship = async (
|
|
24
26
|
issuer: CredentialIssuer,
|
|
@@ -102,7 +104,8 @@ export const createListingSentCredentialRelationship = async (
|
|
|
102
104
|
credential: CredentialInstance,
|
|
103
105
|
metadata?: Record<string, unknown>,
|
|
104
106
|
activityId?: string,
|
|
105
|
-
integrationId?: string
|
|
107
|
+
integrationId?: string,
|
|
108
|
+
transaction?: Transaction
|
|
106
109
|
): Promise<void> => {
|
|
107
110
|
const properties = flattenObject({
|
|
108
111
|
to: to.profileId,
|
|
@@ -133,7 +136,7 @@ export const createListingSentCredentialRelationship = async (
|
|
|
133
136
|
}]->(credential)`
|
|
134
137
|
)
|
|
135
138
|
.set('r = $params')
|
|
136
|
-
.run();
|
|
139
|
+
.run(transaction);
|
|
137
140
|
};
|
|
138
141
|
|
|
139
142
|
export const createReceivedCredentialRelationship = async (
|
|
@@ -218,49 +221,101 @@ export const setDefaultClaimedRole = async (
|
|
|
218
221
|
};
|
|
219
222
|
|
|
220
223
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
224
|
+
* Commit delivery and its audit entry under the same lock as access updates.
|
|
225
|
+
* Signing may have awaited an external service; the captured grant is not authority.
|
|
223
226
|
*/
|
|
224
|
-
export const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
227
|
+
export const createContractCredentialRelationships = async ({
|
|
228
|
+
issuer,
|
|
229
|
+
writerProfileId,
|
|
230
|
+
recipient,
|
|
231
|
+
credential,
|
|
232
|
+
boost,
|
|
233
|
+
terms,
|
|
234
|
+
metadata,
|
|
235
|
+
activityId,
|
|
236
|
+
integrationId,
|
|
237
|
+
listingId,
|
|
238
|
+
allowConsumedOneTime = false,
|
|
239
|
+
}: {
|
|
240
|
+
issuer: CredentialIssuer;
|
|
241
|
+
writerProfileId: string;
|
|
242
|
+
recipient: ProfileType;
|
|
243
|
+
credential: CredentialInstance;
|
|
244
|
+
boost: BoostInstance;
|
|
245
|
+
terms: DbTermsType;
|
|
246
|
+
metadata?: Record<string, unknown>;
|
|
247
|
+
activityId?: string;
|
|
248
|
+
integrationId?: string;
|
|
249
|
+
listingId?: string;
|
|
250
|
+
allowConsumedOneTime?: boolean;
|
|
251
|
+
}): Promise<void> => {
|
|
252
|
+
const date = new Date().toISOString();
|
|
253
|
+
const owner = getIssuerOwnerProfile(issuer);
|
|
254
|
+
const listingIds = [
|
|
255
|
+
...new Set([
|
|
256
|
+
...(isAppStoreListingIssuer(issuer) ? [issuer.listing.listing_id] : []),
|
|
257
|
+
...(listingId ? [listingId] : []),
|
|
258
|
+
]),
|
|
259
|
+
];
|
|
260
|
+
const result = await runAudienceMutation(
|
|
261
|
+
new QueryBuilder(
|
|
262
|
+
new BindParam({
|
|
263
|
+
termsId: terms.id,
|
|
264
|
+
expectedVersion: terms.consentedVersion,
|
|
265
|
+
issuerId: owner.profileId,
|
|
266
|
+
writerProfileId,
|
|
267
|
+
recipientId: recipient.profileId,
|
|
268
|
+
credentialId: credential.id,
|
|
269
|
+
boostId: boost.id,
|
|
270
|
+
listingIds,
|
|
271
|
+
allowConsumedOneTime,
|
|
272
|
+
sent: flattenObject({
|
|
273
|
+
to: recipient.profileId,
|
|
274
|
+
date,
|
|
275
|
+
...(metadata ? { metadata } : {}),
|
|
276
|
+
...(activityId ? { activityId } : {}),
|
|
277
|
+
...(integrationId ? { integrationId } : {}),
|
|
278
|
+
}),
|
|
279
|
+
transactionId: uuid(),
|
|
280
|
+
date,
|
|
281
|
+
})
|
|
282
|
+
).raw(`
|
|
283
|
+
MATCH (terms:ConsentFlowTerms {id:$termsId})-[:CONSENTS_TO]->(contract:ConsentFlowContract)
|
|
284
|
+
SET contract.audienceLock = coalesce(contract.audienceLock, 0) + 1
|
|
285
|
+
WITH terms, contract
|
|
286
|
+
WHERE coalesce(contract.status, 'active') <> 'retired'
|
|
287
|
+
AND contract.currentVersion = $expectedVersion
|
|
288
|
+
AND terms.consentedVersion = $expectedVersion
|
|
289
|
+
AND (terms.status = 'live' OR
|
|
290
|
+
($allowConsumedOneTime AND terms.status = 'consumed' AND terms.oneTime = true))
|
|
291
|
+
AND (terms.expiresAt IS NULL OR trim(terms.expiresAt) = '' OR datetime(terms.expiresAt) > datetime.realtime())
|
|
292
|
+
AND (contract.expiresAt IS NULL OR trim(contract.expiresAt) = '' OR datetime(contract.expiresAt) > datetime.realtime())
|
|
293
|
+
AND NOT $writerProfileId IN coalesce(terms['terms.deniedWriters'], [])
|
|
294
|
+
AND EXISTS { MATCH (contract)-[:CREATED_BY|CAN_WRITE]->(:Profile {profileId:$writerProfileId}) }
|
|
295
|
+
AND EXISTS { MATCH (:Profile {profileId:$recipientId})-[:CREATED_BY]->(terms) }
|
|
296
|
+
MATCH (credential:Credential {id:$credentialId}), (boost:Boost {id:$boostId}),
|
|
297
|
+
(issuer:Profile {profileId:$issuerId})
|
|
298
|
+
WHERE boost.category IS NULL OR terms['terms.write.credentials.categories.' + boost.category] = true
|
|
299
|
+
OPTIONAL MATCH (listing:AppStoreListing) WHERE listing.listing_id IN $listingIds
|
|
300
|
+
WITH terms, contract, credential, boost, issuer, collect(DISTINCT listing) AS listings
|
|
301
|
+
WHERE size(listings) = size($listingIds)
|
|
302
|
+
CREATE (credential)-[:${Credential.getRelationshipByAlias('instanceOf').name}]->(boost)
|
|
303
|
+
CREATE (issuer)-[sent:${Profile.getRelationshipByAlias('credentialSent').name}]->(credential)
|
|
304
|
+
SET sent = $sent
|
|
305
|
+
FOREACH (listing IN listings |
|
|
306
|
+
CREATE (listing)-[delivery:CREDENTIAL_SENT]->(credential)
|
|
307
|
+
SET delivery = $sent)
|
|
308
|
+
CREATE (transaction:ConsentFlowTransaction {id:$transactionId, action:'write', date:$date, boostId:$boostId})
|
|
309
|
+
-[:${ConsentFlowTransaction.getRelationshipByAlias('isFor').name}]->(terms)
|
|
310
|
+
${termsReferralSnapshotCypher()}
|
|
311
|
+
CREATE (credential)-[:${Credential.getRelationshipByAlias('issuedViaTransaction').name} {date:$date}]->(transaction)
|
|
312
|
+
RETURN transaction.id AS id
|
|
313
|
+
`)
|
|
314
|
+
);
|
|
315
|
+
if (!result.records.length) {
|
|
316
|
+
throw new TRPCError({
|
|
317
|
+
code: 'FORBIDDEN',
|
|
318
|
+
message: 'User has not yet approved the latest version of this contract',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
266
321
|
};
|
|
@@ -1,135 +1,27 @@
|
|
|
1
1
|
import { QueryBuilder, BindParam } from 'neogma';
|
|
2
2
|
|
|
3
3
|
import { Credential } from '@models';
|
|
4
|
-
import {
|
|
5
|
-
import { revokeCredentialRefreshForCredential } from '@accesslayer/credential-refresh/update';
|
|
4
|
+
import { mutateCredentialStatus } from '@helpers/credential-status-mutation.helpers';
|
|
6
5
|
|
|
7
|
-
/**
|
|
8
|
-
* Revoke a credential by setting its issuer-controlled status on the CREDENTIAL_SENT relationship.
|
|
9
|
-
* This applies to both pending and claimed credentials without creating a received relationship.
|
|
10
|
-
*/
|
|
6
|
+
/** Revoke the root credential and its public bit atomically. */
|
|
11
7
|
export const revokeCredentialReceived = async (
|
|
12
8
|
credentialId: string,
|
|
13
9
|
profileId: string
|
|
14
|
-
): Promise<boolean> =>
|
|
15
|
-
const revokedAt = new Date().toISOString();
|
|
16
|
-
|
|
17
|
-
const result = await new QueryBuilder(new BindParam({ profileId, revokedAt }))
|
|
18
|
-
.match({ identifier: 'credential', model: Credential, where: { id: credentialId } })
|
|
19
|
-
.raw(
|
|
20
|
-
`MATCH (sender)-[sent:CREDENTIAL_SENT {to: $profileId}]->(credential)
|
|
21
|
-
WHERE sender:Profile OR sender:AppStoreListing
|
|
22
|
-
SET sent.status = "revoked",
|
|
23
|
-
sent.revokedAt = $revokedAt
|
|
24
|
-
RETURN sent`
|
|
25
|
-
)
|
|
26
|
-
.run();
|
|
27
|
-
|
|
28
|
-
if (result.records.length > 0) {
|
|
29
|
-
const bitSet = await setCredentialBitstringStatus(credentialId, 'revocation', true);
|
|
30
|
-
if (!bitSet) {
|
|
31
|
-
// The relationship is marked revoked but the credential has no 'revocation'
|
|
32
|
-
// status-list entry, so the verifiable bit was NOT set. Holders relying purely
|
|
33
|
-
// on verifyCredential can't see the revocation. Surface it instead of failing
|
|
34
|
-
// silently (the authoritative relationship status still drives the UI).
|
|
35
|
-
console.warn(
|
|
36
|
-
`[revokeCredentialReceived] credential ${credentialId} has no verifiable 'revocation' status entry; bitstring bit not set`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
10
|
+
): Promise<boolean> => mutateCredentialStatus(credentialId, profileId, 'revocation', true);
|
|
40
11
|
|
|
41
|
-
|
|
42
|
-
// sent credential invalidates the whole refresh chain. Best-effort — the holder
|
|
43
|
-
// endpoint independently cross-checks this canonical relationship state on every
|
|
44
|
-
// authenticated request, so a failed write here cannot leave a revoked credential
|
|
45
|
-
// servable.
|
|
46
|
-
if (result.records.length > 0) {
|
|
47
|
-
try {
|
|
48
|
-
await revokeCredentialRefreshForCredential(credentialId);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error(
|
|
51
|
-
'[revokeCredentialReceived] failed to revoke credential refresh aggregate:',
|
|
52
|
-
error
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return result.records.length > 0;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Suspend a credential by setting its issuer-controlled status on the CREDENTIAL_SENT relationship.
|
|
62
|
-
* If the credential has already been revoked, the revoked relationship state is preserved.
|
|
63
|
-
*/
|
|
12
|
+
/** Suspend the root credential without changing an irreversible revocation. */
|
|
64
13
|
export const suspendCredentialReceived = async (
|
|
65
14
|
credentialId: string,
|
|
66
15
|
profileId: string
|
|
67
|
-
): Promise<boolean> =>
|
|
68
|
-
const suspendedAt = new Date().toISOString();
|
|
69
|
-
|
|
70
|
-
const result = await new QueryBuilder(new BindParam({ profileId, suspendedAt }))
|
|
71
|
-
.match({ identifier: 'credential', model: Credential, where: { id: credentialId } })
|
|
72
|
-
.raw(
|
|
73
|
-
`MATCH (sender)-[sent:CREDENTIAL_SENT {to: $profileId}]->(credential)
|
|
74
|
-
WHERE (sender:Profile OR sender:AppStoreListing)
|
|
75
|
-
AND coalesce(sent.status, "") <> "revoked"
|
|
76
|
-
SET sent.status = "suspended",
|
|
77
|
-
sent.suspendedAt = $suspendedAt
|
|
78
|
-
RETURN sent`
|
|
79
|
-
)
|
|
80
|
-
.run();
|
|
81
|
-
|
|
82
|
-
if (result.records.length > 0) {
|
|
83
|
-
const bitSet = await setCredentialBitstringStatus(credentialId, 'suspension', true);
|
|
84
|
-
if (!bitSet) {
|
|
85
|
-
// Relationship marked suspended but the credential has no 'suspension' status
|
|
86
|
-
// entry, so the verifiable bit was NOT set. The authoritative relationship
|
|
87
|
-
// status still drives the UI; log so the gap is visible.
|
|
88
|
-
console.warn(
|
|
89
|
-
`[suspendCredentialReceived] credential ${credentialId} has no verifiable 'suspension' status entry; bitstring bit not set`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return result.records.length > 0;
|
|
95
|
-
};
|
|
16
|
+
): Promise<boolean> => mutateCredentialStatus(credentialId, profileId, 'suspension', true);
|
|
96
17
|
|
|
97
|
-
/**
|
|
98
|
-
* Clear a reversible suspension. Revocation remains irreversible and is not cleared.
|
|
99
|
-
*/
|
|
18
|
+
/** Failed bit writes roll back the relationship, so unsuspension remains retryable. */
|
|
100
19
|
export const unsuspendCredentialReceived = async (
|
|
101
20
|
credentialId: string,
|
|
102
21
|
profileId: string
|
|
103
|
-
): Promise<boolean> =>
|
|
104
|
-
const unsuspendedAt = new Date().toISOString();
|
|
105
|
-
|
|
106
|
-
const result = await new QueryBuilder(new BindParam({ profileId, unsuspendedAt }))
|
|
107
|
-
.match({ identifier: 'credential', model: Credential, where: { id: credentialId } })
|
|
108
|
-
.raw(
|
|
109
|
-
`MATCH (sender)-[sent:CREDENTIAL_SENT {to: $profileId}]->(credential)
|
|
110
|
-
WHERE (sender:Profile OR sender:AppStoreListing)
|
|
111
|
-
AND sent.status = "suspended"
|
|
112
|
-
SET sent.status = null,
|
|
113
|
-
sent.unsuspendedAt = $unsuspendedAt
|
|
114
|
-
RETURN sent`
|
|
115
|
-
)
|
|
116
|
-
.run();
|
|
117
|
-
|
|
118
|
-
if (result.records.length > 0) {
|
|
119
|
-
const bitCleared = await setCredentialBitstringStatus(credentialId, 'suspension', false);
|
|
120
|
-
if (!bitCleared) {
|
|
121
|
-
console.warn(
|
|
122
|
-
`[unsuspendCredentialReceived] credential ${credentialId} has no verifiable 'suspension' status entry; bitstring bit not cleared`
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result.records.length > 0;
|
|
128
|
-
};
|
|
22
|
+
): Promise<boolean> => mutateCredentialStatus(credentialId, profileId, 'suspension', false);
|
|
129
23
|
|
|
130
|
-
/**
|
|
131
|
-
* Check if a credential has been revoked for a specific profile
|
|
132
|
-
*/
|
|
24
|
+
/** Check if a credential has been revoked for a specific profile. */
|
|
133
25
|
export const isCredentialRevoked = async (
|
|
134
26
|
credentialId: string,
|
|
135
27
|
profileId: string
|
|
@@ -39,6 +39,28 @@ export const getCredentialRefresh = async (
|
|
|
39
39
|
return CredentialRefreshRecordValidator.parse(normalizeProps(node.properties));
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
/** Select only a holder's actual root delivery; acceptance is deliberately not required. */
|
|
43
|
+
export const getCredentialRefreshReceiptBinding = async (
|
|
44
|
+
rootId: string,
|
|
45
|
+
holderProfileId: string
|
|
46
|
+
): Promise<CredentialRefreshRecord | null | undefined> => {
|
|
47
|
+
const result = await neogma.queryRunner.run(
|
|
48
|
+
`MATCH (sender)-[sent:CREDENTIAL_SENT {to: $holderProfileId}]->(root:Credential {id: $rootId})
|
|
49
|
+
WHERE (sender:Profile OR sender:AppStoreListing)
|
|
50
|
+
AND (sent.status IS NULL OR sent.status <> 'revoked')
|
|
51
|
+
AND NOT EXISTS {
|
|
52
|
+
MATCH (root)-[received:CREDENTIAL_RECEIVED]->(:Profile {profileId: $holderProfileId})
|
|
53
|
+
WHERE received.status = 'revoked'
|
|
54
|
+
}
|
|
55
|
+
OPTIONAL MATCH (refresh:CredentialRefresh)-[:ROOT]->(root)
|
|
56
|
+
RETURN refresh LIMIT 1`,
|
|
57
|
+
{ rootId, holderProfileId }
|
|
58
|
+
);
|
|
59
|
+
if (!result.records.length) return undefined;
|
|
60
|
+
const node = result.records[0]?.get('refresh');
|
|
61
|
+
return node ? CredentialRefreshRecordValidator.parse(normalizeProps(node.properties)) : null;
|
|
62
|
+
};
|
|
63
|
+
|
|
42
64
|
/** Fetches the current head version node (including its holder-encrypted JWE payload). */
|
|
43
65
|
export const getCredentialRefreshHead = async (
|
|
44
66
|
refreshId: string
|
|
@@ -205,7 +227,8 @@ export const getCredentialRefreshVersionsForHolder = async (
|
|
|
205
227
|
effectiveAt: version.effectiveAt,
|
|
206
228
|
etag: version.etag,
|
|
207
229
|
signingMode: version.signingMode,
|
|
208
|
-
|
|
230
|
+
updateSummary: version.updateSummary,
|
|
231
|
+
updateReason: version.updateReason
|
|
209
232
|
} END) WHERE entry IS NOT NULL] AS metadata
|
|
210
233
|
RETURN revoked, metadata`,
|
|
211
234
|
{ refreshId, beforeVersion: beforeVersion ?? null, limitPlusOne: int(limit + 1) }
|
|
@@ -279,7 +302,8 @@ export const getCredentialRefreshVersions = async (
|
|
|
279
302
|
effectiveAt: version.effectiveAt,
|
|
280
303
|
etag: version.etag,
|
|
281
304
|
signingMode: version.signingMode,
|
|
282
|
-
|
|
305
|
+
updateSummary: version.updateSummary,
|
|
306
|
+
updateReason: version.updateReason
|
|
283
307
|
} AS metadata
|
|
284
308
|
ORDER BY version.version DESC
|
|
285
309
|
LIMIT $limitPlusOne`,
|
|
@@ -78,6 +78,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
78
78
|
etag,
|
|
79
79
|
materialDigest,
|
|
80
80
|
updateSummary,
|
|
81
|
+
updateReason,
|
|
81
82
|
credentialDisplayName,
|
|
82
83
|
effectiveAt,
|
|
83
84
|
notificationOutcome,
|
|
@@ -104,6 +105,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
104
105
|
etag,
|
|
105
106
|
signingMode,
|
|
106
107
|
updateSummary,
|
|
108
|
+
updateReason,
|
|
107
109
|
credentialDisplayName,
|
|
108
110
|
idempotencyKey,
|
|
109
111
|
refreshIdempotencyKey,
|
|
@@ -161,8 +163,9 @@ export const advanceCredentialRefreshHead = async (
|
|
|
161
163
|
SET refresh.currentVersion = $nextVersion,
|
|
162
164
|
refresh.etag = $etag,
|
|
163
165
|
refresh.materialDigest = $materialDigest,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
refresh.updateSummary = $updateSummary,
|
|
167
|
+
refresh.updateReason = $updateReason,
|
|
168
|
+
refresh.credentialDisplayName = $credentialDisplayName,
|
|
166
169
|
refresh.signingMode = $signingMode,
|
|
167
170
|
refresh.lastPublishedAt = $publishedAt,
|
|
168
171
|
refresh.idempotencyKey = $idempotencyKey,
|
|
@@ -179,6 +182,7 @@ export const advanceCredentialRefreshHead = async (
|
|
|
179
182
|
etag: etag ?? null,
|
|
180
183
|
materialDigest: materialDigest ?? null,
|
|
181
184
|
updateSummary: updateSummary ?? null,
|
|
185
|
+
updateReason: updateReason ?? null,
|
|
182
186
|
credentialDisplayName: credentialDisplayName ?? null,
|
|
183
187
|
signingMode,
|
|
184
188
|
publishedAt,
|
|
@@ -16,7 +16,12 @@ export const DEFAULT_INBOX_EXPIRY_DAYS = 30;
|
|
|
16
16
|
|
|
17
17
|
export const createInboxCredential = async (input: {
|
|
18
18
|
credential: string;
|
|
19
|
-
refresh?: {
|
|
19
|
+
refresh?: {
|
|
20
|
+
aggregate: Record<string, unknown>;
|
|
21
|
+
issueKey: string;
|
|
22
|
+
requestDigest: string;
|
|
23
|
+
requestDigestCandidates?: string[];
|
|
24
|
+
};
|
|
20
25
|
/** Only set after the normal delivery helper has durably stored the credential. */
|
|
21
26
|
delivered?: boolean;
|
|
22
27
|
isSigned: boolean;
|
|
@@ -93,6 +98,7 @@ export const createInboxCredential = async (input: {
|
|
|
93
98
|
),
|
|
94
99
|
issueKey: input.refresh.issueKey,
|
|
95
100
|
requestDigest: input.refresh.requestDigest,
|
|
101
|
+
requestDigestCandidates: input.refresh.requestDigestCandidates,
|
|
96
102
|
});
|
|
97
103
|
id = created.id;
|
|
98
104
|
} else {
|
|
@@ -32,12 +32,13 @@ export const createPendingInboxRefresh = async (params: {
|
|
|
32
32
|
aggregate: Record<string, unknown>;
|
|
33
33
|
issueKey: string;
|
|
34
34
|
requestDigest: string;
|
|
35
|
+
requestDigestCandidates?: string[];
|
|
35
36
|
}): Promise<InboxCredentialType> => {
|
|
36
37
|
const result = await neogma.queryRunner.run(
|
|
37
38
|
`MATCH (issuer:Profile {profileId: $issuerProfileId})
|
|
38
39
|
MERGE (ic:InboxCredential {refreshIssueKey: $issueKey})
|
|
39
40
|
ON CREATE SET ic += $inbox, ic.refreshRequestDigest = $requestDigest
|
|
40
|
-
|
|
41
|
+
WITH issuer, ic WHERE ic.refreshRequestDigest IN $requestDigestCandidates
|
|
41
42
|
MERGE (refresh:CredentialRefresh {refreshId: ic.refreshId})
|
|
42
43
|
ON CREATE SET refresh += $aggregate
|
|
43
44
|
MERGE (issuer)-[:ISSUED_REFRESH]->(refresh)
|
|
@@ -46,7 +47,11 @@ export const createPendingInboxRefresh = async (params: {
|
|
|
46
47
|
ON CREATE SET p.id = ic.refreshId, p.refreshId = ic.refreshId, p.version = 1,
|
|
47
48
|
p.publishedAt = ic.createdAt, p.signingMode = 'signing-authority'
|
|
48
49
|
RETURN ic`,
|
|
49
|
-
{
|
|
50
|
+
{
|
|
51
|
+
...params,
|
|
52
|
+
requestDigestCandidates: params.requestDigestCandidates ?? [params.requestDigest],
|
|
53
|
+
issuerProfileId: params.aggregate.issuerProfileId,
|
|
54
|
+
}
|
|
50
55
|
);
|
|
51
56
|
const props = result.records[0]?.get('ic')?.properties;
|
|
52
57
|
if (!props)
|
|
@@ -87,6 +92,7 @@ export const advancePendingInboxRefresh = async (params: {
|
|
|
87
92
|
materialDigest: string;
|
|
88
93
|
idempotencyKey?: string;
|
|
89
94
|
updateSummary?: string;
|
|
95
|
+
updateReason?: import('@learncard/types').CredentialRefreshUpdateReason;
|
|
90
96
|
}): Promise<PublishCredentialRefreshResult | null> => {
|
|
91
97
|
const { refreshId, expectedVersion, idempotencyKey } = params;
|
|
92
98
|
const replay = await getPendingInboxPublication(refreshId, idempotencyKey);
|
|
@@ -101,11 +107,13 @@ export const advancePendingInboxRefresh = async (params: {
|
|
|
101
107
|
AND ic.currentStatus = 'PENDING' AND datetime(ic.expiresAt) > datetime()
|
|
102
108
|
AND ($key IS NULL OR NOT EXISTS { MATCH (:InboxRefreshPublication {refreshIdempotencyKey: $key}) })
|
|
103
109
|
CREATE (p:InboxRefreshPublication $publication)
|
|
104
|
-
SET ic.credential = $credential,
|
|
110
|
+
SET ic.credential = $credential,
|
|
111
|
+
ic.credentialName = coalesce($credentialName, ic.credentialName),
|
|
105
112
|
refresh.currentVersion = $version, refresh.materialDigest = $materialDigest,
|
|
106
113
|
refresh.credentialDisplayName = coalesce($credentialName, refresh.credentialDisplayName),
|
|
107
114
|
refresh.lastPublishedAt = $publishedAt, refresh.updatedAt = $publishedAt,
|
|
108
|
-
|
|
115
|
+
refresh.updateSummary = $updateSummary,
|
|
116
|
+
refresh.updateReason = $updateReason
|
|
109
117
|
RETURN p`,
|
|
110
118
|
{
|
|
111
119
|
...params,
|
|
@@ -113,6 +121,7 @@ export const advancePendingInboxRefresh = async (params: {
|
|
|
113
121
|
publishedAt,
|
|
114
122
|
credentialName: params.credentialName ?? null,
|
|
115
123
|
updateSummary: params.updateSummary ?? null,
|
|
124
|
+
updateReason: params.updateReason ?? null,
|
|
116
125
|
key: idempotencyKey ? `${refreshId}:${idempotencyKey}` : null,
|
|
117
126
|
publication: Object.fromEntries(
|
|
118
127
|
Object.entries({
|
|
@@ -123,6 +132,7 @@ export const advancePendingInboxRefresh = async (params: {
|
|
|
123
132
|
publishedAt,
|
|
124
133
|
signingMode: 'signing-authority',
|
|
125
134
|
updateSummary: params.updateSummary,
|
|
135
|
+
updateReason: params.updateReason,
|
|
126
136
|
refreshIdempotencyKey: idempotencyKey
|
|
127
137
|
? `${refreshId}:${idempotencyKey}`
|
|
128
138
|
: undefined,
|