@learncard/network-brain-service 4.1.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/CHANGELOG.md +164 -0
- package/README.md +27 -1
- package/bootstrapLambda.test.ts +70 -32
- package/config/config.dev.json +1 -0
- package/config/config.example.json +2 -1
- package/config/config.production.json +1 -0
- package/config/config.scouts.dev.json +1 -0
- package/config/config.scouts.production.json +1 -0
- package/consentFlowDeadlineLambda.test.ts +108 -0
- package/consentFlowDeadlineLambda.ts +18 -0
- package/consentFlowDeadlineLambdaApp.ts +14 -0
- package/dist/accesslayer/boost/create.d.ts +8 -2
- package/dist/accesslayer/boost/create.d.ts.map +1 -1
- package/dist/accesslayer/campaign/audience.d.ts +4 -4
- package/dist/accesslayer/campaign/audience.d.ts.map +1 -1
- package/dist/accesslayer/campaign/create.d.ts +1 -1
- package/dist/accesslayer/campaign/create.d.ts.map +1 -1
- package/dist/accesslayer/campaign/delivery.d.ts +5 -0
- package/dist/accesslayer/campaign/delivery.d.ts.map +1 -0
- package/dist/accesslayer/campaign/dispatch.d.ts +5 -0
- package/dist/accesslayer/campaign/dispatch.d.ts.map +1 -0
- package/dist/accesslayer/campaign/dispatch.test.d.ts +2 -0
- package/dist/accesslayer/campaign/dispatch.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/email.d.ts +10 -0
- package/dist/accesslayer/campaign/email.d.ts.map +1 -0
- package/dist/accesslayer/campaign/email.test.d.ts +2 -0
- package/dist/accesslayer/campaign/email.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/preferences.d.ts +5 -0
- package/dist/accesslayer/campaign/preferences.d.ts.map +1 -0
- package/dist/accesslayer/campaign/read.d.ts.map +1 -1
- package/dist/accesslayer/campaign/test.d.ts +5 -0
- package/dist/accesslayer/campaign/test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/test.test.d.ts +2 -0
- package/dist/accesslayer/campaign/test.test.d.ts.map +1 -0
- package/dist/accesslayer/campaign/update.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/delete.d.ts +1 -1
- package/dist/accesslayer/consentflowcontract/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +2 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +19 -4
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +6 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/versions.d.ts +68 -0
- package/dist/accesslayer/consentflowcontract/versions.d.ts.map +1 -0
- package/dist/accesslayer/credential/create.d.ts +3 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/delete.d.ts +1 -0
- package/dist/accesslayer/credential/delete.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +15 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/read.d.ts +2 -0
- package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/create.d.ts +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +7 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -1
- package/dist/app.d.ts +11 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +61 -1
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/campaign-unsubscribe.d.ts +7 -0
- package/dist/campaign-unsubscribe.d.ts.map +1 -0
- package/dist/campaign-unsubscribe.test.d.ts +2 -0
- package/dist/campaign-unsubscribe.test.d.ts.map +1 -0
- package/dist/config/environment.d.ts +42 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/stageConfig.d.ts +4 -0
- package/dist/config/stageConfig.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/helpers/boost.helpers.d.ts +4 -1
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/campaign-app-origin.helpers.d.ts +2 -0
- package/dist/helpers/campaign-app-origin.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-email.helpers.d.ts +13 -0
- package/dist/helpers/campaign-email.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-notifications.helpers.d.ts +7 -0
- package/dist/helpers/campaign-notifications.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-queue.helpers.d.ts +7 -0
- package/dist/helpers/campaign-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/campaign-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/campaign-unsubscribe-page.d.ts +2 -0
- package/dist/helpers/campaign-unsubscribe-page.d.ts.map +1 -0
- package/dist/helpers/campaign-unsubscribe.helpers.d.ts +7 -0
- package/dist/helpers/campaign-unsubscribe.helpers.d.ts.map +1 -0
- package/dist/helpers/campaign-webhook.helpers.test.d.ts +2 -0
- package/dist/helpers/campaign-webhook.helpers.test.d.ts.map +1 -0
- package/dist/helpers/consentflow-deadline.helpers.d.ts +5 -0
- package/dist/helpers/consentflow-deadline.helpers.d.ts.map +1 -0
- package/dist/helpers/consentflow-legacy-migration.helpers.d.ts +2 -0
- package/dist/helpers/consentflow-legacy-migration.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -1
- package/dist/helpers/contract.helpers.d.ts +1 -1
- package/dist/helpers/contract.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-display.helpers.d.ts +5 -0
- package/dist/helpers/credential-display.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-display.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-display.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-refresh-materiality.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-materiality.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-refresh.helpers.d.ts +30 -1
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential-status-mutation.helpers.d.ts +5 -0
- package/dist/helpers/credential-status-mutation.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-status-mutation.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-status-mutation.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential.helpers.d.ts +1 -1
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-refresh.helpers.d.ts +4 -2
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts +1 -0
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/learnCard.helpers.d.ts +1 -0
- package/dist/helpers/learnCard.helpers.d.ts.map +1 -1
- package/dist/helpers/managed-status.helpers.d.ts +7 -0
- package/dist/helpers/managed-status.helpers.d.ts.map +1 -0
- package/dist/helpers/managed-status.helpers.test.d.ts +2 -0
- package/dist/helpers/managed-status.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts +2 -0
- package/dist/helpers/notification-delivery-boundary.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notification-webhook-fetch.helpers.d.ts +13 -0
- package/dist/helpers/notification-webhook-fetch.helpers.d.ts.map +1 -0
- package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts +2 -0
- package/dist/helpers/notification-webhook-fetch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +13 -5
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.test.d.ts +2 -0
- package/dist/helpers/refresh-send-intent.helpers.test.d.ts.map +1 -0
- package/dist/helpers/send-contract.helpers.d.ts +11 -0
- package/dist/helpers/send-contract.helpers.d.ts.map +1 -0
- package/dist/helpers/status-list.helpers.d.ts +12 -1
- package/dist/helpers/status-list.helpers.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowContractVersion.d.ts +12 -0
- package/dist/models/ConsentFlowContractVersion.d.ts.map +1 -0
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/Credential.d.ts +2 -0
- package/dist/models/Credential.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -0
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +1 -1
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/campaign-constraints.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/routes/activity.d.ts +15 -5
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts +15 -5
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +12 -2
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/bench.d.ts +11 -1
- package/dist/routes/bench.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +70 -15
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/campaigns.d.ts +183 -51
- package/dist/routes/campaigns.d.ts.map +1 -1
- package/dist/routes/claim-hooks.d.ts +11 -1
- package/dist/routes/claim-hooks.d.ts.map +1 -1
- package/dist/routes/contact-methods.d.ts +11 -1
- package/dist/routes/contact-methods.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +374 -8
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-input-privacy.test.d.ts +2 -0
- package/dist/routes/credential-input-privacy.test.d.ts.map +1 -0
- package/dist/routes/credential-refreshes.d.ts +210 -1
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credential-share-previews.d.ts +11 -1
- package/dist/routes/credential-share-previews.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +132 -3
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/did-metadata.d.ts +11 -1
- package/dist/routes/did-metadata.d.ts.map +1 -1
- package/dist/routes/federation.d.ts +11 -1
- package/dist/routes/federation.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +54 -19
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +100 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/integrations.d.ts +14 -4
- package/dist/routes/integrations.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +11 -1
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts +11 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts +11 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +22 -2
- package/dist/routes/public-share-links.d.ts.map +1 -1
- package/dist/routes/share-links.d.ts +36 -16
- package/dist/routes/share-links.d.ts.map +1 -1
- package/dist/routes/skill-frameworks.d.ts +11 -1
- package/dist/routes/skill-frameworks.d.ts.map +1 -1
- package/dist/routes/skills.d.ts +11 -1
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/storage.d.ts +37 -23
- package/dist/routes/storage.d.ts.map +1 -1
- package/dist/routes/test.d.ts +79 -3
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/utilities.d.ts +11 -1
- package/dist/routes/utilities.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts +11 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts +3 -0
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/delivery.service.d.ts +10 -0
- package/dist/services/delivery/delivery.service.d.ts.map +1 -1
- package/dist/types/activity.d.ts +3 -3
- package/dist/types/app-manifest.d.ts +10 -10
- package/dist/types/campaign.d.ts +13 -1
- package/dist/types/campaign.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +175 -7
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +27 -0
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/credential.d.ts +5 -0
- package/dist/types/credential.d.ts.map +1 -1
- package/dist/types/integration.d.ts +3 -3
- package/lambda.ts +44 -14
- package/lambdaApp.ts +20 -0
- package/package.json +20 -19
- package/project.json +5 -0
- package/scripts/migrate-consent-flow-versions.ts +9 -0
- package/scripts/report-send-template-exposure.ts +47 -0
- package/scripts/verify-python-contracts.ts +16 -1
- package/serverless-local.yml +15 -2
- package/serverless.function-env.cjs +4 -0
- package/serverless.local.yml +12 -0
- package/serverless.yml +48 -3
- package/src/accesslayer/boost/create.ts +62 -7
- package/src/accesslayer/campaign/README.md +275 -6
- package/src/accesslayer/campaign/audience.ts +39 -7
- package/src/accesslayer/campaign/create.ts +4 -2
- package/src/accesslayer/campaign/delivery.ts +66 -0
- package/src/accesslayer/campaign/dispatch.test.ts +59 -0
- package/src/accesslayer/campaign/dispatch.ts +172 -0
- package/src/accesslayer/campaign/email.test.ts +73 -0
- package/src/accesslayer/campaign/email.ts +91 -0
- package/src/accesslayer/campaign/preferences.ts +53 -0
- package/src/accesslayer/campaign/read.ts +3 -1
- package/src/accesslayer/campaign/state.ts +1 -1
- package/src/accesslayer/campaign/test.test.ts +31 -0
- package/src/accesslayer/campaign/test.ts +75 -0
- package/src/accesslayer/campaign/update.ts +15 -4
- package/src/accesslayer/consentflowcontract/create.ts +20 -1
- package/src/accesslayer/consentflowcontract/delete.ts +121 -3
- package/src/accesslayer/consentflowcontract/read.ts +15 -9
- package/src/accesslayer/consentflowcontract/relationships/create.ts +70 -36
- package/src/accesslayer/consentflowcontract/relationships/read.ts +305 -73
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +1 -1
- package/src/accesslayer/consentflowcontract/relationships/update.ts +269 -98
- package/src/accesslayer/consentflowcontract/versions.ts +790 -0
- package/src/accesslayer/credential/create.ts +33 -9
- package/src/accesslayer/credential/delete.ts +45 -0
- package/src/accesslayer/credential/relationships/create.ts +102 -47
- package/src/accesslayer/credential/relationships/update.ts +8 -116
- package/src/accesslayer/credential-refresh/read.ts +26 -2
- package/src/accesslayer/credential-refresh/update.ts +6 -2
- package/src/accesslayer/inbox-credential/create.ts +7 -1
- package/src/accesslayer/inbox-credential/refresh.ts +14 -4
- package/src/campaign-unsubscribe.test.ts +158 -0
- package/src/campaign-unsubscribe.ts +86 -0
- package/src/config/environment.test.ts +52 -0
- package/src/config/environment.ts +58 -0
- package/src/config/serverlessFunctionEnv.test.ts +59 -0
- package/src/credential-refresh.ts +1 -0
- package/src/docker-entry.ts +26 -1
- package/src/helpers/boost-storage.helpers.test.ts +107 -41
- package/src/helpers/boost.helpers.ts +88 -40
- package/src/helpers/campaign-app-origin.helpers.ts +25 -0
- package/src/helpers/campaign-email.helpers.ts +113 -0
- package/src/helpers/campaign-notifications.helpers.ts +127 -0
- package/src/helpers/campaign-queue.helpers.test.ts +442 -0
- package/src/helpers/campaign-queue.helpers.ts +122 -0
- package/src/helpers/campaign-unsubscribe-page.ts +107 -0
- package/src/helpers/campaign-unsubscribe.helpers.ts +56 -0
- package/src/helpers/campaign-webhook.helpers.test.ts +137 -0
- package/src/helpers/consentflow-deadline.helpers.ts +22 -0
- package/src/helpers/consentflow-legacy-migration.helpers.ts +179 -0
- package/src/helpers/contract-events.helpers.ts +35 -3
- package/src/helpers/contract-requests.helpers.ts +1 -1
- package/src/helpers/contract.helpers.ts +5 -1
- package/src/helpers/credential-display.helpers.test.ts +68 -0
- package/src/helpers/credential-display.helpers.ts +15 -0
- package/src/helpers/credential-refresh-email.helpers.ts +5 -12
- package/src/helpers/credential-refresh-materiality.helpers.test.ts +138 -1
- package/src/helpers/credential-refresh-materiality.helpers.ts +62 -1
- package/src/helpers/credential-refresh-proof.helpers.test.ts +1 -1
- package/src/helpers/credential-refresh-proof.helpers.ts +2 -2
- package/src/helpers/credential-refresh.helpers.ts +1166 -376
- package/src/helpers/credential-status-mutation.helpers.test.ts +155 -0
- package/src/helpers/credential-status-mutation.helpers.ts +166 -0
- package/src/helpers/credential.helpers.ts +19 -11
- package/src/helpers/finalize-inbox.helpers.ts +1 -1
- package/src/helpers/inbox-refresh.helpers.ts +34 -8
- package/src/helpers/inbox.helpers.ts +2 -0
- package/src/helpers/learnCard.helpers.ts +23 -0
- package/src/helpers/managed-status.helpers.test.ts +101 -0
- package/src/helpers/managed-status.helpers.ts +147 -0
- package/src/helpers/notification-delivery-boundary.helpers.test.ts +114 -0
- package/src/helpers/notification-webhook-fetch.helpers.test.ts +228 -0
- package/src/helpers/notification-webhook-fetch.helpers.ts +166 -0
- package/src/helpers/notificationQueue.helpers.ts +5 -0
- package/src/helpers/notifications.helpers.ts +19 -12
- package/src/helpers/refresh-send-intent.helpers.test.ts +60 -0
- package/src/helpers/refresh-send-intent.helpers.ts +81 -18
- package/src/helpers/send-contract.helpers.ts +36 -0
- package/src/helpers/status-list.helpers.test.ts +29 -12
- package/src/helpers/status-list.helpers.ts +329 -20
- package/src/models/ConsentFlowContract.ts +3 -0
- package/src/models/ConsentFlowContractVersion.ts +50 -0
- package/src/models/ConsentFlowTerms.ts +4 -0
- package/src/models/Credential.ts +2 -0
- package/src/models/CredentialRefresh.ts +2 -0
- package/src/models/Profile.ts +2 -0
- package/src/models/campaign-constraints.ts +13 -8
- package/src/models/index.ts +2 -0
- package/src/openapi.ts +21 -0
- package/src/routes/app-store.ts +2 -1
- package/src/routes/boosts.ts +219 -137
- package/src/routes/campaigns.ts +98 -19
- package/src/routes/contracts.ts +433 -115
- package/src/routes/credential-input-privacy.test.ts +92 -0
- package/src/routes/credential-refreshes.ts +273 -2
- package/src/routes/federation.ts +7 -2
- package/src/routes/inbox.ts +6 -1
- package/src/routes/index.ts +31 -2
- package/src/routes/workflows.ts +61 -46
- package/src/services/delivery/adapters/postmark.adapter.test.ts +70 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +84 -11
- package/src/services/delivery/delivery.service.ts +4 -0
- package/src/types/campaign.ts +14 -1
- package/src/types/consentflowcontract.ts +43 -2
- package/src/types/credential-refresh.ts +11 -1
- package/src/types/credential.ts +2 -0
- package/test/boost-status-cleanup.spec.ts +123 -0
- package/test/campaigns-email.unit.spec.ts +268 -0
- package/test/campaigns.spec.ts +804 -30
- package/test/consent-autoboost-idempotency.spec.ts +432 -0
- package/test/consent-data-boundaries.spec.ts +322 -26
- package/test/consent-legacy-migration.spec.ts +315 -0
- package/test/consent-recipients.spec.ts +7 -7
- package/test/consent-version-updates.spec.ts +761 -0
- package/test/consentflow.spec.ts +698 -226
- package/test/contract-requests.spec.ts +55 -9
- package/test/credential-refresh-allocation.spec.ts +212 -14
- package/test/credential-refresh-email.spec.ts +6 -10
- package/test/credential-refresh-endpoint.spec.ts +3 -0
- package/test/credential-refresh-publication.spec.ts +52 -15
- package/test/credential-storage.neo4j.spec.ts +33 -6
- package/test/helpers/legacy-autoboost.ts +48 -0
- package/test/holder-mode-send.spec.ts +710 -0
- package/test/inbox-refresh.spec.ts +105 -0
- package/test/inbox-security.e2e.ts +18 -22
- package/test/managed-status-revocation.spec.ts +970 -0
- package/test/notifications-worker.spec.ts +31 -25
- package/test/refresh-send.spec.ts +334 -5
- package/test/smart-resume-consent.spec.ts +1 -1
- package/test/verification/campaigns.ts +40 -0
- package/vitest.campaigns.config.ts +2 -0
- package/vitest.config.ts +14 -0
package/test/consentflow.spec.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
SigningAuthority,
|
|
38
38
|
} from '@models';
|
|
39
39
|
import { testUnsignedBoost } from './helpers/send';
|
|
40
|
+
import { seedLegacyAutoBoost } from './helpers/legacy-autoboost';
|
|
40
41
|
import { BindParam, QueryBuilder } from 'neogma';
|
|
41
42
|
import {
|
|
42
43
|
getContractTermsByUri,
|
|
@@ -44,14 +45,33 @@ import {
|
|
|
44
45
|
hasGuardianApprovalHistory,
|
|
45
46
|
} from '@accesslayer/consentflowcontract/relationships/read';
|
|
46
47
|
import { consentToContract } from '@accesslayer/consentflowcontract/relationships/create';
|
|
47
|
-
import { updateTerms } from '@accesslayer/consentflowcontract/relationships/update';
|
|
48
48
|
|
|
49
|
+
import {
|
|
50
|
+
getConsentFlowContractVersionRecord,
|
|
51
|
+
updateConsentFlowContractVersion,
|
|
52
|
+
} from '@accesslayer/consentflowcontract/versions';
|
|
53
|
+
import { neogma } from '@instance';
|
|
54
|
+
import { migrateLegacyConsentFlowContracts } from '../src/helpers/consentflow-legacy-migration.helpers';
|
|
55
|
+
import { constructUri, getIdFromUri } from '@helpers/uri.helpers';
|
|
56
|
+
import type { ConsentFlowTerms as ConsentFlowTermsType } from '@learncard/types';
|
|
49
57
|
const noAuthClient = getClient();
|
|
50
58
|
let userA: Awaited<ReturnType<typeof getUser>>;
|
|
51
59
|
let userB: Awaited<ReturnType<typeof getUser>>;
|
|
52
60
|
let userC: Awaited<ReturnType<typeof getUser>>;
|
|
53
61
|
let userD: Awaited<ReturnType<typeof getUser>>;
|
|
54
62
|
|
|
63
|
+
const seedStoredContractExpiry = async (
|
|
64
|
+
contractId: string,
|
|
65
|
+
version: number,
|
|
66
|
+
expiresAt: string | null
|
|
67
|
+
) => {
|
|
68
|
+
await neogma.queryRunner.run(
|
|
69
|
+
`MATCH (contract:ConsentFlowContract {id:$contractId})-[:HAS_VERSION]->(accepted:ConsentFlowContractVersion {version:$version})
|
|
70
|
+
SET contract.expiresAt = $expiresAt, accepted.expiresAt = $expiresAt`,
|
|
71
|
+
{ contractId, version, expiresAt }
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
55
75
|
describe('Consent Flow Contracts', () => {
|
|
56
76
|
beforeAll(async () => {
|
|
57
77
|
userA = await getUser();
|
|
@@ -112,6 +132,90 @@ describe('Consent Flow Contracts', () => {
|
|
|
112
132
|
expect(result).toBeDefined();
|
|
113
133
|
expect(typeof result).toBe('string');
|
|
114
134
|
});
|
|
135
|
+
it('rejects a signing result when consent is suspended while issuance is in flight', async () => {
|
|
136
|
+
await userA.clients.fullAuth.profile.registerSigningAuthority({
|
|
137
|
+
endpoint: 'http://localhost:5000/api',
|
|
138
|
+
name: 'mysa',
|
|
139
|
+
did: 'did:key:z6MkitsQTk2GDNYXAFckVcQHtC68S9j9ruVFYWrixM6RG5Mw',
|
|
140
|
+
});
|
|
141
|
+
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
142
|
+
credential: testUnsignedBoost,
|
|
143
|
+
category: 'Achievement',
|
|
144
|
+
name: 'Delayed signing boost',
|
|
145
|
+
});
|
|
146
|
+
const contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
147
|
+
contract: normalContract,
|
|
148
|
+
name: 'Delayed signing contract',
|
|
149
|
+
});
|
|
150
|
+
const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
151
|
+
contractUri,
|
|
152
|
+
terms: normalFullTerms,
|
|
153
|
+
});
|
|
154
|
+
const userBProfile = await userB.clients.fullAuth.profile.getProfile();
|
|
155
|
+
let announceStarted!: () => void;
|
|
156
|
+
let releaseSigning!: () => void;
|
|
157
|
+
const started = new Promise<void>(resolve => {
|
|
158
|
+
announceStarted = resolve;
|
|
159
|
+
});
|
|
160
|
+
const signingGate = new Promise<void>(resolve => {
|
|
161
|
+
releaseSigning = resolve;
|
|
162
|
+
});
|
|
163
|
+
const issueCredential = signingAuthorityMocks.issueCredential.getMockImplementation();
|
|
164
|
+
if (!issueCredential) throw new Error('Real signer was not initialized');
|
|
165
|
+
signingAuthorityMocks.issueCredential.mockImplementation(async (...args) => {
|
|
166
|
+
announceStarted();
|
|
167
|
+
await signingGate;
|
|
168
|
+
return issueCredential(...args);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const write =
|
|
173
|
+
userA.clients.fullAuth.contracts.writeCredentialToContractViaSigningAuthority({
|
|
174
|
+
did: userBProfile!.did,
|
|
175
|
+
contractUri,
|
|
176
|
+
boostUri,
|
|
177
|
+
signingAuthority: { endpoint: 'http://localhost:5000/api', name: 'MySA' },
|
|
178
|
+
});
|
|
179
|
+
await started;
|
|
180
|
+
const changedContract = {
|
|
181
|
+
...normalContract,
|
|
182
|
+
read: {
|
|
183
|
+
...normalContract.read,
|
|
184
|
+
credentials: {
|
|
185
|
+
...normalContract.read.credentials,
|
|
186
|
+
categories: {
|
|
187
|
+
...normalContract.read.credentials.categories,
|
|
188
|
+
Transcript: { required: true },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
194
|
+
uri: contractUri,
|
|
195
|
+
changes: { contract: changedContract },
|
|
196
|
+
reason: 'We need transcripts to personalize your progress report.',
|
|
197
|
+
});
|
|
198
|
+
releaseSigning();
|
|
199
|
+
await expect(write).rejects.toMatchObject({
|
|
200
|
+
code: 'FORBIDDEN',
|
|
201
|
+
});
|
|
202
|
+
const terms = await getContractTermsByUri(termsUri);
|
|
203
|
+
expect(terms).toBeDefined();
|
|
204
|
+
const transactions = await getTransactionsForTerms(terms!.terms.id, { limit: 100 });
|
|
205
|
+
expect(transactions.some(transaction => transaction.action === 'write')).toBe(
|
|
206
|
+
false
|
|
207
|
+
);
|
|
208
|
+
const deliveries = await userB.clients.fullAuth.contracts.getCredentialsForContract(
|
|
209
|
+
{
|
|
210
|
+
termsUri,
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
expect(deliveries.records).toEqual([]);
|
|
214
|
+
} finally {
|
|
215
|
+
releaseSigning();
|
|
216
|
+
signingAuthorityMocks.issueCredential.mockImplementation(issueCredential);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
115
219
|
});
|
|
116
220
|
|
|
117
221
|
describe('createConsentFlowContract', () => {
|
|
@@ -156,6 +260,75 @@ describe('Consent Flow Contracts', () => {
|
|
|
156
260
|
})
|
|
157
261
|
).resolves.not.toThrow();
|
|
158
262
|
});
|
|
263
|
+
it('persists immutable versions and leaves dry runs untouched', async () => {
|
|
264
|
+
const uri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
265
|
+
contract: minimalContract,
|
|
266
|
+
name: 'Versioned test contract',
|
|
267
|
+
});
|
|
268
|
+
const id = getIdFromUri(uri);
|
|
269
|
+
const initial = await getConsentFlowContractVersionRecord(id, 1);
|
|
270
|
+
expect(initial?.name).toBe('Versioned test contract');
|
|
271
|
+
|
|
272
|
+
const changes = {
|
|
273
|
+
contract: {
|
|
274
|
+
write: {
|
|
275
|
+
credentials: {
|
|
276
|
+
categories: {
|
|
277
|
+
OptionalAddition: { required: false, defaultEnabled: true },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
const preview = await updateConsentFlowContractVersion(
|
|
284
|
+
{ uri, changes, dryRun: true },
|
|
285
|
+
'usera',
|
|
286
|
+
'localhost'
|
|
287
|
+
);
|
|
288
|
+
expect(preview).toMatchObject({ version: 2, kind: 'expanded_optional' });
|
|
289
|
+
expect(await getConsentFlowContractVersionRecord(id, 2)).toBeNull();
|
|
290
|
+
|
|
291
|
+
const updated = await updateConsentFlowContractVersion(
|
|
292
|
+
{ uri, changes },
|
|
293
|
+
'usera',
|
|
294
|
+
'localhost'
|
|
295
|
+
);
|
|
296
|
+
expect(updated.version).toBe(2);
|
|
297
|
+
expect((await getConsentFlowContractVersionRecord(id, 1))?.contract).toEqual(
|
|
298
|
+
initial?.contract
|
|
299
|
+
);
|
|
300
|
+
expect(
|
|
301
|
+
(await getConsentFlowContractVersionRecord(id, 2))?.contract.write.credentials
|
|
302
|
+
.categories
|
|
303
|
+
).toHaveProperty('OptionalAddition');
|
|
304
|
+
});
|
|
305
|
+
it('migrates a legacy contract snapshot idempotently from persisted Neo4j state', async () => {
|
|
306
|
+
const uri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
307
|
+
contract: normalContract,
|
|
308
|
+
name: 'Legacy migration contract',
|
|
309
|
+
});
|
|
310
|
+
const id = getIdFromUri(uri);
|
|
311
|
+
await neogma.queryRunner.run(
|
|
312
|
+
`MATCH (contract:ConsentFlowContract {id:$id})-[relationship:HAS_VERSION]->(version:ConsentFlowContractVersion)
|
|
313
|
+
WITH contract, relationship, version
|
|
314
|
+
DELETE relationship, version
|
|
315
|
+
WITH contract
|
|
316
|
+
SET contract.currentVersion=null`,
|
|
317
|
+
{ id }
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
await migrateLegacyConsentFlowContracts();
|
|
321
|
+
const migrated = await getConsentFlowContractVersionRecord(id, 1);
|
|
322
|
+
expect(migrated?.contract).toEqual(normalContract);
|
|
323
|
+
await migrateLegacyConsentFlowContracts();
|
|
324
|
+
|
|
325
|
+
const count = await neogma.queryRunner.run(
|
|
326
|
+
`MATCH (:ConsentFlowContract {id:$id})-[:HAS_VERSION]->(version:ConsentFlowContractVersion)
|
|
327
|
+
RETURN count(version) AS count`,
|
|
328
|
+
{ id }
|
|
329
|
+
);
|
|
330
|
+
expect(Number(count.records[0]?.get('count'))).toBe(1);
|
|
331
|
+
});
|
|
159
332
|
|
|
160
333
|
it('should allow creating a contract with an auto-boost', async () => {
|
|
161
334
|
await userA.clients.fullAuth.profile.registerSigningAuthority({
|
|
@@ -612,6 +785,278 @@ describe('Consent Flow Contracts', () => {
|
|
|
612
785
|
});
|
|
613
786
|
});
|
|
614
787
|
|
|
788
|
+
describe('versioned consent-flow updates', () => {
|
|
789
|
+
let uri: string;
|
|
790
|
+
let termsUri: string;
|
|
791
|
+
|
|
792
|
+
beforeEach(async () => {
|
|
793
|
+
await Profile.delete({ detach: true, where: {} });
|
|
794
|
+
await ConsentFlowContract.delete({ detach: true, where: {} });
|
|
795
|
+
await ConsentFlowTransaction.delete({ detach: true, where: {} });
|
|
796
|
+
await ConsentFlowTerms.delete({ detach: true, where: {} });
|
|
797
|
+
await userA.clients.fullAuth.profile.createProfile({ profileId: 'usera' });
|
|
798
|
+
await userB.clients.fullAuth.profile.createProfile({ profileId: 'userb' });
|
|
799
|
+
uri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
800
|
+
contract: normalContract,
|
|
801
|
+
name: 'Versioned contract',
|
|
802
|
+
});
|
|
803
|
+
({ termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
804
|
+
contractUri: uri,
|
|
805
|
+
terms: normalFullTerms,
|
|
806
|
+
}));
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('keeps empty requests readable through optional additions and removal of the last category', async () => {
|
|
810
|
+
const emptyContract = {
|
|
811
|
+
read: { personal: {}, credentials: { categories: {} } },
|
|
812
|
+
write: { personal: {}, credentials: { categories: {} } },
|
|
813
|
+
};
|
|
814
|
+
const emptyTerms = {
|
|
815
|
+
read: { personal: {}, credentials: { categories: {} } },
|
|
816
|
+
write: { personal: {}, credentials: { categories: {} } },
|
|
817
|
+
};
|
|
818
|
+
const emptyUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
819
|
+
name: 'Empty request',
|
|
820
|
+
contract: emptyContract,
|
|
821
|
+
});
|
|
822
|
+
expect(
|
|
823
|
+
(await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri: emptyUri }))
|
|
824
|
+
.contract
|
|
825
|
+
).toEqual(emptyContract);
|
|
826
|
+
const { termsUri: emptyTermsUri } =
|
|
827
|
+
await userB.clients.fullAuth.contracts.consentToContract({
|
|
828
|
+
contractUri: emptyUri,
|
|
829
|
+
terms: emptyTerms,
|
|
830
|
+
});
|
|
831
|
+
await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
832
|
+
uri: emptyUri,
|
|
833
|
+
changes: {
|
|
834
|
+
contract: {
|
|
835
|
+
write: {
|
|
836
|
+
credentials: { categories: { Certificate: { required: false } } },
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
const optional = await userB.clients.fullAuth.contracts.getConsentedContracts();
|
|
842
|
+
expect(
|
|
843
|
+
optional.records.find(record => record.uri === emptyTermsUri)?.terms.write
|
|
844
|
+
.credentials.categories.Certificate
|
|
845
|
+
).toBe(false);
|
|
846
|
+
const removal = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
847
|
+
uri: emptyUri,
|
|
848
|
+
changes: {
|
|
849
|
+
contract: { write: { credentials: { categories: { Certificate: null } } } },
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
expect(removal.kind).toBe('relaxed');
|
|
853
|
+
expect(
|
|
854
|
+
(await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri: emptyUri }))
|
|
855
|
+
.contract
|
|
856
|
+
).toEqual(emptyContract);
|
|
857
|
+
expect(
|
|
858
|
+
(await userA.clients.fullAuth.contracts.getConsentFlowContracts()).records
|
|
859
|
+
).toEqual(
|
|
860
|
+
expect.arrayContaining([expect.objectContaining({ uri: emptyUri, version: 3 })])
|
|
861
|
+
);
|
|
862
|
+
const records = await userB.clients.fullAuth.contracts.getConsentedContracts();
|
|
863
|
+
expect(records.records.find(record => record.uri === emptyTermsUri)).toMatchObject({
|
|
864
|
+
status: 'live',
|
|
865
|
+
consentedVersion: 3,
|
|
866
|
+
terms: emptyTerms,
|
|
867
|
+
});
|
|
868
|
+
expect(
|
|
869
|
+
await userA.clients.fullAuth.contracts.verifyConsent({
|
|
870
|
+
uri: emptyUri,
|
|
871
|
+
profileId: 'userb',
|
|
872
|
+
})
|
|
873
|
+
).toBe(true);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('previews without writing, requires a reason for expanded access, and blocks stale acceptance', async () => {
|
|
877
|
+
const changedContract = {
|
|
878
|
+
...normalContract,
|
|
879
|
+
read: {
|
|
880
|
+
...normalContract.read,
|
|
881
|
+
credentials: {
|
|
882
|
+
...normalContract.read.credentials,
|
|
883
|
+
categories: {
|
|
884
|
+
...normalContract.read.credentials.categories,
|
|
885
|
+
Transcript: { required: true },
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
};
|
|
890
|
+
const preview = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
891
|
+
uri,
|
|
892
|
+
changes: { contract: changedContract },
|
|
893
|
+
reason: 'We need transcripts to personalize your progress report.',
|
|
894
|
+
dryRun: true,
|
|
895
|
+
});
|
|
896
|
+
expect(preview.kind).toBe('requires_consent');
|
|
897
|
+
expect(preview.version).toBe(2);
|
|
898
|
+
expect(
|
|
899
|
+
await userA.clients.fullAuth.contracts.getContractVersions({ uri })
|
|
900
|
+
).toHaveLength(1);
|
|
901
|
+
|
|
902
|
+
await expect(
|
|
903
|
+
userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
904
|
+
uri,
|
|
905
|
+
changes: { contract: changedContract },
|
|
906
|
+
})
|
|
907
|
+
).rejects.toMatchObject({
|
|
908
|
+
code: 'BAD_REQUEST',
|
|
909
|
+
message: 'A reason is required when asking users for more access',
|
|
910
|
+
});
|
|
911
|
+
expect(
|
|
912
|
+
await userA.clients.fullAuth.contracts.getContractVersions({ uri })
|
|
913
|
+
).toHaveLength(1);
|
|
914
|
+
|
|
915
|
+
const result = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
916
|
+
uri,
|
|
917
|
+
changes: { contract: changedContract },
|
|
918
|
+
reason: 'We need transcripts to personalize your progress report.',
|
|
919
|
+
});
|
|
920
|
+
expect(result.version).toBe(2);
|
|
921
|
+
expect(result.affectedUsers).toBe(1);
|
|
922
|
+
const status = await userB.clients.fullAuth.contracts.getConsentStatus({
|
|
923
|
+
uri,
|
|
924
|
+
profileId: 'userb',
|
|
925
|
+
});
|
|
926
|
+
expect(status.status).toBe('needs_reconsent');
|
|
927
|
+
expect(status.consentedVersion).toBe(1);
|
|
928
|
+
expect(status.currentVersion).toBe(2);
|
|
929
|
+
expect(status.reviewUrl).toContain('/consent-flow/review?uri=');
|
|
930
|
+
expect(
|
|
931
|
+
await userB.clients.fullAuth.contracts.verifyConsent({ uri, profileId: 'userb' })
|
|
932
|
+
).toBe(false);
|
|
933
|
+
const retainedTerms = await getContractTermsByUri(termsUri);
|
|
934
|
+
expect(retainedTerms?.terms.terms).toEqual(normalFullTerms);
|
|
935
|
+
expect(retainedTerms?.terms.consentedVersion).toBe(1);
|
|
936
|
+
const retainedData = await userA.clients.fullAuth.contracts.getConsentedDataForContract(
|
|
937
|
+
{ uri }
|
|
938
|
+
);
|
|
939
|
+
expect(retainedData.records[0]?.credentials.categories.Achievement).toEqual(
|
|
940
|
+
normalFullTerms.read.credentials.categories.Achievement.shared
|
|
941
|
+
);
|
|
942
|
+
expect(retainedData.records[0]?.credentials.categories.Transcript).toBeUndefined();
|
|
943
|
+
await expect(
|
|
944
|
+
userB.clients.fullAuth.contracts.syncCredentialsToContract({
|
|
945
|
+
termsUri,
|
|
946
|
+
categories: { Achievement: ['credential:attempted-write'] },
|
|
947
|
+
})
|
|
948
|
+
).rejects.toMatchObject({
|
|
949
|
+
code: 'FORBIDDEN',
|
|
950
|
+
message: 'User has not yet approved the latest version of this contract',
|
|
951
|
+
});
|
|
952
|
+
const signedCredential = await userA.learnCard.invoke.issueCredential({
|
|
953
|
+
...testUnsignedBoost,
|
|
954
|
+
issuer: userA.learnCard.id.did(),
|
|
955
|
+
});
|
|
956
|
+
await expect(
|
|
957
|
+
userA.clients.fullAuth.contracts.writeCredentialToContract({
|
|
958
|
+
did: userB.learnCard.id.did(),
|
|
959
|
+
contractUri: uri,
|
|
960
|
+
boostUri: 'boost:pending-write',
|
|
961
|
+
credential: signedCredential,
|
|
962
|
+
})
|
|
963
|
+
).rejects.toMatchObject({
|
|
964
|
+
code: 'FORBIDDEN',
|
|
965
|
+
message: 'User has not yet approved the latest version of this contract',
|
|
966
|
+
});
|
|
967
|
+
await expect(
|
|
968
|
+
userA.clients.fullAuth.contracts.writeCredentialToContractViaSigningAuthority({
|
|
969
|
+
did: userB.learnCard.id.did(),
|
|
970
|
+
contractUri: uri,
|
|
971
|
+
boostUri: 'boost:pending-write',
|
|
972
|
+
credential: testUnsignedBoost,
|
|
973
|
+
signingAuthority: { endpoint: 'https://issuer.example', name: 'issuer' },
|
|
974
|
+
})
|
|
975
|
+
).rejects.toMatchObject({
|
|
976
|
+
code: 'FORBIDDEN',
|
|
977
|
+
message: 'User has not yet approved the latest version of this contract',
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
await expect(
|
|
981
|
+
userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
982
|
+
uri: termsUri,
|
|
983
|
+
terms: normalFullTerms,
|
|
984
|
+
acceptVersion: 1,
|
|
985
|
+
})
|
|
986
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
987
|
+
await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
988
|
+
uri,
|
|
989
|
+
changes: { subtitle: 'Updated display information' },
|
|
990
|
+
});
|
|
991
|
+
await expect(
|
|
992
|
+
userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
993
|
+
uri: termsUri,
|
|
994
|
+
terms: normalFullTerms,
|
|
995
|
+
acceptVersion: 2,
|
|
996
|
+
})
|
|
997
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
998
|
+
const stillPending = await userB.clients.fullAuth.contracts.getConsentStatus({
|
|
999
|
+
uri,
|
|
1000
|
+
profileId: 'userb',
|
|
1001
|
+
});
|
|
1002
|
+
expect(stillPending.status).toBe('needs_reconsent');
|
|
1003
|
+
expect(stillPending.consentedVersion).toBe(1);
|
|
1004
|
+
expect(stillPending.currentVersion).toBe(3);
|
|
1005
|
+
const acceptedTerms: ConsentFlowTermsType = structuredClone(normalFullTerms);
|
|
1006
|
+
acceptedTerms.read.credentials.categories.Transcript = { sharing: true, shared: [] };
|
|
1007
|
+
await expect(
|
|
1008
|
+
userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
1009
|
+
uri: termsUri,
|
|
1010
|
+
terms: acceptedTerms,
|
|
1011
|
+
acceptVersion: 3,
|
|
1012
|
+
})
|
|
1013
|
+
).resolves.toBe(true);
|
|
1014
|
+
expect((await getContractTermsByUri(termsUri))?.terms.consentedVersion).toBe(3);
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('migrates relaxed changes by pruning removed grants without requiring reapproval', async () => {
|
|
1018
|
+
const changedContract = {
|
|
1019
|
+
read: { credentials: { categories: { ID: null } } },
|
|
1020
|
+
};
|
|
1021
|
+
const result = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
1022
|
+
uri,
|
|
1023
|
+
changes: { contract: changedContract },
|
|
1024
|
+
});
|
|
1025
|
+
expect(result.kind).toBe('relaxed');
|
|
1026
|
+
const status = await userB.clients.fullAuth.contracts.getConsentStatus({
|
|
1027
|
+
uri,
|
|
1028
|
+
profileId: 'userb',
|
|
1029
|
+
});
|
|
1030
|
+
expect(status.status).toBe('live');
|
|
1031
|
+
expect(status.consentedVersion).toBe(2);
|
|
1032
|
+
const terms = await getContractTermsByUri(termsUri);
|
|
1033
|
+
expect(terms?.terms.terms.read.credentials.categories.ID).toBeUndefined();
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('keeps a newly introduced optional write field off even when defaultEnabled is true', async () => {
|
|
1037
|
+
const changedContract = {
|
|
1038
|
+
...normalContract,
|
|
1039
|
+
write: {
|
|
1040
|
+
...normalContract.write,
|
|
1041
|
+
personal: {
|
|
1042
|
+
...normalContract.write.personal,
|
|
1043
|
+
email: { required: false, defaultEnabled: true },
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
const result = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
1048
|
+
uri,
|
|
1049
|
+
changes: { contract: changedContract },
|
|
1050
|
+
});
|
|
1051
|
+
expect(result.kind).toBe('expanded_optional');
|
|
1052
|
+
|
|
1053
|
+
const terms = await getContractTermsByUri(termsUri);
|
|
1054
|
+
expect(terms?.terms.status).toBe('live');
|
|
1055
|
+
expect(terms?.terms.consentedVersion).toBe(2);
|
|
1056
|
+
expect(terms?.terms.terms.write.personal.email).not.toBe(true);
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
|
|
615
1060
|
describe('deleteConsentFlowContract', () => {
|
|
616
1061
|
let uri: string;
|
|
617
1062
|
|
|
@@ -643,35 +1088,51 @@ describe('Consent Flow Contracts', () => {
|
|
|
643
1088
|
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
644
1089
|
});
|
|
645
1090
|
|
|
646
|
-
it('should allow you to
|
|
1091
|
+
it('should allow you to retire contracts while retaining their details', async () => {
|
|
647
1092
|
const contracts = await userA.clients.fullAuth.contracts.getConsentFlowContracts();
|
|
648
|
-
|
|
649
1093
|
expect(contracts.records).toHaveLength(1);
|
|
650
1094
|
|
|
651
1095
|
await expect(
|
|
652
1096
|
userA.clients.fullAuth.contracts.deleteConsentFlowContract({ uri })
|
|
653
|
-
).resolves.
|
|
1097
|
+
).resolves.toBe(true);
|
|
654
1098
|
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
expect(
|
|
1099
|
+
const retired = await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri });
|
|
1100
|
+
expect(retired.status).toBe('retired');
|
|
1101
|
+
expect(retired.uri).toBe(uri);
|
|
658
1102
|
});
|
|
659
1103
|
|
|
660
|
-
it('
|
|
661
|
-
await userB.clients.fullAuth.contracts.consentToContract({
|
|
1104
|
+
it('withdraws consent while retaining terms and contract history', async () => {
|
|
1105
|
+
const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
662
1106
|
terms: minimalTerms,
|
|
663
1107
|
contractUri: uri,
|
|
664
1108
|
});
|
|
665
|
-
|
|
666
1109
|
const contracts = await userB.clients.fullAuth.contracts.getConsentedContracts();
|
|
667
|
-
|
|
668
1110
|
expect(contracts.records).toHaveLength(1);
|
|
669
1111
|
|
|
670
1112
|
await userA.clients.fullAuth.contracts.deleteConsentFlowContract({ uri });
|
|
671
1113
|
|
|
672
1114
|
const newContracts = await userB.clients.fullAuth.contracts.getConsentedContracts();
|
|
673
|
-
|
|
674
|
-
|
|
1115
|
+
expect(newContracts.records.find(record => record.uri === termsUri)?.status).toBe(
|
|
1116
|
+
'withdrawn'
|
|
1117
|
+
);
|
|
1118
|
+
const retainedTerms = await getContractTermsByUri(termsUri);
|
|
1119
|
+
expect(retainedTerms?.terms.status).toBe('withdrawn');
|
|
1120
|
+
expect(
|
|
1121
|
+
(await userA.clients.fullAuth.contracts.getConsentFlowContract({ uri })).status
|
|
1122
|
+
).toBe('retired');
|
|
1123
|
+
await expect(
|
|
1124
|
+
userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
1125
|
+
uri: termsUri,
|
|
1126
|
+
terms: minimalTerms,
|
|
1127
|
+
acceptVersion: 1,
|
|
1128
|
+
})
|
|
1129
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
1130
|
+
const history = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
|
|
1131
|
+
uri: termsUri,
|
|
1132
|
+
});
|
|
1133
|
+
expect(history.records.some(transaction => transaction.action === 'withdraw')).toBe(
|
|
1134
|
+
true
|
|
1135
|
+
);
|
|
675
1136
|
});
|
|
676
1137
|
});
|
|
677
1138
|
|
|
@@ -1033,7 +1494,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
1033
1494
|
expect(withdrawnData.records).toEqual([]);
|
|
1034
1495
|
});
|
|
1035
1496
|
|
|
1036
|
-
it('returns one-time selected data while
|
|
1497
|
+
it('returns one-time selected data while consumed, but not after withdrawal', async () => {
|
|
1037
1498
|
const initial = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1038
1499
|
did: userBDid,
|
|
1039
1500
|
});
|
|
@@ -1049,14 +1510,14 @@ describe('Consent Flow Contracts', () => {
|
|
|
1049
1510
|
terms: selectedTerms,
|
|
1050
1511
|
oneTime: true,
|
|
1051
1512
|
});
|
|
1052
|
-
expect((await getContractTermsByUri(termsUri))?.terms.status).toBe('
|
|
1513
|
+
expect((await getContractTermsByUri(termsUri))?.terms.status).toBe('consumed');
|
|
1053
1514
|
const shared = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1054
1515
|
did: userBDid,
|
|
1055
1516
|
});
|
|
1056
1517
|
expect(shared.records).toEqual([
|
|
1057
1518
|
expect.objectContaining({
|
|
1058
1519
|
termsUri,
|
|
1059
|
-
status: '
|
|
1520
|
+
status: 'consumed',
|
|
1060
1521
|
credentials: [{ category: 'Achievement', uri: 'achievement2' }],
|
|
1061
1522
|
personal: selectedTerms.read.personal,
|
|
1062
1523
|
}),
|
|
@@ -1088,12 +1549,20 @@ describe('Consent Flow Contracts', () => {
|
|
|
1088
1549
|
did: userBDid,
|
|
1089
1550
|
})
|
|
1090
1551
|
).records[0]?.status
|
|
1091
|
-
).toBe('
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1552
|
+
).toBe('consumed');
|
|
1553
|
+
const expiredAt = '2000-01-01T00:00:00.000Z';
|
|
1554
|
+
if (target === 'contract') {
|
|
1555
|
+
await seedStoredContractExpiry(
|
|
1556
|
+
relationship.contract.id,
|
|
1557
|
+
relationship.terms.consentedVersion ?? 1,
|
|
1558
|
+
expiredAt
|
|
1559
|
+
);
|
|
1560
|
+
} else {
|
|
1561
|
+
await ConsentFlowTerms.update(
|
|
1562
|
+
{ expiresAt: expiredAt },
|
|
1563
|
+
{ where: { id: relationship.terms.id } }
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1097
1566
|
expect(
|
|
1098
1567
|
(
|
|
1099
1568
|
await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
@@ -1104,13 +1573,13 @@ describe('Consent Flow Contracts', () => {
|
|
|
1104
1573
|
}
|
|
1105
1574
|
);
|
|
1106
1575
|
|
|
1107
|
-
it('does not treat other
|
|
1576
|
+
it('does not treat other consumed terms as a one-time grant', async () => {
|
|
1108
1577
|
const initial = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1109
1578
|
did: userBDid,
|
|
1110
1579
|
});
|
|
1111
1580
|
const relationship = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
|
|
1112
1581
|
await ConsentFlowTerms.update(
|
|
1113
|
-
{ status: '
|
|
1582
|
+
{ status: 'consumed' },
|
|
1114
1583
|
{ where: { id: relationship.terms.id } }
|
|
1115
1584
|
);
|
|
1116
1585
|
expect(
|
|
@@ -1259,8 +1728,6 @@ describe('Consent Flow Contracts', () => {
|
|
|
1259
1728
|
did: userBDid,
|
|
1260
1729
|
});
|
|
1261
1730
|
const relationship = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
|
|
1262
|
-
const model = target === 'terms' ? ConsentFlowTerms : ConsentFlowContract;
|
|
1263
|
-
const id = relationship[target].id;
|
|
1264
1731
|
|
|
1265
1732
|
for (const [expiresAt, active] of [
|
|
1266
1733
|
[null, true],
|
|
@@ -1270,10 +1737,18 @@ describe('Consent Flow Contracts', () => {
|
|
|
1270
1737
|
['2000-01-01T00:00:00.000Z', false],
|
|
1271
1738
|
['not-a-date', false],
|
|
1272
1739
|
] as const) {
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1740
|
+
if (target === 'contract') {
|
|
1741
|
+
await seedStoredContractExpiry(
|
|
1742
|
+
relationship.contract.id,
|
|
1743
|
+
relationship.terms.consentedVersion ?? 1,
|
|
1744
|
+
expiresAt
|
|
1745
|
+
);
|
|
1746
|
+
} else {
|
|
1747
|
+
await ConsentFlowTerms.update(
|
|
1748
|
+
{ expiresAt },
|
|
1749
|
+
{ where: { id: relationship.terms.id } }
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1277
1752
|
const data = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1278
1753
|
did: userBDid,
|
|
1279
1754
|
});
|
|
@@ -1292,28 +1767,38 @@ describe('Consent Flow Contracts', () => {
|
|
|
1292
1767
|
});
|
|
1293
1768
|
const original = initial.records[0]!;
|
|
1294
1769
|
const relationship = (await getContractTermsByUri(original.termsUri))!;
|
|
1295
|
-
await
|
|
1296
|
-
|
|
1770
|
+
const changed = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
1771
|
+
uri: constructUri('contract', relationship.contract.id, 'localhost%3A3000'),
|
|
1772
|
+
changes: {
|
|
1297
1773
|
reasonForAccessing: 'Personalized learning',
|
|
1298
1774
|
expiresAt: '2999-01-01T00:00:00.000Z',
|
|
1299
1775
|
},
|
|
1300
|
-
|
|
1301
|
-
);
|
|
1776
|
+
reason: 'The contract metadata was updated.',
|
|
1777
|
+
});
|
|
1302
1778
|
await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
1303
1779
|
uri: original.termsUri,
|
|
1304
1780
|
terms: normalAchievementOnlyTerms,
|
|
1781
|
+
acceptVersion: changed.version,
|
|
1305
1782
|
});
|
|
1306
1783
|
const updated = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1307
1784
|
did: userBDid,
|
|
1308
1785
|
});
|
|
1309
1786
|
expect(updated.records[0]).toMatchObject({
|
|
1310
1787
|
createdAt: original.createdAt,
|
|
1311
|
-
contractUpdatedAt:
|
|
1788
|
+
contractUpdatedAt: (await getConsentFlowContractVersionRecord(
|
|
1789
|
+
relationship.contract.id,
|
|
1790
|
+
changed.version
|
|
1791
|
+
))!.createdAt,
|
|
1312
1792
|
contractExpiresAt: '2999-01-01T00:00:00.000Z',
|
|
1313
1793
|
reasonForAccessing: 'Personalized learning',
|
|
1314
1794
|
guardian: { required: false, approved: false },
|
|
1315
1795
|
terms: normalAchievementOnlyTerms,
|
|
1316
1796
|
});
|
|
1797
|
+
expect(
|
|
1798
|
+
(await getTransactionsForTerms(relationship.terms.id, { limit: 100 })).find(
|
|
1799
|
+
transaction => transaction.action === 'consent'
|
|
1800
|
+
)?.date
|
|
1801
|
+
).toBe(original.createdAt);
|
|
1317
1802
|
expect(Date.parse(updated.records[0]!.date)).toBeGreaterThanOrEqual(
|
|
1318
1803
|
Date.parse(original.date)
|
|
1319
1804
|
);
|
|
@@ -1405,23 +1890,24 @@ describe('Consent Flow Contracts', () => {
|
|
|
1405
1890
|
transactions.find(transaction => transaction.guardianApproval)?.guardianApproval
|
|
1406
1891
|
).toEqual(approval);
|
|
1407
1892
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1893
|
+
const changedContract = {
|
|
1894
|
+
...normalContract,
|
|
1895
|
+
read: {
|
|
1896
|
+
...normalContract.read,
|
|
1897
|
+
credentials: {
|
|
1898
|
+
...normalContract.read.credentials,
|
|
1899
|
+
categories: {
|
|
1900
|
+
...normalContract.read.credentials.categories,
|
|
1901
|
+
Transcript: { required: true },
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
};
|
|
1906
|
+
await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
1907
|
+
uri: constructUri('contract', relationship.contract.id, 'localhost%3A3000'),
|
|
1908
|
+
changes: { contract: changedContract },
|
|
1909
|
+
reason: 'We need transcripts to personalize your progress report.',
|
|
1419
1910
|
});
|
|
1420
|
-
|
|
1421
|
-
await ConsentFlowContract.update(
|
|
1422
|
-
{ updatedAt: relationship.contract.updatedAt },
|
|
1423
|
-
{ where: { id: relationship.contract.id } }
|
|
1424
|
-
);
|
|
1425
1911
|
await Profile.delete({ detach: true, where: { profileId: 'userc' } });
|
|
1426
1912
|
const unmanaged = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1427
1913
|
did: userBDid,
|
|
@@ -1433,7 +1919,25 @@ describe('Consent Flow Contracts', () => {
|
|
|
1433
1919
|
});
|
|
1434
1920
|
|
|
1435
1921
|
const current = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
|
|
1436
|
-
|
|
1922
|
+
const acceptedTerms: ConsentFlowTermsType = structuredClone(normalAchievementOnlyTerms);
|
|
1923
|
+
acceptedTerms.read.credentials.categories.Transcript = { sharing: true, shared: [] };
|
|
1924
|
+
await expect(
|
|
1925
|
+
userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
1926
|
+
uri: initial.records[0]!.termsUri,
|
|
1927
|
+
terms: acceptedTerms,
|
|
1928
|
+
acceptVersion: current.contract.currentVersion,
|
|
1929
|
+
})
|
|
1930
|
+
).rejects.toMatchObject({
|
|
1931
|
+
code: 'FORBIDDEN',
|
|
1932
|
+
message: 'This consent requires approval from a current guardian',
|
|
1933
|
+
});
|
|
1934
|
+
// Legacy metadata may be absent; the original guardian approval must remain auditable.
|
|
1935
|
+
await neogma.queryRunner.run(
|
|
1936
|
+
`MATCH (terms:ConsentFlowTerms {id:$id})
|
|
1937
|
+
REMOVE terms.\`guardianApproval.guardianProfileId\`, terms.\`guardianApproval.guardianDid\`,
|
|
1938
|
+
terms.\`guardianApproval.approvedAt\`, terms.\`guardianApproval.contractUpdatedAt\``,
|
|
1939
|
+
{ id: relationship.terms.id }
|
|
1940
|
+
);
|
|
1437
1941
|
const cleared = (await getContractTermsByUri(initial.records[0]!.termsUri))!;
|
|
1438
1942
|
expect(cleared.terms.guardianApproval).toBeUndefined();
|
|
1439
1943
|
expect(await hasGuardianApprovalHistory(cleared.terms)).toBe(true);
|
|
@@ -1442,9 +1946,6 @@ describe('Consent Flow Contracts', () => {
|
|
|
1442
1946
|
});
|
|
1443
1947
|
expect(historical.records[0]?.guardian).toEqual({ required: true, approved: false });
|
|
1444
1948
|
const history = await getTransactionsForTerms(relationship.terms.id, { limit: 10 });
|
|
1445
|
-
expect(
|
|
1446
|
-
history.find(transaction => transaction.action === 'update')?.guardianApproval
|
|
1447
|
-
).toBeUndefined();
|
|
1448
1949
|
expect(
|
|
1449
1950
|
history.find(transaction => transaction.guardianApproval)?.guardianApproval
|
|
1450
1951
|
).toEqual(approval);
|
|
@@ -1902,69 +2403,58 @@ describe('Consent Flow Contracts', () => {
|
|
|
1902
2403
|
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
1903
2404
|
});
|
|
1904
2405
|
|
|
1905
|
-
it(
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
endpoint: 'http://localhost:5000/api',
|
|
1914
|
-
name: 'mysa',
|
|
1915
|
-
});
|
|
1916
|
-
|
|
1917
|
-
const signingAuthority = {
|
|
1918
|
-
endpoint: sa!.signingAuthority.endpoint,
|
|
1919
|
-
name: sa!.relationship.name,
|
|
1920
|
-
};
|
|
1921
|
-
|
|
1922
|
-
// Create a boost
|
|
1923
|
-
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
1924
|
-
credential: testUnsignedBoost,
|
|
1925
|
-
category: 'Achievement',
|
|
1926
|
-
name: 'Auto-issued Boost',
|
|
1927
|
-
});
|
|
1928
|
-
|
|
1929
|
-
// Create a contract with the auto-boost
|
|
1930
|
-
const contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
1931
|
-
contract: normalContract,
|
|
1932
|
-
name: 'Auto Boost Contract',
|
|
1933
|
-
autoboosts: [
|
|
1934
|
-
{
|
|
1935
|
-
boostUri,
|
|
1936
|
-
signingAuthority,
|
|
1937
|
-
},
|
|
1938
|
-
],
|
|
1939
|
-
});
|
|
1940
|
-
|
|
1941
|
-
// Consent to the contract
|
|
1942
|
-
const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
1943
|
-
contractUri,
|
|
1944
|
-
terms: normalFullTerms,
|
|
1945
|
-
});
|
|
1946
|
-
|
|
1947
|
-
// Check that a credential was automatically issued
|
|
1948
|
-
const result = await userB.clients.fullAuth.contracts.getCredentialsForContract({
|
|
1949
|
-
termsUri,
|
|
1950
|
-
});
|
|
2406
|
+
it.each([false, true])(
|
|
2407
|
+
'auto-issues boosts for contracts with autoboosts when oneTime=%s',
|
|
2408
|
+
async oneTime => {
|
|
2409
|
+
await userA.clients.fullAuth.profile.registerSigningAuthority({
|
|
2410
|
+
endpoint: 'http://localhost:5000/api',
|
|
2411
|
+
name: 'mysa',
|
|
2412
|
+
did: 'did:key:z6MkitsQTk2GDNYXAFckVcQHtC68S9j9ruVFYWrixM6RG5Mw',
|
|
2413
|
+
});
|
|
1951
2414
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2415
|
+
const sa = await userA.clients.fullAuth.profile.signingAuthority({
|
|
2416
|
+
endpoint: 'http://localhost:5000/api',
|
|
2417
|
+
name: 'mysa',
|
|
2418
|
+
});
|
|
2419
|
+
const signingAuthority = {
|
|
2420
|
+
endpoint: sa!.signingAuthority.endpoint,
|
|
2421
|
+
name: sa!.relationship.name,
|
|
2422
|
+
};
|
|
1955
2423
|
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2424
|
+
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
2425
|
+
credential: testUnsignedBoost,
|
|
2426
|
+
category: 'Achievement',
|
|
2427
|
+
name: 'Auto-issued Boost',
|
|
2428
|
+
});
|
|
2429
|
+
const contractUri =
|
|
2430
|
+
await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
2431
|
+
contract: normalContract,
|
|
2432
|
+
name: 'Auto Boost Contract',
|
|
2433
|
+
autoboosts: [{ boostUri, signingAuthority }],
|
|
2434
|
+
});
|
|
1960
2435
|
|
|
1961
|
-
|
|
1962
|
-
|
|
2436
|
+
const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
2437
|
+
contractUri,
|
|
2438
|
+
terms: normalFullTerms,
|
|
2439
|
+
oneTime,
|
|
2440
|
+
});
|
|
2441
|
+
const result = await userB.clients.fullAuth.contracts.getCredentialsForContract({
|
|
2442
|
+
termsUri,
|
|
2443
|
+
});
|
|
2444
|
+
expect(result.records).toHaveLength(1);
|
|
2445
|
+
expect(result.records[0]?.boostUri).toContain(boostUri.split(':').pop());
|
|
1963
2446
|
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2447
|
+
const transactions =
|
|
2448
|
+
await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
|
|
2449
|
+
uri: termsUri,
|
|
2450
|
+
});
|
|
2451
|
+
expect(transactions.records.length).toBeGreaterThanOrEqual(2);
|
|
2452
|
+
expect(transactions.records.some(tx => tx.action === 'write')).toBeTruthy();
|
|
2453
|
+
expect((await getContractTermsByUri(termsUri))?.terms.status).toBe(
|
|
2454
|
+
oneTime ? 'consumed' : 'live'
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
);
|
|
1968
2458
|
|
|
1969
2459
|
describe('first-consent auto-boost issuance dates', () => {
|
|
1970
2460
|
// A malformed date seen on a production Boost template; DIDKit refuses to sign it.
|
|
@@ -2131,7 +2621,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
2131
2621
|
}
|
|
2132
2622
|
});
|
|
2133
2623
|
|
|
2134
|
-
it('
|
|
2624
|
+
it('does not reissue the same autoboost when terms are updated repeatedly', async () => {
|
|
2135
2625
|
await userA.clients.fullAuth.profile.registerSigningAuthority({
|
|
2136
2626
|
endpoint: 'http://localhost:5000/api',
|
|
2137
2627
|
name: 'mysa',
|
|
@@ -2187,30 +2677,34 @@ describe('Consent Flow Contracts', () => {
|
|
|
2187
2677
|
},
|
|
2188
2678
|
});
|
|
2189
2679
|
|
|
2190
|
-
//
|
|
2680
|
+
// Updating terms cannot cause the same automatic boost to be issued again.
|
|
2191
2681
|
const updatedResult = await userB.clients.fullAuth.contracts.getCredentialsForContract({
|
|
2192
2682
|
termsUri,
|
|
2193
2683
|
});
|
|
2194
|
-
|
|
2195
|
-
// Should have one more credential than before
|
|
2196
|
-
expect(updatedResult.records.length).toBeGreaterThan(initialCount);
|
|
2197
|
-
|
|
2198
|
-
// The latest credential should be from the same boost
|
|
2684
|
+
expect(updatedResult.records).toHaveLength(initialCount);
|
|
2199
2685
|
expect(updatedResult.records[0]?.boostUri).toContain(boostUri.split(':').pop());
|
|
2200
2686
|
|
|
2201
|
-
|
|
2202
|
-
const transactions = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
|
|
2687
|
+
await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
2203
2688
|
uri: termsUri,
|
|
2689
|
+
terms: {
|
|
2690
|
+
...normalFullTerms,
|
|
2691
|
+
read: { ...normalFullTerms.read, personal: { name: 'Updated again' } },
|
|
2692
|
+
},
|
|
2204
2693
|
});
|
|
2694
|
+
const repeatedResult = await userB.clients.fullAuth.contracts.getCredentialsForContract(
|
|
2695
|
+
{
|
|
2696
|
+
termsUri,
|
|
2697
|
+
}
|
|
2698
|
+
);
|
|
2699
|
+
expect(repeatedResult.records).toHaveLength(initialCount);
|
|
2205
2700
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
// Should have at least two auto-boost transactions
|
|
2701
|
+
const transactions = await userB.clients.fullAuth.contracts.getTermsTransactionHistory({
|
|
2702
|
+
uri: termsUri,
|
|
2703
|
+
});
|
|
2210
2704
|
const autoBoostTxCount = transactions.records.filter(
|
|
2211
2705
|
tx => tx.action === 'write'
|
|
2212
2706
|
).length;
|
|
2213
|
-
expect(autoBoostTxCount).
|
|
2707
|
+
expect(autoBoostTxCount).toBe(1);
|
|
2214
2708
|
});
|
|
2215
2709
|
|
|
2216
2710
|
it('should support multiple auto-boosts per contract', async () => {
|
|
@@ -2394,7 +2888,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
2394
2888
|
expect(terms.deniedWriters).toEqual(['userc']);
|
|
2395
2889
|
});
|
|
2396
2890
|
|
|
2397
|
-
it('
|
|
2891
|
+
it('keeps owner autoboosts active when consent denies a writer', async () => {
|
|
2398
2892
|
await userA.clients.fullAuth.profile.registerSigningAuthority({
|
|
2399
2893
|
endpoint: 'http://localhost:5000/api',
|
|
2400
2894
|
name: 'mysa',
|
|
@@ -2448,9 +2942,10 @@ describe('Consent Flow Contracts', () => {
|
|
|
2448
2942
|
autoboosts: [{ boostUri, signingAuthority }],
|
|
2449
2943
|
});
|
|
2450
2944
|
|
|
2451
|
-
await
|
|
2452
|
-
|
|
2453
|
-
|
|
2945
|
+
await seedLegacyAutoBoost(contractUri, {
|
|
2946
|
+
boostUri: boostUri2,
|
|
2947
|
+
signingAuthority: signingAuthority2,
|
|
2948
|
+
issuerProfileId: 'userc',
|
|
2454
2949
|
});
|
|
2455
2950
|
|
|
2456
2951
|
// Consent to the contract
|
|
@@ -2481,7 +2976,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
2481
2976
|
expect(hasAutoBoostTx).toBeTruthy();
|
|
2482
2977
|
});
|
|
2483
2978
|
|
|
2484
|
-
it('
|
|
2979
|
+
it('retains legacy delegated deliveries across withdrawal and reconsent', async () => {
|
|
2485
2980
|
// Setup: userA (owner) and userC (writer) can issue boosts
|
|
2486
2981
|
// User A's SA
|
|
2487
2982
|
await userA.clients.fullAuth.profile.registerSigningAuthority({
|
|
@@ -2532,43 +3027,25 @@ describe('Consent Flow Contracts', () => {
|
|
|
2532
3027
|
},
|
|
2533
3028
|
],
|
|
2534
3029
|
});
|
|
2535
|
-
await
|
|
3030
|
+
await seedLegacyAutoBoost(contractUri, {
|
|
3031
|
+
boostUri: boostC_Uri,
|
|
3032
|
+
signingAuthority: {
|
|
3033
|
+
endpoint: saC_Details.signingAuthority.endpoint,
|
|
3034
|
+
name: saC_Details.relationship.name,
|
|
3035
|
+
},
|
|
3036
|
+
issuerProfileId: 'userc',
|
|
3037
|
+
});
|
|
3038
|
+
const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
2536
3039
|
contractUri,
|
|
2537
|
-
|
|
2538
|
-
{
|
|
2539
|
-
boostUri: boostC_Uri,
|
|
2540
|
-
signingAuthority: {
|
|
2541
|
-
endpoint: saC_Details.signingAuthority.endpoint,
|
|
2542
|
-
name: saC_Details.relationship.name,
|
|
2543
|
-
},
|
|
2544
|
-
},
|
|
2545
|
-
],
|
|
3040
|
+
terms: normalFullTerms,
|
|
2546
3041
|
});
|
|
2547
|
-
|
|
2548
|
-
// 1. Initial consent by userB (no denied writers)
|
|
2549
|
-
const { termsUri: initialTermsUri } =
|
|
2550
|
-
await userB.clients.fullAuth.contracts.consentToContract({
|
|
2551
|
-
contractUri,
|
|
2552
|
-
terms: normalFullTerms, // No deniedWriters
|
|
2553
|
-
});
|
|
2554
3042
|
const credentialsAfterInitialConsent =
|
|
2555
|
-
await userB.clients.fullAuth.contracts.getCredentialsForContract({
|
|
2556
|
-
termsUri: initialTermsUri,
|
|
2557
|
-
});
|
|
2558
|
-
// Should have boosts from both A and C
|
|
2559
|
-
expect(credentialsAfterInitialConsent.records).toHaveLength(2);
|
|
2560
|
-
expect(
|
|
2561
|
-
credentialsAfterInitialConsent.records.some(cred =>
|
|
2562
|
-
cred.boostUri?.includes(boostA_Uri.split(':').pop()!)
|
|
2563
|
-
)
|
|
2564
|
-
).toBeTruthy();
|
|
3043
|
+
await userB.clients.fullAuth.contracts.getCredentialsForContract({ termsUri });
|
|
2565
3044
|
expect(
|
|
2566
|
-
credentialsAfterInitialConsent.records.
|
|
2567
|
-
|
|
2568
|
-
)
|
|
2569
|
-
).toBeTruthy();
|
|
3045
|
+
credentialsAfterInitialConsent.records.map(credential => credential.boostUri).sort()
|
|
3046
|
+
).toEqual([boostA_Uri, boostC_Uri].sort());
|
|
2570
3047
|
|
|
2571
|
-
await userB.clients.fullAuth.contracts.withdrawConsent({ uri:
|
|
3048
|
+
await userB.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
|
|
2572
3049
|
|
|
2573
3050
|
// 2. Re-consent by userB, denying userC
|
|
2574
3051
|
const { termsUri: reconsentTermsUri } =
|
|
@@ -2583,18 +3060,10 @@ describe('Consent Flow Contracts', () => {
|
|
|
2583
3060
|
termsUri: reconsentTermsUri,
|
|
2584
3061
|
});
|
|
2585
3062
|
|
|
2586
|
-
//
|
|
2587
|
-
expect(credentialsAfterReconsent.records).toHaveLength(3);
|
|
3063
|
+
// Withdrawing or denying future writes does not erase already delivered credentials.
|
|
2588
3064
|
expect(
|
|
2589
|
-
credentialsAfterReconsent.records.
|
|
2590
|
-
|
|
2591
|
-
).length
|
|
2592
|
-
).toBe(2);
|
|
2593
|
-
expect(
|
|
2594
|
-
credentialsAfterReconsent.records.filter(cred =>
|
|
2595
|
-
cred.boostUri?.includes(boostC_Uri.split(':').pop()!)
|
|
2596
|
-
).length
|
|
2597
|
-
).toBe(1);
|
|
3065
|
+
credentialsAfterReconsent.records.map(credential => credential.boostUri).sort()
|
|
3066
|
+
).toEqual([boostA_Uri, boostC_Uri].sort());
|
|
2598
3067
|
});
|
|
2599
3068
|
|
|
2600
3069
|
it('should handle updating terms for a contract, filtering auto-issued boosts based on new deniedWriters', async () => {
|
|
@@ -2648,17 +3117,13 @@ describe('Consent Flow Contracts', () => {
|
|
|
2648
3117
|
},
|
|
2649
3118
|
],
|
|
2650
3119
|
});
|
|
2651
|
-
await
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
name: saC_Details.relationship.name,
|
|
2659
|
-
},
|
|
2660
|
-
},
|
|
2661
|
-
],
|
|
3120
|
+
await seedLegacyAutoBoost(contractUri, {
|
|
3121
|
+
boostUri: boostC_Uri,
|
|
3122
|
+
signingAuthority: {
|
|
3123
|
+
endpoint: saC_Details.signingAuthority.endpoint,
|
|
3124
|
+
name: saC_Details.relationship.name,
|
|
3125
|
+
},
|
|
3126
|
+
issuerProfileId: 'userc',
|
|
2662
3127
|
});
|
|
2663
3128
|
|
|
2664
3129
|
// 1. Initial consent by userB (no denied writers)
|
|
@@ -2668,7 +3133,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
2668
3133
|
});
|
|
2669
3134
|
const credentialsAfterInitialConsent =
|
|
2670
3135
|
await userB.clients.fullAuth.contracts.getCredentialsForContract({ termsUri });
|
|
2671
|
-
//
|
|
3136
|
+
// Both owner and designated-writer configurations are active for new consent.
|
|
2672
3137
|
expect(credentialsAfterInitialConsent.records).toHaveLength(2);
|
|
2673
3138
|
|
|
2674
3139
|
// 2. Update terms by userB, denying userC
|
|
@@ -2684,18 +3149,8 @@ describe('Consent Flow Contracts', () => {
|
|
|
2684
3149
|
termsUri,
|
|
2685
3150
|
});
|
|
2686
3151
|
|
|
2687
|
-
//
|
|
2688
|
-
expect(credentialsAfterUpdate.records).toHaveLength(
|
|
2689
|
-
expect(
|
|
2690
|
-
credentialsAfterUpdate.records.filter(cred =>
|
|
2691
|
-
cred.boostUri?.includes(boostA_Uri.split(':').pop()!)
|
|
2692
|
-
).length
|
|
2693
|
-
).toBe(2);
|
|
2694
|
-
expect(
|
|
2695
|
-
credentialsAfterUpdate.records.filter(cred =>
|
|
2696
|
-
cred.boostUri?.includes(boostC_Uri.split(':').pop()!)
|
|
2697
|
-
).length
|
|
2698
|
-
).toBe(1);
|
|
3152
|
+
// Updating terms must not re-issue already delivered automatic credentials.
|
|
3153
|
+
expect(credentialsAfterUpdate.records).toHaveLength(2);
|
|
2699
3154
|
});
|
|
2700
3155
|
});
|
|
2701
3156
|
|
|
@@ -3484,7 +3939,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3484
3939
|
|
|
3485
3940
|
beforeEach(async () => {
|
|
3486
3941
|
vi.useFakeTimers();
|
|
3487
|
-
vi.setSystemTime(new Date(
|
|
3942
|
+
vi.setSystemTime(new Date(2999, 3, 21));
|
|
3488
3943
|
|
|
3489
3944
|
await Profile.delete({ detach: true, where: {} });
|
|
3490
3945
|
await ConsentFlowContract.delete({ detach: true, where: {} });
|
|
@@ -3496,7 +3951,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3496
3951
|
contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
3497
3952
|
contract: minimalContract,
|
|
3498
3953
|
name: 'a',
|
|
3499
|
-
expiresAt: '
|
|
3954
|
+
expiresAt: '3000-01-01T00:00:00.000Z', // New consent uses Neo4j's real clock.
|
|
3500
3955
|
});
|
|
3501
3956
|
const { termsUri: _termsUri } =
|
|
3502
3957
|
await userB.clients.fullAuth.contracts.consentToContract({
|
|
@@ -3505,10 +3960,18 @@ describe('Consent Flow Contracts', () => {
|
|
|
3505
3960
|
});
|
|
3506
3961
|
termsUri = _termsUri;
|
|
3507
3962
|
// Verification's fake JS clock can still inspect an existing historical consent.
|
|
3508
|
-
await
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3963
|
+
const expiryVersion = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
3964
|
+
uri: contractUri,
|
|
3965
|
+
changes: { expiresAt: new Date(Date.UTC(2999, 3, 25)).toISOString() },
|
|
3966
|
+
reason: 'The contract expiry was updated.',
|
|
3967
|
+
});
|
|
3968
|
+
if (expiryVersion.kind === 'requires_consent') {
|
|
3969
|
+
await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
3970
|
+
uri: termsUri,
|
|
3971
|
+
terms: minimalTerms,
|
|
3972
|
+
acceptVersion: expiryVersion.version,
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3512
3975
|
});
|
|
3513
3976
|
|
|
3514
3977
|
afterAll(async () => {
|
|
@@ -3522,22 +3985,22 @@ describe('Consent Flow Contracts', () => {
|
|
|
3522
3985
|
await ConsentFlowTransaction.delete({ detach: true, where: {} });
|
|
3523
3986
|
});
|
|
3524
3987
|
|
|
3525
|
-
it('
|
|
3988
|
+
it('returns consent state for public and authenticated callers', async () => {
|
|
3526
3989
|
await expect(
|
|
3527
3990
|
noAuthClient.contracts.verifyConsent({ uri: contractUri, profileId: 'userb' })
|
|
3528
|
-
).resolves.
|
|
3991
|
+
).resolves.toBe(true);
|
|
3529
3992
|
await expect(
|
|
3530
3993
|
userA.clients.partialAuth.contracts.verifyConsent({
|
|
3531
3994
|
uri: contractUri,
|
|
3532
3995
|
profileId: 'userb',
|
|
3533
3996
|
})
|
|
3534
|
-
).resolves.
|
|
3997
|
+
).resolves.toBe(true);
|
|
3535
3998
|
await expect(
|
|
3536
3999
|
userA.clients.fullAuth.contracts.verifyConsent({
|
|
3537
4000
|
uri: contractUri,
|
|
3538
4001
|
profileId: 'userb',
|
|
3539
4002
|
})
|
|
3540
|
-
).resolves.
|
|
4003
|
+
).resolves.toBe(true);
|
|
3541
4004
|
});
|
|
3542
4005
|
|
|
3543
4006
|
it('should actually verify consent', async () => {
|
|
@@ -3578,18 +4041,27 @@ describe('Consent Flow Contracts', () => {
|
|
|
3578
4041
|
{
|
|
3579
4042
|
contract: minimalContract,
|
|
3580
4043
|
name: 'b',
|
|
3581
|
-
expiresAt: '
|
|
4044
|
+
expiresAt: '3000-01-01T00:00:00.000Z',
|
|
3582
4045
|
}
|
|
3583
4046
|
);
|
|
3584
4047
|
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
4048
|
+
const { termsUri: newTermsUri } =
|
|
4049
|
+
await userB.clients.fullAuth.contracts.consentToContract({
|
|
4050
|
+
contractUri: newContractUri,
|
|
4051
|
+
terms: minimalTerms,
|
|
4052
|
+
});
|
|
4053
|
+
const expiryVersion = await userA.clients.fullAuth.contracts.updateConsentFlowContract({
|
|
4054
|
+
uri: newContractUri,
|
|
4055
|
+
changes: { expiresAt: new Date(2999, 4, 19).toISOString() },
|
|
4056
|
+
reason: 'The contract expiry was updated.',
|
|
3588
4057
|
});
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4058
|
+
if (expiryVersion.kind === 'requires_consent') {
|
|
4059
|
+
await userB.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
4060
|
+
uri: newTermsUri,
|
|
4061
|
+
terms: minimalTerms,
|
|
4062
|
+
acceptVersion: expiryVersion.version,
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
3593
4065
|
|
|
3594
4066
|
expect(
|
|
3595
4067
|
await userA.clients.fullAuth.contracts.verifyConsent({
|
|
@@ -3598,7 +4070,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3598
4070
|
})
|
|
3599
4071
|
).toBeTruthy();
|
|
3600
4072
|
|
|
3601
|
-
vi.setSystemTime(new Date(
|
|
4073
|
+
vi.setSystemTime(new Date(2999, 5, 21));
|
|
3602
4074
|
|
|
3603
4075
|
expect(
|
|
3604
4076
|
await userA.clients.fullAuth.contracts.verifyConsent({
|
|
@@ -3619,7 +4091,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3619
4091
|
await userB.clients.fullAuth.contracts.consentToContract({
|
|
3620
4092
|
contractUri: newContractUri,
|
|
3621
4093
|
terms: minimalTerms,
|
|
3622
|
-
expiresAt: new Date(
|
|
4094
|
+
expiresAt: new Date(2999, 4, 19).toISOString(),
|
|
3623
4095
|
});
|
|
3624
4096
|
|
|
3625
4097
|
expect(
|
|
@@ -3629,7 +4101,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3629
4101
|
})
|
|
3630
4102
|
).toBeTruthy();
|
|
3631
4103
|
|
|
3632
|
-
vi.setSystemTime(new Date(
|
|
4104
|
+
vi.setSystemTime(new Date(2999, 5, 21));
|
|
3633
4105
|
|
|
3634
4106
|
expect(
|
|
3635
4107
|
await userA.clients.fullAuth.contracts.verifyConsent({
|
|
@@ -4641,10 +5113,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
4641
5113
|
Achievement: ['credential:123'],
|
|
4642
5114
|
},
|
|
4643
5115
|
})
|
|
4644
|
-
).rejects.toMatchObject({
|
|
4645
|
-
code: 'FORBIDDEN',
|
|
4646
|
-
message: expect.stringContaining('withdrawn'),
|
|
4647
|
-
});
|
|
5116
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
4648
5117
|
});
|
|
4649
5118
|
|
|
4650
5119
|
it('should deduplicate credentials when syncing', async () => {
|
|
@@ -5208,7 +5677,10 @@ describe('Consent Flow Contracts', () => {
|
|
|
5208
5677
|
it('should remove all specified autoboosts even if some in the list do not exist on the contract', async () => {
|
|
5209
5678
|
await userA.clients.fullAuth.contracts.removeAutoBoostsFromContract({
|
|
5210
5679
|
contractUri,
|
|
5211
|
-
boostUris: [
|
|
5680
|
+
boostUris: [
|
|
5681
|
+
boost1Uri,
|
|
5682
|
+
constructUri('boost', 'not-on-this-contract', 'localhost%3A3000'),
|
|
5683
|
+
],
|
|
5212
5684
|
});
|
|
5213
5685
|
const contract = await userA.clients.fullAuth.contracts.getConsentFlowContract({
|
|
5214
5686
|
uri: contractUri,
|