@learncard/network-brain-service 3.19.1 → 4.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 +44 -111
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +109 -0
- package/README.md +9 -2
- package/bootstrapLambda.test.ts +72 -0
- 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/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 -18
- 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 +27 -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,122 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import { VCValidator, type BitstringStatusListEntry } from '@learncard/types';
|
|
3
|
+
import { Credential, Profile, StatusList } from '@models';
|
|
4
|
+
import { storeCredential } from '../src/accesslayer/credential/create';
|
|
5
|
+
import { getCredentialById } from '../src/accesslayer/credential/read';
|
|
6
|
+
import { setCredentialBitstringStatus, decodeBitstring } from '../src/helpers/status-list.helpers';
|
|
7
|
+
import { getUser } from './helpers/getClient';
|
|
8
|
+
import { app as statusListsApp } from '../src/status-lists';
|
|
9
|
+
|
|
10
|
+
let issuer: Awaited<ReturnType<typeof getUser>>;
|
|
11
|
+
const credentialIds: string[] = [];
|
|
12
|
+
|
|
13
|
+
const isStatusBitSet = async (entry: BitstringStatusListEntry): Promise<boolean> => {
|
|
14
|
+
const id = entry.statusListCredential.split('/').pop();
|
|
15
|
+
if (!id) throw new Error('Missing status list id');
|
|
16
|
+
const response = await statusListsApp.inject({ method: 'GET', url: `/status-lists/${id}` });
|
|
17
|
+
expect(response.statusCode).toBe(200);
|
|
18
|
+
const list = JSON.parse(response.body);
|
|
19
|
+
const bytes = decodeBitstring(list.credentialSubject.encodedList, 131_072);
|
|
20
|
+
const index = Number(entry.statusListIndex);
|
|
21
|
+
return ((bytes[Math.floor(index / 8)] ?? 0) & (1 << (index % 8))) !== 0;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('credential status storage in Neo4j', () => {
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
issuer = await getUser();
|
|
27
|
+
await issuer.clients.fullAuth.profile.createProfile({ profileId: 'storage-issuer' });
|
|
28
|
+
await statusListsApp.ready();
|
|
29
|
+
});
|
|
30
|
+
afterAll(async () => {
|
|
31
|
+
for (const id of credentialIds) await Credential.delete({ where: { id } });
|
|
32
|
+
await Profile.delete({ detach: true, where: { profileId: 'storage-issuer' } });
|
|
33
|
+
await StatusList.delete({ detach: true, where: { ownerProfileId: 'storage-issuer' } });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('persists trusted public status descriptors for revocation consumers without decrypting the payload', async () => {
|
|
37
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
38
|
+
statusPurposes: ['revocation'],
|
|
39
|
+
});
|
|
40
|
+
if (!entry) throw new Error('Expected allocated revocation entry');
|
|
41
|
+
const signed = await issuer.learnCard.invoke.issueCredential({
|
|
42
|
+
...(await issuer.learnCard.invoke.getTestVc()),
|
|
43
|
+
credentialStatus: entry,
|
|
44
|
+
});
|
|
45
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
46
|
+
const stored = await storeCredential({
|
|
47
|
+
kind: 'issued-credential',
|
|
48
|
+
credential: jwe,
|
|
49
|
+
statusEntries: [entry],
|
|
50
|
+
});
|
|
51
|
+
credentialIds.push(stored.id);
|
|
52
|
+
|
|
53
|
+
const row = await getCredentialById(stored.id);
|
|
54
|
+
expect(row?.credential).toBe(JSON.stringify(jwe));
|
|
55
|
+
expect(JSON.parse(row?.statusEntries ?? 'null')).toEqual([entry]);
|
|
56
|
+
expect(await isStatusBitSet(entry)).toBe(false);
|
|
57
|
+
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(true);
|
|
58
|
+
expect(await isStatusBitSet(entry)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each(['embedded', 'absent'] as const)(
|
|
62
|
+
'preserves a valid signed wire VC and proof with %s status',
|
|
63
|
+
async statusCase => {
|
|
64
|
+
const unsigned = await issuer.learnCard.invoke.getTestVc();
|
|
65
|
+
if (statusCase === 'embedded') {
|
|
66
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
67
|
+
statusPurposes: ['revocation'],
|
|
68
|
+
});
|
|
69
|
+
unsigned.credentialStatus = entry;
|
|
70
|
+
}
|
|
71
|
+
const signed = await issuer.learnCard.invoke.issueCredential(unsigned);
|
|
72
|
+
const stored = await storeCredential(signed);
|
|
73
|
+
credentialIds.push(stored.id);
|
|
74
|
+
const row = await getCredentialById(stored.id);
|
|
75
|
+
const recovered = VCValidator.parse(JSON.parse(row?.credential ?? 'null'));
|
|
76
|
+
expect(recovered).toEqual(signed);
|
|
77
|
+
expect(
|
|
78
|
+
await issuer.learnCard.invoke.verifyCredential(recovered, { checks: ['proof'] })
|
|
79
|
+
).toMatchObject({
|
|
80
|
+
checks: expect.arrayContaining(['proof']),
|
|
81
|
+
warnings: [],
|
|
82
|
+
errors: [],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
it('does not promote wire JWE metadata to trusted status descriptors', async () => {
|
|
88
|
+
const [entry] = await issuer.clients.fullAuth.boost.allocateCredentialStatus({
|
|
89
|
+
statusPurposes: ['revocation'],
|
|
90
|
+
});
|
|
91
|
+
if (!entry) throw new Error('Expected allocated revocation entry');
|
|
92
|
+
const signed = await issuer.learnCard.invoke.issueCredential({
|
|
93
|
+
...(await issuer.learnCard.invoke.getTestVc()),
|
|
94
|
+
credentialStatus: entry,
|
|
95
|
+
});
|
|
96
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
97
|
+
const wire = { ...jwe, kind: 'issued-credential', statusEntries: [entry] };
|
|
98
|
+
const stored = await storeCredential(wire);
|
|
99
|
+
credentialIds.push(stored.id);
|
|
100
|
+
const row = await getCredentialById(stored.id);
|
|
101
|
+
expect(row?.credential).toBe(JSON.stringify(wire));
|
|
102
|
+
expect(row?.statusEntries).toBeUndefined();
|
|
103
|
+
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
|
|
104
|
+
expect(await isStatusBitSet(entry)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('does not persist or apply an empty trusted status descriptor set', async () => {
|
|
108
|
+
const signed = await issuer.learnCard.invoke.issueCredential(
|
|
109
|
+
await issuer.learnCard.invoke.getTestVc()
|
|
110
|
+
);
|
|
111
|
+
const jwe = await issuer.learnCard.invoke.createDagJwe(signed, [issuer.learnCard.id.did()]);
|
|
112
|
+
const stored = await storeCredential({
|
|
113
|
+
kind: 'issued-credential',
|
|
114
|
+
credential: jwe,
|
|
115
|
+
statusEntries: [],
|
|
116
|
+
});
|
|
117
|
+
credentialIds.push(stored.id);
|
|
118
|
+
const row = await getCredentialById(stored.id);
|
|
119
|
+
expect(row?.statusEntries).toBeUndefined();
|
|
120
|
+
expect(await setCredentialBitstringStatus(stored.id, 'revocation', true)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -216,6 +216,21 @@ describe('Guardian-approved consent mutations', () => {
|
|
|
216
216
|
expect(record?.terms.guardianApproval).toBeUndefined();
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
it('preserves the manager-based gate for explicitly typed profiles without managers', async () => {
|
|
220
|
+
await Profile.update(
|
|
221
|
+
{ type: 'child', isServiceProfile: true },
|
|
222
|
+
{ where: { profileId: 'child-user' } }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const { termsUri } = await childCaller().contracts.consentToContract({
|
|
226
|
+
contractUri,
|
|
227
|
+
terms: minimalTerms,
|
|
228
|
+
});
|
|
229
|
+
const record = await getContractTermsByUri(termsUri);
|
|
230
|
+
expect(record?.terms.status).toBe('live');
|
|
231
|
+
expect(record?.terms.guardianApproval).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
219
234
|
it('does not create consent or an audit transaction without guardian approval', async () => {
|
|
220
235
|
await manageChild();
|
|
221
236
|
await expect(
|
|
@@ -238,6 +253,27 @@ describe('Guardian-approved consent mutations', () => {
|
|
|
238
253
|
expect(record?.terms.guardianApproval).toBeUndefined();
|
|
239
254
|
});
|
|
240
255
|
|
|
256
|
+
it('requires guardian approval for legacy service-flagged children', async () => {
|
|
257
|
+
await Profile.update(
|
|
258
|
+
{ type: 'child', isServiceProfile: true },
|
|
259
|
+
{ where: { profileId: 'child-user' } }
|
|
260
|
+
);
|
|
261
|
+
await manageChild();
|
|
262
|
+
|
|
263
|
+
await expect(
|
|
264
|
+
childCaller().contracts.consentToContract({ contractUri, terms: minimalTerms })
|
|
265
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
266
|
+
|
|
267
|
+
const { termsUri } = await childCaller(await approve()).contracts.consentToContract({
|
|
268
|
+
contractUri,
|
|
269
|
+
terms: minimalTerms,
|
|
270
|
+
});
|
|
271
|
+
expect((await getContractTermsByUri(termsUri))?.terms.guardianApproval).toMatchObject({
|
|
272
|
+
guardianProfileId: 'guardian-user',
|
|
273
|
+
guardianDid: guardian.id.did(),
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
241
277
|
it('records the verified guardian and contract version on consent and permission updates', async () => {
|
|
242
278
|
await manageChild();
|
|
243
279
|
const beforeApproval = Date.now();
|
|
@@ -3,7 +3,12 @@ import Fastify, { type FastifyInstance } from 'fastify';
|
|
|
3
3
|
import { fastifyTRPCOpenApiPlugin } from 'trpc-to-openapi';
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import
|
|
6
|
+
import {
|
|
7
|
+
JWEValidator,
|
|
8
|
+
type IssueInboxCredentialType,
|
|
9
|
+
type UnsignedVC,
|
|
10
|
+
type VC,
|
|
11
|
+
} from '@learncard/types';
|
|
7
12
|
import { getBitstringStatusListEntries } from '@learncard/helpers';
|
|
8
13
|
|
|
9
14
|
import { appRouter, createContext } from '../src/app';
|
|
@@ -380,7 +385,8 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
380
385
|
expect(result.deliveries).toEqual([
|
|
381
386
|
{ id: issued.issuanceId, credential: result.verifiableCredentials[0] },
|
|
382
387
|
]);
|
|
383
|
-
|
|
388
|
+
const encrypted = JWEValidator.parse(result.verifiableCredentials[0]);
|
|
389
|
+
expect(await recipient.learnCard.invoke.decryptDagJwe(encrypted)).toMatchObject({
|
|
384
390
|
type: ['VerifiableCredential', 'ClrCredential'],
|
|
385
391
|
proof: expect.anything(),
|
|
386
392
|
credentialSubject: {
|
|
@@ -397,7 +403,7 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
397
403
|
});
|
|
398
404
|
|
|
399
405
|
it.each(['finalize', 'claim link', 'claim without profile'] as const)(
|
|
400
|
-
'recovers exactly one
|
|
406
|
+
'recovers exactly one authorized delivery after discarding the %s response',
|
|
401
407
|
async path => {
|
|
402
408
|
const credential = await signedCredential();
|
|
403
409
|
const issued = await issue({
|
|
@@ -439,11 +445,11 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
439
445
|
expect(
|
|
440
446
|
(await service.invoke.decryptDagJwe(recovery.credential).catch(() => null)) || null
|
|
441
447
|
).toBeNull();
|
|
442
|
-
|
|
448
|
+
const issuerRead =
|
|
443
449
|
(await issuer.learnCard.invoke
|
|
444
450
|
.decryptDagJwe(recovery.credential)
|
|
445
|
-
.catch(() => null)) || null
|
|
446
|
-
).
|
|
451
|
+
.catch(() => null)) || null;
|
|
452
|
+
expect(issuerRead).toEqual(path === 'finalize' ? credential : null);
|
|
447
453
|
expect((await post('/api/inbox/deliveries', {})).status).toBe(401);
|
|
448
454
|
expect(
|
|
449
455
|
(await (await post('/api/inbox/deliveries', {}, issuer)).json()).records
|
|
@@ -510,9 +516,14 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
510
516
|
await verifyRecipientContact('idless@example.test');
|
|
511
517
|
const response = await post('/api/inbox/finalize', {}, recipient);
|
|
512
518
|
expect(response.status).toBe(200);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
]);
|
|
519
|
+
const { deliveries } = await response.json();
|
|
520
|
+
expect(deliveries).toHaveLength(1);
|
|
521
|
+
expect(deliveries[0].id).toBe(issued.issuanceId);
|
|
522
|
+
expect(
|
|
523
|
+
await recipient.learnCard.invoke.decryptDagJwe(
|
|
524
|
+
JWEValidator.parse(deliveries[0].credential)
|
|
525
|
+
)
|
|
526
|
+
).toEqual(credential);
|
|
516
527
|
} else {
|
|
517
528
|
expect(await claim(issued.claimUrl!)).toEqual([credential]);
|
|
518
529
|
}
|
|
@@ -893,7 +904,12 @@ describe('Universal Inbox escrow (HTTP + isolated Neo4j/Redis)', () => {
|
|
|
893
904
|
expect(config).toMatch(
|
|
894
905
|
/inboxMaintenance:\s+handler: lambda\.inboxMaintenanceHandler[\s\S]*?rate: rate\(1 day\)\s+enabled: true/
|
|
895
906
|
);
|
|
896
|
-
|
|
907
|
+
// lambda.ts is a thin bootstrap entry that delegates to the lazily imported lambdaApp.ts.
|
|
908
|
+
const entry = await readFile(new URL('../lambda.ts', import.meta.url), 'utf8');
|
|
909
|
+
expect(entry).toMatch(
|
|
910
|
+
/export const inboxMaintenanceHandler[\s\S]*?\(await getApplication\(\)\)\.inboxMaintenanceHandler\(/
|
|
911
|
+
);
|
|
912
|
+
const handler = await readFile(new URL('../lambdaApp.ts', import.meta.url), 'utf8');
|
|
897
913
|
expect(handler).toMatch(/inboxMaintenanceHandler[\s\S]*?await runInboxMaintenance\(\)/);
|
|
898
914
|
});
|
|
899
915
|
});
|
package/test/inbox.spec.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
VC,
|
|
9
9
|
VP,
|
|
10
10
|
JWE,
|
|
11
|
+
JWEValidator,
|
|
11
12
|
} from '@learncard/types';
|
|
12
13
|
import { sendSpy, addNotificationToQueueSpy } from './helpers/spies';
|
|
13
14
|
import * as notifications from '@helpers/notifications.helpers';
|
|
@@ -16,6 +17,9 @@ import { getProfileByProfileId } from '@accesslayer/profile/read';
|
|
|
16
17
|
import { areProfilesConnected } from '@helpers/connection.helpers';
|
|
17
18
|
import { updateInboxCredential } from '@accesslayer/inbox-credential/update';
|
|
18
19
|
import { neogma } from '@instance';
|
|
20
|
+
import { Credential } from '@models';
|
|
21
|
+
import { getInboxCredentialById } from '@accesslayer/inbox-credential/read';
|
|
22
|
+
import * as credentialRelationships from '@accesslayer/credential/relationships/create';
|
|
19
23
|
|
|
20
24
|
const noAuthClient = getClient();
|
|
21
25
|
let userA: Awaited<ReturnType<typeof getUser>>;
|
|
@@ -1832,6 +1836,174 @@ describe('Universal Inbox', () => {
|
|
|
1832
1836
|
await SigningAuthority.delete({ detach: true, where: {} });
|
|
1833
1837
|
});
|
|
1834
1838
|
|
|
1839
|
+
const preparePendingBoost = async (email: string) => {
|
|
1840
|
+
await Profile.relateTo({
|
|
1841
|
+
alias: 'usesSigningAuthority',
|
|
1842
|
+
where: {
|
|
1843
|
+
source: { profileId: 'usera' },
|
|
1844
|
+
target: { endpoint: 'https://example.com' },
|
|
1845
|
+
},
|
|
1846
|
+
properties: { name: 'example', did: userA.learnCard.id.did() },
|
|
1847
|
+
});
|
|
1848
|
+
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
1849
|
+
credential: testUnsignedBoost,
|
|
1850
|
+
claimPermissions: { canRevoke: true },
|
|
1851
|
+
});
|
|
1852
|
+
const issued = await userA.clients.fullAuth.inbox.issue({
|
|
1853
|
+
templateUri: boostUri,
|
|
1854
|
+
recipient: { type: 'email', value: email },
|
|
1855
|
+
configuration: {
|
|
1856
|
+
signingAuthority: { endpoint: 'https://example.com', name: 'example' },
|
|
1857
|
+
webhookUrl: 'https://example.com/finalized',
|
|
1858
|
+
},
|
|
1859
|
+
});
|
|
1860
|
+
await updateInboxCredential(issued.issuanceId, { isAccepted: true });
|
|
1861
|
+
await userB.clients.fullAuth.contactMethods.addContactMethod({
|
|
1862
|
+
type: 'email',
|
|
1863
|
+
value: email,
|
|
1864
|
+
});
|
|
1865
|
+
await userB.clients.fullAuth.contactMethods.verifyContactMethod({
|
|
1866
|
+
token: sendSpy.mock.calls.at(-1)?.[0].templateModel.verificationToken,
|
|
1867
|
+
});
|
|
1868
|
+
addNotificationToQueueSpy.mockClear();
|
|
1869
|
+
return { boostUri, inboxId: issued.issuanceId };
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
it('rolls back a failed received write and completes all delivery tracking on retry', async () => {
|
|
1873
|
+
const { boostUri, inboxId } = await preparePendingBoost('atomic-receipt@test.com');
|
|
1874
|
+
const escrow = (await getInboxCredentialById(inboxId))!.credential;
|
|
1875
|
+
const originalCredentialIds = (await Credential.findMany()).map(record => record.id);
|
|
1876
|
+
const failedWrite = vi
|
|
1877
|
+
.spyOn(credentialRelationships, 'createReceivedCredentialRelationship')
|
|
1878
|
+
.mockRejectedValueOnce(new Error('injected received write failure'));
|
|
1879
|
+
try {
|
|
1880
|
+
await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
|
|
1881
|
+
claimed: 0,
|
|
1882
|
+
errors: 1,
|
|
1883
|
+
deliveries: [],
|
|
1884
|
+
});
|
|
1885
|
+
} finally {
|
|
1886
|
+
failedWrite.mockRestore();
|
|
1887
|
+
}
|
|
1888
|
+
expect(await getInboxCredentialById(inboxId)).toMatchObject({
|
|
1889
|
+
currentStatus: 'PENDING',
|
|
1890
|
+
credential: escrow,
|
|
1891
|
+
});
|
|
1892
|
+
expect((await Credential.findMany()).map(record => record.id).sort()).toEqual(
|
|
1893
|
+
originalCredentialIds.sort()
|
|
1894
|
+
);
|
|
1895
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toEqual([]);
|
|
1896
|
+
const auditBeforeRetry = await neogma.queryRunner.run(
|
|
1897
|
+
'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
|
|
1898
|
+
{ id: inboxId }
|
|
1899
|
+
);
|
|
1900
|
+
expect(auditBeforeRetry.records[0].get('count').toNumber()).toBe(0);
|
|
1901
|
+
expect(
|
|
1902
|
+
addNotificationToQueueSpy.mock.calls.some(
|
|
1903
|
+
([notification]) =>
|
|
1904
|
+
notification.type ===
|
|
1905
|
+
LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR &&
|
|
1906
|
+
notification.data?.inbox?.status === 'PENDING'
|
|
1907
|
+
)
|
|
1908
|
+
).toBe(true);
|
|
1909
|
+
addNotificationToQueueSpy.mockClear();
|
|
1910
|
+
|
|
1911
|
+
const retry = await userB.clients.fullAuth.inbox.finalize({});
|
|
1912
|
+
expect(retry).toMatchObject({ claimed: 1, errors: 0, deliveries: [{ id: inboxId }] });
|
|
1913
|
+
const finalized = await getInboxCredentialById(inboxId);
|
|
1914
|
+
expect(finalized?.currentStatus).toBe('ISSUED');
|
|
1915
|
+
expect(finalized?.credential).toBeUndefined();
|
|
1916
|
+
expect(
|
|
1917
|
+
await userA.clients.fullAuth.boost.getBoostRecipients({
|
|
1918
|
+
uri: boostUri,
|
|
1919
|
+
includeUnacceptedBoosts: false,
|
|
1920
|
+
})
|
|
1921
|
+
).toHaveLength(1);
|
|
1922
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toHaveLength(1);
|
|
1923
|
+
const auditAfterRetry = await neogma.queryRunner.run(
|
|
1924
|
+
'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
|
|
1925
|
+
{ id: inboxId }
|
|
1926
|
+
);
|
|
1927
|
+
expect(auditAfterRetry.records[0].get('count').toNumber()).toBe(1);
|
|
1928
|
+
expect(
|
|
1929
|
+
addNotificationToQueueSpy.mock.calls.some(
|
|
1930
|
+
([notification]) =>
|
|
1931
|
+
notification.type === LCNNotificationTypeEnumValidator.enum.ISSUANCE_ERROR
|
|
1932
|
+
)
|
|
1933
|
+
).toBe(false);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
it('rolls back partial claim permissions and grants hooks and the default role on retry', async () => {
|
|
1937
|
+
const { boostUri, inboxId } = await preparePendingBoost('atomic-permissions@test.com');
|
|
1938
|
+
const targetUri = await userA.clients.fullAuth.boost.createBoost({
|
|
1939
|
+
credential: testUnsignedBoost,
|
|
1940
|
+
});
|
|
1941
|
+
await userA.clients.fullAuth.claimHook.createClaimHook({
|
|
1942
|
+
hook: {
|
|
1943
|
+
type: 'GRANT_PERMISSIONS',
|
|
1944
|
+
data: { claimUri: boostUri, targetUri, permissions: { canIssue: true } },
|
|
1945
|
+
},
|
|
1946
|
+
});
|
|
1947
|
+
const failedRole = vi
|
|
1948
|
+
.spyOn(credentialRelationships, 'setDefaultClaimedRole')
|
|
1949
|
+
.mockRejectedValueOnce(new Error('injected claimed-role failure'));
|
|
1950
|
+
try {
|
|
1951
|
+
await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
|
|
1952
|
+
claimed: 0,
|
|
1953
|
+
errors: 1,
|
|
1954
|
+
});
|
|
1955
|
+
} finally {
|
|
1956
|
+
failedRole.mockRestore();
|
|
1957
|
+
}
|
|
1958
|
+
expect(await getInboxCredentialById(inboxId)).toMatchObject({
|
|
1959
|
+
currentStatus: 'PENDING',
|
|
1960
|
+
});
|
|
1961
|
+
expect(
|
|
1962
|
+
(await userB.clients.fullAuth.boost.getBoostPermissions({ uri: targetUri }))
|
|
1963
|
+
.canIssue
|
|
1964
|
+
).toBe(false);
|
|
1965
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toEqual([]);
|
|
1966
|
+
|
|
1967
|
+
await expect(userB.clients.fullAuth.inbox.finalize({})).resolves.toMatchObject({
|
|
1968
|
+
claimed: 1,
|
|
1969
|
+
errors: 0,
|
|
1970
|
+
});
|
|
1971
|
+
expect(
|
|
1972
|
+
(await userB.clients.fullAuth.boost.getBoostPermissions({ uri: targetUri }))
|
|
1973
|
+
.canIssue
|
|
1974
|
+
).toBe(true);
|
|
1975
|
+
expect(
|
|
1976
|
+
(await userB.clients.fullAuth.boost.getBoostPermissions({ uri: boostUri }))
|
|
1977
|
+
.canRevoke
|
|
1978
|
+
).toBe(true);
|
|
1979
|
+
const [received] = await userB.clients.fullAuth.credential.receivedCredentials({});
|
|
1980
|
+
expect(
|
|
1981
|
+
await userB.clients.fullAuth.credential.acceptCredential({ uri: received.uri })
|
|
1982
|
+
).toBe(true);
|
|
1983
|
+
const roleCounts = await neogma.queryRunner.run(
|
|
1984
|
+
'MATCH (:Profile {profileId: "userb"})-[r:HAS_ROLE]->(b:Boost) WHERE b.id IN $ids RETURN b.id AS id, count(r) AS count',
|
|
1985
|
+
{ ids: [boostUri, targetUri].map(uri => uri.split(':').at(-1)) }
|
|
1986
|
+
);
|
|
1987
|
+
expect(roleCounts.records.map(record => record.get('count').toNumber())).toEqual([
|
|
1988
|
+
1, 1,
|
|
1989
|
+
]);
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
it('commits only one accepted delivery and claim audit under concurrent finalization', async () => {
|
|
1993
|
+
const { inboxId } = await preparePendingBoost('atomic-concurrent@test.com');
|
|
1994
|
+
const results = await Promise.all(
|
|
1995
|
+
Array.from({ length: 3 }, () => userB.clients.fullAuth.inbox.finalize({}))
|
|
1996
|
+
);
|
|
1997
|
+
expect(results.reduce((count, result) => count + result.claimed, 0)).toBe(1);
|
|
1998
|
+
expect(results.reduce((count, result) => count + result.errors, 0)).toBe(0);
|
|
1999
|
+
expect(await userB.clients.fullAuth.credential.receivedCredentials({})).toHaveLength(1);
|
|
2000
|
+
const audit = await neogma.queryRunner.run(
|
|
2001
|
+
'MATCH ()-[r:CLAIMED_INBOX_CREDENTIAL]->(ic:InboxCredential {id: $id}) RETURN count(r) AS count',
|
|
2002
|
+
{ id: inboxId }
|
|
2003
|
+
);
|
|
2004
|
+
expect(audit.records[0].get('count').toNumber()).toBe(1);
|
|
2005
|
+
});
|
|
2006
|
+
|
|
1835
2007
|
it('should not allow finalize without full auth', async () => {
|
|
1836
2008
|
await expect(noAuthClient.inbox.finalize({})).rejects.toMatchObject({
|
|
1837
2009
|
code: 'UNAUTHORIZED',
|
|
@@ -1912,8 +2084,11 @@ describe('Universal Inbox', () => {
|
|
|
1912
2084
|
expect(res.claimed).toBe(2);
|
|
1913
2085
|
expect(res.errors).toBe(0);
|
|
1914
2086
|
expect(res.verifiableCredentials).toHaveLength(2);
|
|
1915
|
-
//
|
|
1916
|
-
for (const
|
|
2087
|
+
// The HTTP contract is encrypted; only the holder decrypts the signed VCs.
|
|
2088
|
+
for (const encrypted of res.verifiableCredentials) {
|
|
2089
|
+
const vc = await userB.learnCard.invoke.decryptDagJwe<VC>(
|
|
2090
|
+
JWEValidator.parse(encrypted)
|
|
2091
|
+
);
|
|
1917
2092
|
expect(vc.proof).toBeDefined();
|
|
1918
2093
|
}
|
|
1919
2094
|
|
|
@@ -2089,12 +2264,13 @@ describe('Universal Inbox', () => {
|
|
|
2089
2264
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
2090
2265
|
|
|
2091
2266
|
try {
|
|
2092
|
-
await
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2267
|
+
const result = await userB.clients.fullAuth.inbox.finalize({});
|
|
2268
|
+
expect(result).toMatchObject({ processed: 1, claimed: 1, errors: 0 });
|
|
2269
|
+
expect(
|
|
2270
|
+
await userB.learnCard.invoke.decryptDagJwe<VC>(
|
|
2271
|
+
JWEValidator.parse(result.verifiableCredentials[0])
|
|
2272
|
+
)
|
|
2273
|
+
).toMatchObject({ proof: expect.anything() });
|
|
2098
2274
|
} finally {
|
|
2099
2275
|
queryRunnerSpy.mockRestore();
|
|
2100
2276
|
consoleErrorSpy.mockRestore();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeInlineTemplateHash,
|
|
5
|
+
validateInlineTemplateDataOrThrow,
|
|
6
|
+
validateInlineTemplateOrThrow,
|
|
7
|
+
} from '../src/helpers/inline-template.helpers';
|
|
8
|
+
|
|
9
|
+
describe('inline-template.helpers', () => {
|
|
10
|
+
it('throws developer-readable template validation errors', () => {
|
|
11
|
+
expect(() =>
|
|
12
|
+
validateInlineTemplateOrThrow({
|
|
13
|
+
name: '',
|
|
14
|
+
image: 'http://not-https.example.com/badge.png',
|
|
15
|
+
})
|
|
16
|
+
).toThrow(/TEMPLATE_INVALID:/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('throws strict templateData validation errors with suggestions', () => {
|
|
20
|
+
const compiled = computeInlineTemplateHash({
|
|
21
|
+
name: 'Course Completion: {{courseName}}',
|
|
22
|
+
description: 'Awarded to {{learnerName}}',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(() =>
|
|
26
|
+
validateInlineTemplateDataOrThrow(compiled.variableManifest, {
|
|
27
|
+
courseNmae: 'Broken key',
|
|
28
|
+
learnerName: 'Ada',
|
|
29
|
+
})
|
|
30
|
+
).toThrow(
|
|
31
|
+
/TEMPLATE_DATA_INVALID: .*templateData.courseName is required.*did you mean "courseName"/
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('hashes template content deterministically', () => {
|
|
36
|
+
const first = computeInlineTemplateHash({
|
|
37
|
+
name: 'Course Completion: {{courseName}}',
|
|
38
|
+
description: 'Awarded to {{learnerName}}',
|
|
39
|
+
walletSkills: [{ frameworkId: 'framework-1', id: 'skill-1' }],
|
|
40
|
+
});
|
|
41
|
+
const second = computeInlineTemplateHash({
|
|
42
|
+
name: 'Course Completion: {{courseName}}',
|
|
43
|
+
description: 'Awarded to {{learnerName}}',
|
|
44
|
+
walletSkills: [{ frameworkId: 'framework-1', id: 'skill-1' }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(first.contentHash).toBe(second.contentHash);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -37,10 +37,13 @@ const clearDb = async (): Promise<void> => {
|
|
|
37
37
|
const createManagedRecipient = async (
|
|
38
38
|
profileId: string,
|
|
39
39
|
email: string,
|
|
40
|
-
isServiceProfile: boolean
|
|
40
|
+
isServiceProfile: boolean,
|
|
41
|
+
parentCategory = isServiceProfile ? 'ID' : 'Family',
|
|
42
|
+
profileType?: string
|
|
41
43
|
): Promise<void> => {
|
|
42
44
|
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
43
45
|
credential: testUnsignedBoost,
|
|
46
|
+
category: parentCategory,
|
|
44
47
|
});
|
|
45
48
|
const managerDid = await userA.clients.fullAuth.profileManager.createChildProfileManager({
|
|
46
49
|
parentUri: boostUri,
|
|
@@ -51,6 +54,7 @@ const createManagedRecipient = async (
|
|
|
51
54
|
profileId,
|
|
52
55
|
displayName: profileId,
|
|
53
56
|
isServiceProfile,
|
|
57
|
+
...(profileType ? { type: profileType } : {}),
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
const contactMethod = await createContactMethod({
|
|
@@ -95,6 +99,25 @@ describe('Inbox send to managed recipients', () => {
|
|
|
95
99
|
await clearDb();
|
|
96
100
|
});
|
|
97
101
|
|
|
102
|
+
it.each([
|
|
103
|
+
{ parentCategory: 'Family', profileType: undefined },
|
|
104
|
+
{ parentCategory: 'ID', profileType: 'child' },
|
|
105
|
+
])(
|
|
106
|
+
'rejects a service flag for child creation through $parentCategory / $profileType',
|
|
107
|
+
async ({ parentCategory, profileType }) => {
|
|
108
|
+
await expect(
|
|
109
|
+
createManagedRecipient(
|
|
110
|
+
'service-child',
|
|
111
|
+
'child@example.com',
|
|
112
|
+
true,
|
|
113
|
+
parentCategory,
|
|
114
|
+
profileType
|
|
115
|
+
)
|
|
116
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
117
|
+
expect(await Profile.findMany({ where: { profileId: 'service-child' } })).toEqual([]);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
98
121
|
it('delivers directly to a managed service profile without a guardian gate', async () => {
|
|
99
122
|
await createManagedRecipient('managed-org', 'org@example.com', true);
|
|
100
123
|
|
|
@@ -114,7 +137,6 @@ describe('Inbox send to managed recipients', () => {
|
|
|
114
137
|
const result = await sendTo('child@example.com');
|
|
115
138
|
|
|
116
139
|
expect(result.inbox?.status).toBe('PENDING');
|
|
117
|
-
expect(result.inbox?.guardianStatus).toBe('AWAITING_GUARDIAN');
|
|
118
140
|
|
|
119
141
|
const credentials = await InboxCredential.findMany({ where: {} });
|
|
120
142
|
expect(credentials).toHaveLength(1);
|