@learncard/network-brain-service 3.19.1 → 4.0.1
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 +44 -111
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +120 -0
- package/README.md +9 -2
- package/bootstrapLambda.test.ts +72 -0
- package/bundle.mjs +9 -1
- package/config/config.dev.json +29 -0
- package/config/config.example.json +29 -0
- package/config/config.json +1 -0
- package/config/config.production.json +29 -0
- package/config/config.scouts.dev.json +24 -0
- package/config/config.scouts.production.json +24 -0
- package/contractEventsLambda.ts +18 -0
- package/contractEventsLambdaApp.ts +19 -0
- package/didWebLambda.ts +13 -18
- package/didWebLambdaApp.ts +22 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
- package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
- package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
- package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -1
- package/dist/accesslayer/share-link/attachment.d.ts +37 -0
- package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
- package/dist/accesslayer/share-link/helpers.d.ts +3 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
- package/dist/accesslayer/share-link/types.d.ts +8 -3
- package/dist/accesslayer/share-link/types.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +49 -0
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/environment.d.ts +5 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +112 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/helpers/app-manifest.helpers.d.ts +5 -0
- package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
- package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
- package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
- package/dist/helpers/consent-referral.helpers.d.ts +2 -0
- package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
- package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
- package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts +25 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-requests.helpers.d.ts +19 -0
- package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
- package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inline-template.helpers.d.ts +45 -0
- package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts +6 -0
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/profile.helpers.d.ts +1 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/share-link-attachment-access.d.ts +4 -0
- package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment.d.ts +18 -0
- package/dist/helpers/share-link-attachment.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/production.d.ts +3 -5
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/types.d.ts +8 -2
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/smartResume.helpers.d.ts +3 -0
- package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AppManifestVersion.d.ts +20 -0
- package/dist/models/AppManifestVersion.d.ts.map +1 -0
- package/dist/models/AppStoreListing.d.ts +13 -0
- package/dist/models/AppStoreListing.d.ts.map +1 -1
- package/dist/models/ConsentFlowContract.d.ts +13 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +2 -0
- package/dist/models/Integration.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +2 -0
- package/dist/models/ShareLink.d.ts.map +1 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/share-link-constraints.d.ts.map +1 -1
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -0
- package/dist/routes/app-store.d.ts +619 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +2 -2
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +146 -3
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +34 -0
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +52 -2
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +55 -0
- package/dist/routes/public-share-links.d.ts.map +1 -1
- package/dist/routes/share-links.d.ts +175 -14
- package/dist/routes/share-links.d.ts.map +1 -1
- package/dist/routes/test.d.ts +5 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/types/app-manifest.d.ts +339 -0
- package/dist/types/app-manifest.d.ts.map +1 -0
- package/dist/types/app-store-listing.d.ts +34 -0
- package/dist/types/app-store-listing.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +30 -3
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +1 -1
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +49 -195
- package/lambdaApp.ts +203 -0
- package/package.json +20 -19
- package/project.json +8 -1
- package/scripts/export-openapi.ts +20 -0
- package/scripts/offline-schema-environment.ts +18 -0
- package/scripts/verify-python-contracts.ts +72 -0
- package/serverless-local.yml +3 -6
- package/serverless.function-env.cjs +50 -0
- package/serverless.local.yml +0 -1
- package/serverless.yml +29 -74
- package/shareLinkMaintenanceLambda.ts +13 -44
- package/shareLinkMaintenanceLambdaApp.ts +50 -0
- package/src/accesslayer/app-manifest-version/create.ts +57 -0
- package/src/accesslayer/app-manifest-version/read.ts +129 -0
- package/src/accesslayer/app-manifest-version/update.ts +61 -0
- package/src/accesslayer/app-store-listing/read.ts +6 -0
- package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
- package/src/accesslayer/app-store-listing/update.ts +15 -0
- package/src/accesslayer/boost/relationships/create.ts +8 -2
- package/src/accesslayer/consentflowcontract/create.ts +81 -8
- package/src/accesslayer/consentflowcontract/read.ts +84 -10
- package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
- package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
- package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
- package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
- package/src/accesslayer/credential/create.test.ts +2 -115
- package/src/accesslayer/credential/create.ts +11 -6
- package/src/accesslayer/credential/relationships/create.ts +17 -10
- package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
- package/src/accesslayer/inbox-credential/update.ts +4 -2
- package/src/accesslayer/share-link/abandon.ts +20 -0
- package/src/accesslayer/share-link/attachment.ts +360 -0
- package/src/accesslayer/share-link/finalize.ts +55 -7
- package/src/accesslayer/share-link/helpers.ts +33 -2
- package/src/accesslayer/share-link/recovery.ts +20 -0
- package/src/accesslayer/share-link/reserve.ts +85 -6
- package/src/accesslayer/share-link/revoke.ts +35 -0
- package/src/accesslayer/share-link/types.ts +9 -4
- package/src/cache/app-store.caches.ts +35 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/environment.test.ts +17 -12
- package/src/config/environment.ts +1 -0
- package/src/config/serverlessFunctionEnv.test.ts +105 -0
- package/src/config/stageConfig.test.ts +25 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +24 -0
- package/src/helpers/app-manifest.helpers.ts +185 -0
- package/src/helpers/claim-hooks.helpers.ts +20 -7
- package/src/helpers/consent-referral.helpers.ts +6 -0
- package/src/helpers/consent-scopes.helpers.ts +58 -0
- package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
- package/src/helpers/contract-events.helpers.ts +540 -0
- package/src/helpers/contract-requests.helpers.ts +211 -0
- package/src/helpers/finalize-inbox.helpers.ts +163 -44
- package/src/helpers/inbox.helpers.ts +51 -66
- package/src/helpers/inline-template.helpers.ts +560 -0
- package/src/helpers/notificationMessages.ts +59 -0
- package/src/helpers/notificationQueue.helpers.ts +23 -5
- package/src/helpers/notifications.helpers.ts +25 -1
- package/src/helpers/profile.helpers.ts +6 -0
- package/src/helpers/share-link-attachment-access.ts +87 -0
- package/src/helpers/share-link-attachment.ts +115 -0
- package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
- package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
- package/src/helpers/share-link-maintenance/runner.ts +28 -0
- package/src/helpers/share-link-maintenance/runtime.ts +6 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
- package/src/helpers/share-link-maintenance/types.ts +2 -0
- package/src/helpers/share-link-owner-projection.ts +2 -0
- package/src/helpers/share-link-policy/production.ts +37 -17
- package/src/helpers/share-link-policy/resolver.ts +16 -11
- package/src/helpers/share-link-policy/types.ts +15 -7
- package/src/helpers/signingAuthority.helpers.ts +23 -14
- package/src/helpers/smartResume.helpers.ts +123 -0
- package/src/models/AppManifestVersion.ts +43 -0
- package/src/models/AppStoreListing.ts +32 -0
- package/src/models/ConsentFlowContract.ts +34 -3
- package/src/models/ConsentFlowTerms.ts +10 -0
- package/src/models/ConsentFlowTransaction.ts +3 -0
- package/src/models/Integration.ts +2 -0
- package/src/models/ShareLink.ts +4 -0
- package/src/models/index.ts +23 -0
- package/src/models/share-link-constraints.ts +5 -0
- package/src/openapi.ts +44 -8
- package/src/routes/app-store.ts +1054 -27
- package/src/routes/boosts.ts +12 -14
- package/src/routes/contracts.ts +382 -262
- package/src/routes/inbox.ts +4 -2
- package/src/routes/index.ts +4 -1
- package/src/routes/profile-manager.ts +19 -0
- package/src/routes/profiles.ts +12 -0
- package/src/routes/public-share-links.ts +169 -0
- package/src/routes/share-links.ts +69 -0
- package/src/routes/workflows.ts +3 -2
- package/src/types/app-manifest.ts +28 -0
- package/src/types/app-store-listing.ts +13 -0
- package/src/types/consentflowcontract.ts +25 -4
- package/test/app-manifest.helpers.spec.ts +170 -0
- package/test/app-manifest.routes.spec.ts +299 -0
- package/test/app-store-listing-updates.spec.ts +236 -0
- package/test/app-store-listing.spec.ts +65 -3
- package/test/consent-data-boundaries.spec.ts +352 -0
- package/test/consent-recipients.spec.ts +513 -0
- package/test/consent-scopes.helpers.spec.ts +67 -0
- package/test/consentflow.spec.ts +63 -2
- package/test/contract-requests.spec.ts +1588 -0
- package/test/credential-storage.neo4j.spec.ts +122 -0
- package/test/guardian-gated-routes.spec.ts +36 -0
- package/test/inbox-security.e2e.ts +26 -10
- package/test/inbox.spec.ts +184 -8
- package/test/inline-template.helpers.spec.ts +49 -0
- package/test/managed-recipient-inbox.spec.ts +24 -2
- package/test/openapi.spec.ts +14 -73
- package/test/profiles.spec.ts +30 -0
- package/test/share-link-attachment.neo4j.spec.ts +262 -0
- package/test/share-link-attachment.unit.spec.ts +244 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
- package/test/share-link-maintenance.unit.spec.ts +32 -5
- package/test/share-link-owner-route.unit.spec.ts +70 -2
- package/test/share-link-policy.unit.spec.ts +18 -4
- package/test/share-link-production-policy.unit.spec.ts +58 -9
- package/test/share-link-public-route.unit.spec.ts +279 -0
- package/test/share-link-service-policy.neo4j.spec.ts +403 -0
- package/test/signing-authority-encryption.spec.ts +75 -0
- package/test/smart-resume-consent.spec.ts +233 -0
- package/test/smart-resume-upload.spec.ts +65 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +13 -0
- package/vitest.inbox-e2e.config.ts +2 -0
- package/workerLambda.test.ts +100 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { getUser } from './helpers/getClient';
|
|
4
|
+
import { normalContract, normalFullTerms } from './helpers/contract';
|
|
5
|
+
import { Profile, ConsentFlowContract } from '@models';
|
|
6
|
+
import { createConsentFlowContract } from '@accesslayer/consentflowcontract/create';
|
|
7
|
+
import { getIdFromUri } from '@helpers/uri.helpers';
|
|
8
|
+
import {
|
|
9
|
+
getContractTermsByUri,
|
|
10
|
+
getConsentedDataForContract,
|
|
11
|
+
} from '@accesslayer/consentflowcontract/relationships/read';
|
|
12
|
+
import { syncCredentialsToContract } from '@accesslayer/consentflowcontract/relationships/update';
|
|
13
|
+
import { testUnsignedBoost, testVc } from './helpers/send';
|
|
14
|
+
import { openApiDocument } from '../src/openapi';
|
|
15
|
+
import { neogma } from '@instance';
|
|
16
|
+
|
|
17
|
+
const actor = async (role: string) => {
|
|
18
|
+
const user = await getUser(randomBytes(32).toString('hex'));
|
|
19
|
+
const profileId = `audience-${role}-${randomBytes(5).toString('hex')}`;
|
|
20
|
+
await user.clients.fullAuth.profile.createProfile({ profileId });
|
|
21
|
+
return { ...user, profileId, did: (await user.clients.fullAuth.profile.getProfile())!.did };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('contract data recipients', () => {
|
|
25
|
+
let owner: Awaited<ReturnType<typeof actor>>;
|
|
26
|
+
let recipient: Awaited<ReturnType<typeof actor>>;
|
|
27
|
+
let learner: Awaited<ReturnType<typeof actor>>;
|
|
28
|
+
let outsider: Awaited<ReturnType<typeof actor>>;
|
|
29
|
+
beforeEach(async () => {
|
|
30
|
+
[owner, recipient, learner, outsider] = await Promise.all([
|
|
31
|
+
actor('owner'),
|
|
32
|
+
actor('recipient'),
|
|
33
|
+
actor('learner'),
|
|
34
|
+
actor('outsider'),
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
const create = (recipients: string[] = []) =>
|
|
38
|
+
owner.clients.fullAuth.contracts.createConsentFlowContract({
|
|
39
|
+
name: 'Synthetic audience contract',
|
|
40
|
+
contract: normalContract,
|
|
41
|
+
recipients,
|
|
42
|
+
});
|
|
43
|
+
const accept = async (contractUri: string, audienceVersion?: number) =>
|
|
44
|
+
learner.clients.fullAuth.contracts.consentToContract({
|
|
45
|
+
contractUri,
|
|
46
|
+
terms: normalFullTerms,
|
|
47
|
+
audienceVersion,
|
|
48
|
+
});
|
|
49
|
+
const details = (uri: string) =>
|
|
50
|
+
owner.clients.fullAuth.contracts.getConsentFlowContract({ uri });
|
|
51
|
+
|
|
52
|
+
it('deduplicates recipients, excludes the owner, resolves DIDs and strips private profile fields', async () => {
|
|
53
|
+
await Profile.update(
|
|
54
|
+
{ email: 'synthetic-private@example.com' },
|
|
55
|
+
{ where: { profileId: recipient.profileId } }
|
|
56
|
+
);
|
|
57
|
+
const uri = await create([recipient.profileId, recipient.did, owner.profileId]);
|
|
58
|
+
const result = await details(uri);
|
|
59
|
+
expect(result.recipients?.map(profile => profile.profileId)).toEqual([recipient.profileId]);
|
|
60
|
+
expect(result.audienceVersion).toBe(1);
|
|
61
|
+
expect(result.recipients![0]).not.toHaveProperty('email');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('validates writers and autoboosts before creating any contract or audience', async () => {
|
|
65
|
+
const name = `atomic-validation-${owner.profileId}`;
|
|
66
|
+
for (const configuration of [
|
|
67
|
+
{ writers: ['missing-writer'] },
|
|
68
|
+
{
|
|
69
|
+
autoboosts: [
|
|
70
|
+
{
|
|
71
|
+
boostUri: 'lc:boost:localhost:missing-boost',
|
|
72
|
+
signingAuthority: { endpoint: 'https://example.com', name: 'missing' },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
]) {
|
|
77
|
+
await expect(
|
|
78
|
+
owner.clients.fullAuth.contracts.createConsentFlowContract({
|
|
79
|
+
name,
|
|
80
|
+
contract: normalContract,
|
|
81
|
+
recipients: [recipient.profileId],
|
|
82
|
+
...configuration,
|
|
83
|
+
})
|
|
84
|
+
).rejects.toThrow();
|
|
85
|
+
expect(await ConsentFlowContract.findMany({ where: { name } })).toHaveLength(0);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('creates no partial contract if a referenced recipient disappears before the atomic write', async () => {
|
|
90
|
+
const name = `atomic-missing-${owner.profileId}`;
|
|
91
|
+
await expect(
|
|
92
|
+
createConsentFlowContract({
|
|
93
|
+
name,
|
|
94
|
+
contract: normalContract,
|
|
95
|
+
ownerProfileId: owner.profileId,
|
|
96
|
+
recipientIds: [recipient.profileId, 'missing-recipient'],
|
|
97
|
+
})
|
|
98
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
99
|
+
expect(await ConsentFlowContract.findMany({ where: { name } })).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('rolls back the contract and all relationships if a later configuration write fails', async () => {
|
|
103
|
+
const boostUri = await owner.clients.fullAuth.boost.createBoost({
|
|
104
|
+
credential: testUnsignedBoost,
|
|
105
|
+
});
|
|
106
|
+
const name = `atomic-rollback-${owner.profileId}`;
|
|
107
|
+
await expect(
|
|
108
|
+
createConsentFlowContract({
|
|
109
|
+
name,
|
|
110
|
+
contract: normalContract,
|
|
111
|
+
ownerProfileId: owner.profileId,
|
|
112
|
+
recipientIds: [recipient.profileId],
|
|
113
|
+
writerIds: [outsider.profileId],
|
|
114
|
+
autoboosts: [
|
|
115
|
+
{
|
|
116
|
+
id: getIdFromUri(boostUri),
|
|
117
|
+
signingAuthorityName: 'synthetic',
|
|
118
|
+
// Neo4j rejects map-valued properties after the preceding writes.
|
|
119
|
+
signingAuthorityEndpoint: { invalid: true } as unknown as string,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
})
|
|
123
|
+
).rejects.toThrow();
|
|
124
|
+
expect(await ConsentFlowContract.findMany({ where: { name } })).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('keeps audience versions monotonic when a recipient is added and removed before consent', async () => {
|
|
128
|
+
const uri = await create();
|
|
129
|
+
await owner.clients.fullAuth.contracts.addContractRecipient({
|
|
130
|
+
contractUri: uri,
|
|
131
|
+
recipient: recipient.profileId,
|
|
132
|
+
});
|
|
133
|
+
await owner.clients.fullAuth.contracts.removeContractRecipient({
|
|
134
|
+
contractUri: uri,
|
|
135
|
+
recipient: recipient.profileId,
|
|
136
|
+
});
|
|
137
|
+
expect((await details(uri)).recipients).toEqual([]);
|
|
138
|
+
expect((await details(uri)).audienceVersion).toBe(2);
|
|
139
|
+
await expect(accept(uri)).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
140
|
+
await accept(uri, 2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('includes public recipients in consent listings, including empty legacy audiences', async () => {
|
|
144
|
+
await Profile.update(
|
|
145
|
+
{ email: 'synthetic-private@example.com' },
|
|
146
|
+
{ where: { profileId: recipient.profileId } }
|
|
147
|
+
);
|
|
148
|
+
const uri = await create([recipient.profileId]);
|
|
149
|
+
await accept(uri, 1);
|
|
150
|
+
const legacyUri = await create();
|
|
151
|
+
await accept(legacyUri);
|
|
152
|
+
const records = (await learner.clients.fullAuth.contracts.getConsentedContracts()).records;
|
|
153
|
+
const listed = records.find(record => record.contract.uri === uri)!.contract;
|
|
154
|
+
expect(listed.recipients?.map(profile => profile.profileId)).toEqual([recipient.profileId]);
|
|
155
|
+
expect(listed.recipients![0]).not.toHaveProperty('email');
|
|
156
|
+
expect(
|
|
157
|
+
records.find(record => record.contract.uri === legacyUri)!.contract.recipients
|
|
158
|
+
).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('exports empty recipients and version zero for legacy contracts without an audience version', async () => {
|
|
162
|
+
const uri = await create();
|
|
163
|
+
await neogma.queryRunner.run(
|
|
164
|
+
'MATCH (contract:ConsentFlowContract {id: $id}) REMOVE contract.audienceVersion',
|
|
165
|
+
{ id: getIdFromUri(uri) }
|
|
166
|
+
);
|
|
167
|
+
await accept(uri);
|
|
168
|
+
const exported = (await learner.clients.fullAuth.credential.getHolderExportMetadata())
|
|
169
|
+
.consentRecords[0]!;
|
|
170
|
+
expect(exported.contract.recipients).toEqual([]);
|
|
171
|
+
expect(exported.contract.audienceVersion).toBe(0);
|
|
172
|
+
expect(exported.contract.owner.did).toBe(owner.did);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('exports the current public audience and version while preserving holder history', async () => {
|
|
176
|
+
await Profile.update(
|
|
177
|
+
{ email: 'synthetic-private@example.com' },
|
|
178
|
+
{ where: { profileId: recipient.profileId } }
|
|
179
|
+
);
|
|
180
|
+
const uri = await create([recipient.profileId]);
|
|
181
|
+
const { termsUri } = await accept(uri, 1);
|
|
182
|
+
const exported = (await learner.clients.fullAuth.credential.getHolderExportMetadata())
|
|
183
|
+
.consentRecords[0]!;
|
|
184
|
+
expect(exported.contract.owner.did).toBe(owner.did);
|
|
185
|
+
expect(exported.contract.audienceVersion).toBe(1);
|
|
186
|
+
expect(exported.contract.recipients).toEqual([
|
|
187
|
+
expect.objectContaining({ profileId: recipient.profileId, did: recipient.did }),
|
|
188
|
+
]);
|
|
189
|
+
expect(exported.contract.recipients![0]).not.toHaveProperty('email');
|
|
190
|
+
await owner.clients.fullAuth.contracts.removeContractRecipient({
|
|
191
|
+
contractUri: uri,
|
|
192
|
+
recipient: recipient.profileId,
|
|
193
|
+
});
|
|
194
|
+
await learner.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
|
|
195
|
+
const later = (await learner.clients.fullAuth.credential.getHolderExportMetadata())
|
|
196
|
+
.consentRecords[0]!;
|
|
197
|
+
expect(later.contract.recipients).toEqual([]);
|
|
198
|
+
expect(later.contract.audienceVersion).toBe(2);
|
|
199
|
+
expect(later.status).toBe('withdrawn');
|
|
200
|
+
expect(later.transactions).toEqual(
|
|
201
|
+
expect.arrayContaining([
|
|
202
|
+
expect.objectContaining({ action: 'consent' }),
|
|
203
|
+
expect.objectContaining({ action: 'withdraw' }),
|
|
204
|
+
])
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('collapses repeated autoboosts after normalizing signing-authority names', async () => {
|
|
209
|
+
const endpoint = 'https://example.com/synthetic-signer';
|
|
210
|
+
await owner.clients.fullAuth.profile.registerSigningAuthority({
|
|
211
|
+
endpoint,
|
|
212
|
+
name: 'synthetic',
|
|
213
|
+
did: owner.did,
|
|
214
|
+
});
|
|
215
|
+
const boostUri = await owner.clients.fullAuth.boost.createBoost({
|
|
216
|
+
credential: testUnsignedBoost,
|
|
217
|
+
});
|
|
218
|
+
const uri = await owner.clients.fullAuth.contracts.createConsentFlowContract({
|
|
219
|
+
name: 'Duplicate autoboost configuration',
|
|
220
|
+
contract: normalContract,
|
|
221
|
+
autoboosts: ['synthetic', 'Synthetic', 'synthetic'].map(name => ({
|
|
222
|
+
boostUri,
|
|
223
|
+
signingAuthority: { endpoint, name },
|
|
224
|
+
})),
|
|
225
|
+
});
|
|
226
|
+
expect((await details(uri)).autoBoosts).toEqual([boostUri]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('rejects conflicting signers for the same autoboost before creating a contract', async () => {
|
|
230
|
+
const endpoint = 'https://example.com/synthetic-signer';
|
|
231
|
+
for (const name of ['first', 'second']) {
|
|
232
|
+
await owner.clients.fullAuth.profile.registerSigningAuthority({
|
|
233
|
+
endpoint,
|
|
234
|
+
name,
|
|
235
|
+
did: owner.did,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const boostUri = await owner.clients.fullAuth.boost.createBoost({
|
|
239
|
+
credential: testUnsignedBoost,
|
|
240
|
+
});
|
|
241
|
+
const name = `autoboost-conflict-${owner.profileId}`;
|
|
242
|
+
await expect(
|
|
243
|
+
owner.clients.fullAuth.contracts.createConsentFlowContract({
|
|
244
|
+
name,
|
|
245
|
+
contract: normalContract,
|
|
246
|
+
autoboosts: ['first', 'second'].map(name => ({
|
|
247
|
+
boostUri,
|
|
248
|
+
signingAuthority: { endpoint, name },
|
|
249
|
+
})),
|
|
250
|
+
})
|
|
251
|
+
).rejects.toMatchObject({
|
|
252
|
+
code: 'BAD_REQUEST',
|
|
253
|
+
message: expect.stringContaining('Conflicting signing authorities'),
|
|
254
|
+
});
|
|
255
|
+
expect(await ConsentFlowContract.findMany({ where: { name } })).toHaveLength(0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('rejects missing and stale acknowledgments without recording consent', async () => {
|
|
259
|
+
const uri = await create([recipient.profileId]);
|
|
260
|
+
await expect(accept(uri)).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
261
|
+
await expect(accept(uri, 0)).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
262
|
+
expect((await learner.clients.fullAuth.contracts.getConsentedContracts()).records).toEqual(
|
|
263
|
+
[]
|
|
264
|
+
);
|
|
265
|
+
await accept(uri, 1);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('allows permitted data reads for recipients but grants no admin or writer rights', async () => {
|
|
269
|
+
const uri = await create([recipient.profileId]);
|
|
270
|
+
await accept(uri, 1);
|
|
271
|
+
const routes = recipient.clients.fullAuth.contracts;
|
|
272
|
+
const query = { id: uri.split(':').at(-1)! };
|
|
273
|
+
expect((await routes.getConsentedDataForContract({ uri })).records).toHaveLength(1);
|
|
274
|
+
expect(
|
|
275
|
+
(await routes.getConsentedDataForDid({ did: learner.did, query })).records
|
|
276
|
+
).toHaveLength(1);
|
|
277
|
+
expect((await routes.getConsentedData()).records).toHaveLength(1);
|
|
278
|
+
await expect(
|
|
279
|
+
routes.addContractRecipient({ contractUri: uri, recipient: outsider.profileId })
|
|
280
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
281
|
+
await expect(
|
|
282
|
+
routes.removeContractRecipient({ contractUri: uri, recipient: recipient.profileId })
|
|
283
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
284
|
+
await expect(routes.deleteConsentFlowContract({ uri })).rejects.toMatchObject({
|
|
285
|
+
code: 'UNAUTHORIZED',
|
|
286
|
+
});
|
|
287
|
+
const boostUri = await recipient.clients.fullAuth.boost.createBoost({
|
|
288
|
+
credential: testUnsignedBoost,
|
|
289
|
+
});
|
|
290
|
+
await expect(
|
|
291
|
+
routes.writeCredentialToContract({
|
|
292
|
+
did: learner.did,
|
|
293
|
+
contractUri: uri,
|
|
294
|
+
boostUri,
|
|
295
|
+
credential: testVc,
|
|
296
|
+
})
|
|
297
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
298
|
+
await expect(
|
|
299
|
+
outsider.clients.fullAuth.contracts.getConsentedDataForContract({ uri })
|
|
300
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
301
|
+
expect(
|
|
302
|
+
(await outsider.clients.fullAuth.contracts.getConsentedDataForDid({ did: learner.did }))
|
|
303
|
+
.records
|
|
304
|
+
).toEqual([]);
|
|
305
|
+
expect((await outsider.clients.fullAuth.contracts.getConsentedData()).records).toEqual([]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('keeps explicitly added writers out of the data audience', async () => {
|
|
309
|
+
const uri = await owner.clients.fullAuth.contracts.createConsentFlowContract({
|
|
310
|
+
name: 'Writer isolation',
|
|
311
|
+
contract: normalContract,
|
|
312
|
+
writers: [outsider.profileId],
|
|
313
|
+
recipients: [recipient.profileId],
|
|
314
|
+
});
|
|
315
|
+
await accept(uri, 1);
|
|
316
|
+
await expect(
|
|
317
|
+
outsider.clients.fullAuth.contracts.getConsentedDataForContract({ uri })
|
|
318
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
319
|
+
expect((await outsider.clients.fullAuth.contracts.getConsentedData()).records).toEqual([]);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('removal stops recipient discovery and rejects old client updates and syncs while owner access remains', async () => {
|
|
323
|
+
const uri = await create([recipient.profileId]);
|
|
324
|
+
const { termsUri } = await accept(uri, 1);
|
|
325
|
+
await owner.clients.fullAuth.contracts.removeContractRecipient({
|
|
326
|
+
contractUri: uri,
|
|
327
|
+
recipient: recipient.profileId,
|
|
328
|
+
});
|
|
329
|
+
expect((await details(uri)).audienceVersion).toBe(2);
|
|
330
|
+
await expect(
|
|
331
|
+
recipient.clients.fullAuth.contracts.getConsentedDataForContract({ uri })
|
|
332
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
333
|
+
expect(
|
|
334
|
+
(
|
|
335
|
+
await recipient.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
336
|
+
did: learner.did,
|
|
337
|
+
})
|
|
338
|
+
).records
|
|
339
|
+
).toEqual([]);
|
|
340
|
+
expect((await recipient.clients.fullAuth.contracts.getConsentedData()).records).toEqual([]);
|
|
341
|
+
expect(
|
|
342
|
+
(await owner.clients.fullAuth.contracts.getConsentedDataForContract({ uri })).records
|
|
343
|
+
).toHaveLength(1);
|
|
344
|
+
for (const audienceVersion of [undefined, 1]) {
|
|
345
|
+
await expect(
|
|
346
|
+
learner.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
347
|
+
uri: termsUri,
|
|
348
|
+
terms: normalFullTerms,
|
|
349
|
+
audienceVersion,
|
|
350
|
+
})
|
|
351
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
352
|
+
await expect(
|
|
353
|
+
learner.clients.fullAuth.contracts.syncCredentialsToContract({
|
|
354
|
+
termsUri,
|
|
355
|
+
categories: { Achievement: ['urn:stale:copy'] },
|
|
356
|
+
audienceVersion,
|
|
357
|
+
})
|
|
358
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
359
|
+
}
|
|
360
|
+
await learner.clients.fullAuth.contracts.syncCredentialsToContract({
|
|
361
|
+
termsUri,
|
|
362
|
+
categories: { Achievement: ['urn:current:copy'] },
|
|
363
|
+
audienceVersion: 2,
|
|
364
|
+
});
|
|
365
|
+
expect(
|
|
366
|
+
JSON.stringify(
|
|
367
|
+
await owner.clients.fullAuth.contracts.getConsentedDataForContract({ uri })
|
|
368
|
+
)
|
|
369
|
+
).not.toContain('urn:stale:copy');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('freezes additions after first consent even following withdrawal and all recipient removals', async () => {
|
|
373
|
+
const uri = await create([recipient.profileId]);
|
|
374
|
+
const { termsUri } = await accept(uri, 1);
|
|
375
|
+
await learner.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
|
|
376
|
+
await owner.clients.fullAuth.contracts.removeContractRecipient({
|
|
377
|
+
contractUri: uri,
|
|
378
|
+
recipient: recipient.profileId,
|
|
379
|
+
});
|
|
380
|
+
await expect(
|
|
381
|
+
owner.clients.fullAuth.contracts.addContractRecipient({
|
|
382
|
+
contractUri: uri,
|
|
383
|
+
recipient: outsider.profileId,
|
|
384
|
+
})
|
|
385
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
386
|
+
expect((await details(uri)).recipients).toEqual([]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('pre-consent addition is idempotent and invalidates the previous version', async () => {
|
|
390
|
+
const uri = await create();
|
|
391
|
+
await owner.clients.fullAuth.contracts.addContractRecipient({
|
|
392
|
+
contractUri: uri,
|
|
393
|
+
recipient: recipient.profileId,
|
|
394
|
+
});
|
|
395
|
+
await owner.clients.fullAuth.contracts.addContractRecipient({
|
|
396
|
+
contractUri: uri,
|
|
397
|
+
recipient: recipient.profileId,
|
|
398
|
+
});
|
|
399
|
+
expect((await details(uri)).audienceVersion).toBe(1);
|
|
400
|
+
await expect(accept(uri, 0)).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
401
|
+
await accept(uri, 1);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it.each([1, 2, 3, 4, 5])(
|
|
405
|
+
'serializes first consent versus recipient addition (attempt %s)',
|
|
406
|
+
async () => {
|
|
407
|
+
const uri = await create();
|
|
408
|
+
const results = await Promise.allSettled([
|
|
409
|
+
accept(uri, 0),
|
|
410
|
+
owner.clients.fullAuth.contracts.addContractRecipient({
|
|
411
|
+
contractUri: uri,
|
|
412
|
+
recipient: recipient.profileId,
|
|
413
|
+
}),
|
|
414
|
+
]);
|
|
415
|
+
expect(results.filter(result => result.status === 'fulfilled')).toHaveLength(1);
|
|
416
|
+
const audience = await details(uri);
|
|
417
|
+
const history = await learner.clients.fullAuth.contracts.getConsentedContracts();
|
|
418
|
+
if (results[0]!.status === 'fulfilled') {
|
|
419
|
+
expect(audience.recipients).toEqual([]);
|
|
420
|
+
expect(history.records).toHaveLength(1);
|
|
421
|
+
} else {
|
|
422
|
+
expect(audience.recipients).toHaveLength(1);
|
|
423
|
+
expect(history.records).toEqual([]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
it('rejects a sync snapshot prepared before a consent edit, without restoring disabled sharing', async () => {
|
|
429
|
+
const uri = await create([recipient.profileId]);
|
|
430
|
+
const { termsUri } = await accept(uri, 1);
|
|
431
|
+
const snapshot = (await getContractTermsByUri(termsUri))!;
|
|
432
|
+
const terms = structuredClone(normalFullTerms);
|
|
433
|
+
terms.read.credentials.categories.Achievement!.sharing = false;
|
|
434
|
+
await learner.clients.fullAuth.contracts.updateConsentedContractTerms({
|
|
435
|
+
uri: termsUri,
|
|
436
|
+
terms,
|
|
437
|
+
audienceVersion: 1,
|
|
438
|
+
});
|
|
439
|
+
await expect(
|
|
440
|
+
syncCredentialsToContract(snapshot, { Achievement: ['urn:stale:snapshot'] }, 1)
|
|
441
|
+
).rejects.toMatchObject({ code: 'CONFLICT' });
|
|
442
|
+
const current = (await getContractTermsByUri(termsUri))!;
|
|
443
|
+
expect(current.terms.terms.read.credentials.categories.Achievement!.sharing).toBe(false);
|
|
444
|
+
expect(
|
|
445
|
+
JSON.stringify(
|
|
446
|
+
await recipient.clients.fullAuth.contracts.getConsentedDataForContract({ uri })
|
|
447
|
+
)
|
|
448
|
+
).not.toContain('urn:stale:snapshot');
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('rechecks membership in the data query after an earlier authorization decision', async () => {
|
|
452
|
+
const uri = await create([recipient.profileId]);
|
|
453
|
+
await accept(uri, 1);
|
|
454
|
+
const id = uri.split(':').at(-1)!;
|
|
455
|
+
expect(
|
|
456
|
+
await getConsentedDataForContract(id, recipient.profileId, { limit: 10 })
|
|
457
|
+
).toHaveLength(1);
|
|
458
|
+
await owner.clients.fullAuth.contracts.removeContractRecipient({
|
|
459
|
+
contractUri: uri,
|
|
460
|
+
recipient: recipient.profileId,
|
|
461
|
+
});
|
|
462
|
+
expect(await getConsentedDataForContract(id, recipient.profileId, { limit: 10 })).toEqual(
|
|
463
|
+
[]
|
|
464
|
+
);
|
|
465
|
+
expect(await getConsentedDataForContract(id, owner.profileId, { limit: 10 })).toHaveLength(
|
|
466
|
+
1
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('includes recipient endpoints and audience acknowledgements in generated OpenAPI', () => {
|
|
471
|
+
const schema = JSON.stringify(openApiDocument);
|
|
472
|
+
expect(openApiDocument.paths?.['/consent-flow-contract/recipient/add']?.post).toBeTruthy();
|
|
473
|
+
expect(
|
|
474
|
+
openApiDocument.paths?.['/consent-flow-contract/recipient/remove']?.post
|
|
475
|
+
).toBeTruthy();
|
|
476
|
+
expect(schema).toContain('audienceVersion');
|
|
477
|
+
expect(schema).toContain('recipients');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('recipient access also stops on withdrawal', async () => {
|
|
481
|
+
const uri = await create([recipient.profileId]);
|
|
482
|
+
const { termsUri } = await accept(uri, 1);
|
|
483
|
+
await learner.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
|
|
484
|
+
expect(
|
|
485
|
+
(await recipient.clients.fullAuth.contracts.getConsentedDataForContract({ uri }))
|
|
486
|
+
.records
|
|
487
|
+
).toEqual([]);
|
|
488
|
+
expect(
|
|
489
|
+
(
|
|
490
|
+
await recipient.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
491
|
+
did: learner.did,
|
|
492
|
+
})
|
|
493
|
+
).records
|
|
494
|
+
).toEqual([]);
|
|
495
|
+
expect((await recipient.clients.fullAuth.contracts.getConsentedData()).records).toEqual([]);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('encrypts real data for owner and recipient; an outsider and removed recipient cannot decrypt new copies', async () => {
|
|
499
|
+
const payload = { id: 'urn:synthetic:audience', value: 'Synthetic outcome' };
|
|
500
|
+
const shared = await learner.learnCard.invoke.createDagJwe(payload, [
|
|
501
|
+
owner.learnCard.id.did(),
|
|
502
|
+
recipient.learnCard.id.did(),
|
|
503
|
+
]);
|
|
504
|
+
expect(await owner.learnCard.invoke.decryptDagJwe(shared)).toEqual(payload);
|
|
505
|
+
expect(await recipient.learnCard.invoke.decryptDagJwe(shared)).toEqual(payload);
|
|
506
|
+
await expect(outsider.learnCard.invoke.decryptDagJwe(shared)).resolves.toBe('');
|
|
507
|
+
const afterRemoval = await learner.learnCard.invoke.createDagJwe(payload, [
|
|
508
|
+
owner.learnCard.id.did(),
|
|
509
|
+
]);
|
|
510
|
+
expect(await owner.learnCard.invoke.decryptDagJwe(afterRemoval)).toEqual(payload);
|
|
511
|
+
await expect(recipient.learnCard.invoke.decryptDagJwe(afterRemoval)).resolves.toBe('');
|
|
512
|
+
});
|
|
513
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildConsentFlowContractFromScopes,
|
|
5
|
+
computeConsentScopeHash,
|
|
6
|
+
normalizeConsentScopes,
|
|
7
|
+
} from '../src/helpers/consent-scopes.helpers';
|
|
8
|
+
|
|
9
|
+
describe('consent-scopes.helpers', () => {
|
|
10
|
+
it('maps normalized consent scopes to immutable contract terms', () => {
|
|
11
|
+
const scopes = normalizeConsentScopes({
|
|
12
|
+
read: {
|
|
13
|
+
credentialCategories: ['Skill', 'Achievement'],
|
|
14
|
+
personalFields: ['email', 'name'],
|
|
15
|
+
},
|
|
16
|
+
write: {
|
|
17
|
+
credentialCategories: ['Achievement'],
|
|
18
|
+
},
|
|
19
|
+
reason: 'ignored for mapping',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
expect(buildConsentFlowContractFromScopes(scopes)).toEqual({
|
|
23
|
+
read: {
|
|
24
|
+
credentials: {
|
|
25
|
+
categories: {
|
|
26
|
+
Achievement: { required: false, defaultEnabled: true },
|
|
27
|
+
Skill: { required: false, defaultEnabled: true },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
personal: {
|
|
31
|
+
email: { required: false, defaultEnabled: true },
|
|
32
|
+
name: { required: false, defaultEnabled: true },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
write: {
|
|
36
|
+
credentials: {
|
|
37
|
+
categories: {
|
|
38
|
+
Achievement: { required: false, defaultEnabled: true },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
personal: {},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('hashes scope sets deterministically and ignores reason/order/duplicates', () => {
|
|
47
|
+
const first = normalizeConsentScopes({
|
|
48
|
+
read: {
|
|
49
|
+
credentialCategories: ['Skill', 'Achievement', 'Skill'],
|
|
50
|
+
personalFields: ['name'],
|
|
51
|
+
},
|
|
52
|
+
write: { credentialCategories: ['Achievement'] },
|
|
53
|
+
reason: 'First reason',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const second = normalizeConsentScopes({
|
|
57
|
+
read: {
|
|
58
|
+
credentialCategories: ['Achievement', 'Skill'],
|
|
59
|
+
personalFields: ['name'],
|
|
60
|
+
},
|
|
61
|
+
write: { credentialCategories: ['Achievement'] },
|
|
62
|
+
reason: 'Different reason',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(computeConsentScopeHash(first)).toBe(computeConsentScopeHash(second));
|
|
66
|
+
});
|
|
67
|
+
});
|
package/test/consentflow.spec.ts
CHANGED
|
@@ -960,6 +960,47 @@ describe('Consent Flow Contracts', () => {
|
|
|
960
960
|
expect(data.records[0]?.terms).toEqual(normalFullTerms);
|
|
961
961
|
});
|
|
962
962
|
|
|
963
|
+
it('does not require guardian approval for managed service profiles, but does for managed children', async () => {
|
|
964
|
+
await Profile.relateTo({
|
|
965
|
+
alias: 'managedBy',
|
|
966
|
+
where: { source: { profileId: 'userb' }, target: { profileId: 'userc' } },
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
const managedChildData = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
970
|
+
did: userBDid,
|
|
971
|
+
});
|
|
972
|
+
expect(managedChildData.records[0]?.guardian.required).toBe(true);
|
|
973
|
+
|
|
974
|
+
await Profile.update({ isServiceProfile: true }, { where: { profileId: 'userb' } });
|
|
975
|
+
const serviceProfileData =
|
|
976
|
+
await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
977
|
+
did: userBDid,
|
|
978
|
+
});
|
|
979
|
+
expect(serviceProfileData.records[0]?.guardian).toEqual({
|
|
980
|
+
required: false,
|
|
981
|
+
approved: false,
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('keeps guardian approval required for legacy service-flagged children', async () => {
|
|
986
|
+
await Profile.relateTo({
|
|
987
|
+
alias: 'managedBy',
|
|
988
|
+
where: { source: { profileId: 'userb' }, target: { profileId: 'userc' } },
|
|
989
|
+
});
|
|
990
|
+
await Profile.update(
|
|
991
|
+
{ type: 'child', isServiceProfile: true },
|
|
992
|
+
{ where: { profileId: 'userb' } }
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
const data = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
996
|
+
did: userBDid,
|
|
997
|
+
});
|
|
998
|
+
expect(data.records[0]?.guardian).toEqual({
|
|
999
|
+
required: true,
|
|
1000
|
+
approved: false,
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
963
1004
|
it('should omit withdrawn consent before returning provider-facing data', async () => {
|
|
964
1005
|
const activeData = await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
965
1006
|
did: userBDid,
|
|
@@ -1162,6 +1203,17 @@ describe('Consent Flow Contracts', () => {
|
|
|
1162
1203
|
approved: true,
|
|
1163
1204
|
approval,
|
|
1164
1205
|
});
|
|
1206
|
+
await Profile.update({ isServiceProfile: true }, { where: { profileId: 'userb' } });
|
|
1207
|
+
const serviceProfileWithApproval =
|
|
1208
|
+
await userA.clients.fullAuth.contracts.getConsentedDataForDid({
|
|
1209
|
+
did: userBDid,
|
|
1210
|
+
});
|
|
1211
|
+
expect(serviceProfileWithApproval.records[0]?.guardian).toEqual({
|
|
1212
|
+
required: true,
|
|
1213
|
+
approved: true,
|
|
1214
|
+
approval,
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1165
1217
|
const current = (await getContractTermsByUri(approved.records[0]!.termsUri))!;
|
|
1166
1218
|
expect(current.terms.guardianApproval).toEqual(approval);
|
|
1167
1219
|
const transactions = await getTransactionsForTerms(current.terms.id, { limit: 10 });
|
|
@@ -3343,7 +3395,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3343
3395
|
contractUri = await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
3344
3396
|
contract: minimalContract,
|
|
3345
3397
|
name: 'a',
|
|
3346
|
-
expiresAt:
|
|
3398
|
+
expiresAt: '2999-01-01T00:00:00.000Z', // New consent uses Neo4j's real clock.
|
|
3347
3399
|
});
|
|
3348
3400
|
const { termsUri: _termsUri } =
|
|
3349
3401
|
await userB.clients.fullAuth.contracts.consentToContract({
|
|
@@ -3351,6 +3403,11 @@ describe('Consent Flow Contracts', () => {
|
|
|
3351
3403
|
terms: minimalTerms,
|
|
3352
3404
|
});
|
|
3353
3405
|
termsUri = _termsUri;
|
|
3406
|
+
// Verification's fake JS clock can still inspect an existing historical consent.
|
|
3407
|
+
await ConsentFlowContract.update(
|
|
3408
|
+
{ expiresAt: new Date(Date.UTC(2024, 3, 25)).toISOString() },
|
|
3409
|
+
{ where: { id: contractUri.split(':').at(-1)! } }
|
|
3410
|
+
);
|
|
3354
3411
|
});
|
|
3355
3412
|
|
|
3356
3413
|
afterAll(async () => {
|
|
@@ -3420,7 +3477,7 @@ describe('Consent Flow Contracts', () => {
|
|
|
3420
3477
|
{
|
|
3421
3478
|
contract: minimalContract,
|
|
3422
3479
|
name: 'b',
|
|
3423
|
-
expiresAt:
|
|
3480
|
+
expiresAt: '2999-01-01T00:00:00.000Z',
|
|
3424
3481
|
}
|
|
3425
3482
|
);
|
|
3426
3483
|
|
|
@@ -3428,6 +3485,10 @@ describe('Consent Flow Contracts', () => {
|
|
|
3428
3485
|
contractUri: newContractUri,
|
|
3429
3486
|
terms: minimalTerms,
|
|
3430
3487
|
});
|
|
3488
|
+
await ConsentFlowContract.update(
|
|
3489
|
+
{ expiresAt: new Date(2024, 4, 19).toISOString() },
|
|
3490
|
+
{ where: { id: newContractUri.split(':').at(-1)! } }
|
|
3491
|
+
);
|
|
3431
3492
|
|
|
3432
3493
|
expect(
|
|
3433
3494
|
await userA.clients.fullAuth.contracts.verifyConsent({
|