@learncard/network-brain-service 4.0.3 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +14 -0
- package/CHANGELOG.md +76 -0
- package/config/config.dev.json +5 -0
- package/config/config.example.json +5 -0
- package/config/config.production.json +5 -0
- package/config/config.scouts.dev.json +5 -0
- package/config/config.scouts.production.json +5 -0
- package/dist/accesslayer/campaign/audience.d.ts +7 -0
- package/dist/accesslayer/campaign/audience.d.ts.map +1 -0
- package/dist/accesslayer/campaign/create.d.ts +3 -0
- package/dist/accesslayer/campaign/create.d.ts.map +1 -0
- package/dist/accesslayer/campaign/delete.d.ts +2 -0
- package/dist/accesslayer/campaign/delete.d.ts.map +1 -0
- package/dist/accesslayer/campaign/read.d.ts +6 -0
- package/dist/accesslayer/campaign/read.d.ts.map +1 -0
- package/dist/accesslayer/campaign/shared.d.ts +16 -0
- package/dist/accesslayer/campaign/shared.d.ts.map +1 -0
- package/dist/accesslayer/campaign/state.d.ts +4 -0
- package/dist/accesslayer/campaign/state.d.ts.map +1 -0
- package/dist/accesslayer/campaign/update.d.ts +3 -0
- package/dist/accesslayer/campaign/update.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential-share-previews.d.ts +5 -0
- package/dist/accesslayer/credential-share-previews.d.ts.map +1 -0
- package/dist/accesslayer/inbox-batch/schema.d.ts +4 -0
- package/dist/accesslayer/inbox-batch/schema.d.ts.map +1 -0
- package/dist/accesslayer/inbox-batch/schema.test.d.ts +2 -0
- package/dist/accesslayer/inbox-batch/schema.test.d.ts.map +1 -0
- package/dist/accesslayer/inbox-batch/store.d.ts +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -1
- package/dist/accesslayer/profile/delete.d.ts.map +1 -1
- package/dist/accesslayer/skill/read.d.ts +12 -2
- package/dist/accesslayer/skill/read.d.ts.map +1 -1
- package/dist/accesslayer/skill/update.d.ts +11 -1
- package/dist/accesslayer/skill/update.d.ts.map +1 -1
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +2 -1
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/environment.d.ts +268 -178
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/skillEmbedding.d.ts +70 -0
- package/dist/config/skillEmbedding.d.ts.map +1 -0
- package/dist/config/skillEmbedding.neo4j.test.d.ts +2 -0
- package/dist/config/skillEmbedding.neo4j.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +20 -0
- package/dist/config/stageConfig.d.ts.map +1 -1
- package/dist/constants/campaigns.d.ts +3 -0
- package/dist/constants/campaigns.d.ts.map +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/helpers/bedrock-embedding.helpers.d.ts +40 -0
- package/dist/helpers/bedrock-embedding.helpers.d.ts.map +1 -0
- package/dist/helpers/bedrock-embedding.helpers.test.d.ts +2 -0
- package/dist/helpers/bedrock-embedding.helpers.test.d.ts.map +1 -0
- package/dist/helpers/bedrock-embedding.smoke.test.d.ts +2 -0
- package/dist/helpers/bedrock-embedding.smoke.test.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts +1 -0
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/skill-embedding.helpers.d.ts +65 -7
- package/dist/helpers/skill-embedding.helpers.d.ts.map +1 -1
- package/dist/helpers/skill-embedding.helpers.test.d.ts +2 -0
- package/dist/helpers/skill-embedding.helpers.test.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/Campaign.d.ts +10 -0
- package/dist/models/Campaign.d.ts.map +1 -0
- package/dist/models/CredentialSharePreview.d.ts +14 -0
- package/dist/models/CredentialSharePreview.d.ts.map +1 -0
- package/dist/models/Integration.d.ts +1 -1
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/campaign-constraints.d.ts +2 -0
- package/dist/models/campaign-constraints.d.ts.map +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/routes/app-store.d.ts +1 -1
- package/dist/routes/boosts.d.ts +24 -4
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/campaigns.d.ts +1411 -0
- package/dist/routes/campaigns.d.ts.map +1 -0
- package/dist/routes/contracts.d.ts +19 -6
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-share-previews.d.ts +39 -0
- package/dist/routes/credential-share-previews.d.ts.map +1 -0
- package/dist/routes/credentials.d.ts +4 -1
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +5 -5
- package/dist/routes/index.d.ts +4 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/integrations.d.ts +3 -3
- package/dist/routes/profile-manager.d.ts +4 -0
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts +44 -0
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/skill-frameworks.d.ts +1 -0
- package/dist/routes/skill-frameworks.d.ts.map +1 -1
- package/dist/routes/skills-semantic-search.test.d.ts +2 -0
- package/dist/routes/skills-semantic-search.test.d.ts.map +1 -0
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/test.d.ts +3 -1
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/seed/seedSkillFrameworks.d.ts +1 -0
- package/dist/seed/seedSkillFrameworks.d.ts.map +1 -1
- package/dist/seed/seedSkillFrameworks.embeddings.test.d.ts +2 -0
- package/dist/seed/seedSkillFrameworks.embeddings.test.d.ts.map +1 -0
- package/dist/types/app-manifest.d.ts +2 -2
- package/dist/types/campaign.d.ts +19 -0
- package/dist/types/campaign.d.ts.map +1 -0
- package/dist/types/consentflowcontract.d.ts +1 -1
- package/dist/types/integration.d.ts +3 -3
- package/dist/types/profile.d.ts +9 -0
- package/dist/types/profile.d.ts.map +1 -1
- package/dist/types/skill.d.ts.map +1 -1
- package/esbuildPlugins.cjs +9 -6
- package/package.json +20 -17
- package/project.json +14 -0
- package/scripts/skill-embeddings-backfill.ts +161 -0
- package/serverless-local.yml +4 -0
- package/serverless.function-env.cjs +3 -0
- package/serverless.local.yml +4 -0
- package/serverless.yml +12 -1
- package/src/accesslayer/campaign/README.md +174 -0
- package/src/accesslayer/campaign/audience.ts +152 -0
- package/src/accesslayer/campaign/create.ts +47 -0
- package/src/accesslayer/campaign/delete.ts +18 -0
- package/src/accesslayer/campaign/read.ts +125 -0
- package/src/accesslayer/campaign/shared.ts +92 -0
- package/src/accesslayer/campaign/state.ts +80 -0
- package/src/accesslayer/campaign/update.ts +50 -0
- package/src/accesslayer/consentflowcontract/relationships/create.ts +3 -1
- package/src/accesslayer/consentflowcontract/relationships/update.ts +5 -1
- package/src/accesslayer/credential-share-previews.ts +76 -0
- package/src/accesslayer/inbox-batch/schema.test.ts +91 -0
- package/src/accesslayer/inbox-batch/schema.ts +54 -0
- package/src/accesslayer/inbox-batch/store.ts +2 -38
- package/src/accesslayer/profile/delete.ts +6 -0
- package/src/accesslayer/skill/read.ts +56 -16
- package/src/accesslayer/skill/update.ts +43 -1
- package/src/app.ts +9 -0
- package/src/config/environment.test.ts +208 -0
- package/src/config/environment.ts +166 -11
- package/src/config/skillEmbedding.neo4j.test.ts +138 -0
- package/src/config/skillEmbedding.ts +202 -0
- package/src/constants/campaigns.ts +9 -0
- package/src/helpers/bedrock-embedding.helpers.test.ts +208 -0
- package/src/helpers/bedrock-embedding.helpers.ts +243 -0
- package/src/helpers/bedrock-embedding.smoke.test.ts +53 -0
- package/src/helpers/boost-storage.helpers.test.ts +155 -1
- package/src/helpers/boost.helpers.ts +52 -10
- package/src/helpers/skill-embedding.helpers.test.ts +1044 -0
- package/src/helpers/skill-embedding.helpers.ts +611 -123
- package/src/models/Campaign.ts +32 -0
- package/src/models/CredentialSharePreview.ts +30 -0
- package/src/models/Profile.ts +1 -0
- package/src/models/campaign-constraints.ts +22 -0
- package/src/models/index.ts +29 -1
- package/src/routes/campaigns.ts +179 -0
- package/src/routes/credential-share-previews.ts +74 -0
- package/src/routes/profiles.ts +7 -0
- package/src/routes/skills-semantic-search.test.ts +168 -0
- package/src/routes/skills.ts +42 -6
- package/src/routes/workflows.ts +8 -1
- package/src/seed/seedSkillFrameworks.embeddings.test.ts +82 -0
- package/src/seed/seedSkillFrameworks.test.ts +6 -0
- package/src/seed/seedSkillFrameworks.ts +83 -18
- package/src/types/campaign.ts +17 -0
- package/src/types/skill.ts +6 -0
- package/test/campaigns.spec.ts +789 -0
- package/test/consentflow.spec.ts +101 -0
- package/test/credential-share-previews.spec.ts +97 -0
- package/test/inbox-batch.spec.ts +189 -1
- package/test/verification/campaigns.ts +49 -0
- package/vitest.campaigns.config.ts +21 -0
- package/vitest.config.ts +11 -0
- package/vitest.integration.config.ts +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-2206 opt-in smoke test against REAL Amazon Bedrock (paid: three InvokeModel calls).
|
|
3
|
+
* Skipped unless SKILL_EMBEDDING_BEDROCK_SMOKE=true. Never enable it in CI.
|
|
4
|
+
*
|
|
5
|
+
* SKILL_EMBEDDING_BEDROCK_SMOKE=true AWS_PROFILE=<profile> \
|
|
6
|
+
* bunx vitest run --config vitest.config.ts src/helpers/bedrock-embedding.smoke.test.ts
|
|
7
|
+
*
|
|
8
|
+
* Uses the normal AWS credential chain (SigV4) in us-east-1 and the same request shape,
|
|
9
|
+
* purposes, dimension and normalisation as production.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_SKILL_EMBEDDING_DIMENSION,
|
|
15
|
+
DEFAULT_SKILL_EMBEDDING_REGION,
|
|
16
|
+
NOVA2_EMBEDDING_MODEL_ID,
|
|
17
|
+
} from '../config/skillEmbedding';
|
|
18
|
+
import { embedTextWithBedrock, type BedrockEmbeddingConfig } from './bedrock-embedding.helpers';
|
|
19
|
+
|
|
20
|
+
const enabled = process.env.SKILL_EMBEDDING_BEDROCK_SMOKE === 'true';
|
|
21
|
+
|
|
22
|
+
const config: BedrockEmbeddingConfig = {
|
|
23
|
+
modelId: process.env.SKILL_EMBEDDING_BEDROCK_MODEL_ID || NOVA2_EMBEDDING_MODEL_ID,
|
|
24
|
+
region: process.env.SKILL_EMBEDDING_BEDROCK_REGION || DEFAULT_SKILL_EMBEDDING_REGION,
|
|
25
|
+
dimension: Number(process.env.SKILL_EMBEDDING_DIMENSION || DEFAULT_SKILL_EMBEDDING_DIMENSION),
|
|
26
|
+
timeoutMs: 15_000,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const dot = (a: number[], b: number[]) =>
|
|
30
|
+
a.reduce((sum, value, index) => sum + value * (b[index] ?? 0), 0);
|
|
31
|
+
|
|
32
|
+
describe.skipIf(!enabled)('Bedrock Nova 2 embeddings smoke (real AWS, opt-in)', () => {
|
|
33
|
+
it('returns unit vectors where a related skill outranks an unrelated one', async () => {
|
|
34
|
+
const query = await embedTextWithBedrock('python data analysis', 'query', config);
|
|
35
|
+
const related = await embedTextWithBedrock(
|
|
36
|
+
'Data Analysis\nAnalyse datasets with Python and statistics',
|
|
37
|
+
'index',
|
|
38
|
+
config
|
|
39
|
+
);
|
|
40
|
+
const unrelated = await embedTextWithBedrock(
|
|
41
|
+
'Customer service\nHelp customers resolve issues politely',
|
|
42
|
+
'index',
|
|
43
|
+
config
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (const vector of [query, related, unrelated]) {
|
|
47
|
+
expect(vector).toHaveLength(config.dimension);
|
|
48
|
+
expect(Math.hypot(...vector)).toBeCloseTo(1, 6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(dot(query, related)).toBeGreaterThan(dot(query, unrelated));
|
|
52
|
+
}, 60_000);
|
|
53
|
+
});
|
|
@@ -65,7 +65,12 @@ vi.mock('@tracing', () => ({
|
|
|
65
65
|
traceInternal: async (_name: string, action: () => unknown) => action(),
|
|
66
66
|
}));
|
|
67
67
|
|
|
68
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
issueClaimLinkBoost,
|
|
70
|
+
prepareCredentialFromBoost,
|
|
71
|
+
sendBoost,
|
|
72
|
+
stampIssuanceDate,
|
|
73
|
+
} from './boost.helpers';
|
|
69
74
|
|
|
70
75
|
const vc = {
|
|
71
76
|
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
@@ -223,6 +228,155 @@ describe('boost payload storage', () => {
|
|
|
223
228
|
}
|
|
224
229
|
);
|
|
225
230
|
|
|
231
|
+
describe('malformed template issuance dates', () => {
|
|
232
|
+
// Seen on a production Boost template; it is not RFC 3339, so DIDKit refuses to sign it.
|
|
233
|
+
const MALFORMED_DATE = '2025-10-19+053008:18:16';
|
|
234
|
+
// The reused JOTA-JOTI "Health and Well-being" Boost that carried it.
|
|
235
|
+
const BOOST_ID = 'e073d183-8304-419f-b4f6-ef91fda6524c';
|
|
236
|
+
const V1 = ['https://www.w3.org/2018/credentials/v1'];
|
|
237
|
+
const V2 = ['https://www.w3.org/ns/credentials/v2'];
|
|
238
|
+
const authority = { relationship: { did: 'did:example:sa' } } as Parameters<
|
|
239
|
+
typeof issueClaimLinkBoost
|
|
240
|
+
>[4];
|
|
241
|
+
const boostWith = (template: Record<string, unknown>) =>
|
|
242
|
+
({
|
|
243
|
+
...options.boost,
|
|
244
|
+
dataValues: {
|
|
245
|
+
id: BOOST_ID,
|
|
246
|
+
boost: JSON.stringify({ ...vc, boostId: undefined, ...template }),
|
|
247
|
+
},
|
|
248
|
+
}) as Parameters<typeof issueClaimLinkBoost>[0];
|
|
249
|
+
const expectFreshDate = (value: unknown, before: number) => {
|
|
250
|
+
expect(typeof value).toBe('string');
|
|
251
|
+
expect(value).toBe(new Date(value as string).toISOString());
|
|
252
|
+
expect(Date.parse(value as string)).toBeGreaterThanOrEqual(before);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
mocks.signingAuthority.mockResolvedValue({
|
|
257
|
+
kind: 'issued-credential',
|
|
258
|
+
credential: jwe,
|
|
259
|
+
statusEntries: [],
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it.each([
|
|
264
|
+
[
|
|
265
|
+
'VC 1.1 issuanceDate',
|
|
266
|
+
{ '@context': V1, issuanceDate: MALFORMED_DATE },
|
|
267
|
+
'issuanceDate',
|
|
268
|
+
],
|
|
269
|
+
[
|
|
270
|
+
'VC 2.0 validFrom',
|
|
271
|
+
{ '@context': V2, issuanceDate: undefined, validFrom: MALFORMED_DATE },
|
|
272
|
+
'validFrom',
|
|
273
|
+
],
|
|
274
|
+
] as const)(
|
|
275
|
+
'restamps a malformed %s when a claim link is claimed',
|
|
276
|
+
async (_kind, template, field) => {
|
|
277
|
+
const before = Date.now();
|
|
278
|
+
await issueClaimLinkBoost(
|
|
279
|
+
boostWith(template),
|
|
280
|
+
options.domain,
|
|
281
|
+
options.from,
|
|
282
|
+
options.to,
|
|
283
|
+
authority
|
|
284
|
+
);
|
|
285
|
+
const unsigned = mocks.signingAuthority.mock.calls[0]![1];
|
|
286
|
+
expectFreshDate(unsigned[field], before);
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
it('drops a stale issuanceDate left on a VC 2.0 template', async () => {
|
|
291
|
+
const before = Date.now();
|
|
292
|
+
await issueClaimLinkBoost(
|
|
293
|
+
boostWith({ '@context': V2, issuanceDate: MALFORMED_DATE }),
|
|
294
|
+
options.domain,
|
|
295
|
+
options.from,
|
|
296
|
+
options.to,
|
|
297
|
+
authority
|
|
298
|
+
);
|
|
299
|
+
const unsigned = mocks.signingAuthority.mock.calls[0]![1];
|
|
300
|
+
expectFreshDate(unsigned.validFrom, before);
|
|
301
|
+
expect(unsigned).not.toHaveProperty('issuanceDate');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('drops a stale issuanceDate left on a VC 2.0 template for direct sends', async () => {
|
|
305
|
+
const before = Date.now();
|
|
306
|
+
const credential = await prepareCredentialFromBoost(
|
|
307
|
+
boostWith({ '@context': V2, issuanceDate: MALFORMED_DATE }),
|
|
308
|
+
`lc:network:network.example/boost:${BOOST_ID}`,
|
|
309
|
+
options.domain
|
|
310
|
+
);
|
|
311
|
+
expectFreshDate(credential.validFrom, before);
|
|
312
|
+
expect(credential).not.toHaveProperty('issuanceDate');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it.each([
|
|
316
|
+
{ credentialSubject: { creditsEarned: 'not a number' } },
|
|
317
|
+
{ awardedDate: 'bad-date' },
|
|
318
|
+
])('reports invalid standard fields as BAD_REQUEST', async fields => {
|
|
319
|
+
await expect(
|
|
320
|
+
prepareCredentialFromBoost(
|
|
321
|
+
boostWith({
|
|
322
|
+
'@context': V2,
|
|
323
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential'],
|
|
324
|
+
proof: undefined,
|
|
325
|
+
...fields,
|
|
326
|
+
}),
|
|
327
|
+
`lc:network:network.example/boost:${BOOST_ID}`,
|
|
328
|
+
options.domain
|
|
329
|
+
)
|
|
330
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it.each(['', ' '])(
|
|
334
|
+
'omits blank awardedDate after template substitution %j',
|
|
335
|
+
async completed => {
|
|
336
|
+
const credential = await prepareCredentialFromBoost(
|
|
337
|
+
boostWith({
|
|
338
|
+
'@context': V2,
|
|
339
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential'],
|
|
340
|
+
proof: undefined,
|
|
341
|
+
awardedDate: '{{completed}}',
|
|
342
|
+
}),
|
|
343
|
+
`lc:network:network.example/boost:${BOOST_ID}`,
|
|
344
|
+
options.domain,
|
|
345
|
+
{ templateData: { completed } }
|
|
346
|
+
);
|
|
347
|
+
expect(credential).not.toHaveProperty('awardedDate');
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
describe('stampIssuanceDate', () => {
|
|
352
|
+
const NOW = '2026-10-18T05:00:00.000Z';
|
|
353
|
+
|
|
354
|
+
it('sets validFrom and removes issuanceDate on VC 2.0', () => {
|
|
355
|
+
const stamped = stampIssuanceDate(
|
|
356
|
+
{
|
|
357
|
+
...vc,
|
|
358
|
+
'@context': V2,
|
|
359
|
+
validFrom: MALFORMED_DATE,
|
|
360
|
+
issuanceDate: MALFORMED_DATE,
|
|
361
|
+
},
|
|
362
|
+
NOW
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(stamped.validFrom).toBe(NOW);
|
|
366
|
+
expect(stamped).not.toHaveProperty('issuanceDate');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('only sets issuanceDate on VC 1.1', () => {
|
|
370
|
+
const template = { ...vc, '@context': V1, issuanceDate: MALFORMED_DATE };
|
|
371
|
+
|
|
372
|
+
expect(stampIssuanceDate({ ...template }, NOW)).toEqual({
|
|
373
|
+
...template,
|
|
374
|
+
issuanceDate: NOW,
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
226
380
|
it('preserves explicit notification and acceptance opt-outs', async () => {
|
|
227
381
|
await sendBoost({
|
|
228
382
|
...options,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import cloneDeep from 'lodash/cloneDeep';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CredentialFieldError,
|
|
4
|
+
isEncrypted,
|
|
5
|
+
isVC2Format,
|
|
6
|
+
materializeCredentialFields,
|
|
7
|
+
} from '@learncard/helpers';
|
|
3
8
|
import { TRPCError } from '@trpc/server';
|
|
4
9
|
import {
|
|
5
10
|
VC,
|
|
@@ -87,6 +92,29 @@ export const isBoostViewableByClaimLink = async (boost: BoostInstance): Promise<
|
|
|
87
92
|
return true;
|
|
88
93
|
};
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Stamps the issuance time onto a credential built from a Boost template.
|
|
97
|
+
*
|
|
98
|
+
* A template's own issuanceDate/validFrom is when the template was written, not when this
|
|
99
|
+
* credential is issued, and templates created through the API are not date-validated: a
|
|
100
|
+
* malformed value (e.g. "2025-10-19+053008:18:16") makes DIDKit refuse to sign. VC 2.0
|
|
101
|
+
* templates get validFrom and lose any leftover issuanceDate, which the VC 2.0 context doesn't
|
|
102
|
+
* define (DIDKit rejects it as an undefined JSON-LD term). VC 1.1 templates get issuanceDate.
|
|
103
|
+
*/
|
|
104
|
+
export const stampIssuanceDate = <T extends UnsignedVC | VC>(
|
|
105
|
+
credential: T,
|
|
106
|
+
now: string = new Date().toISOString()
|
|
107
|
+
): T => {
|
|
108
|
+
if (isVC2Format(credential)) {
|
|
109
|
+
credential.validFrom = now;
|
|
110
|
+
delete credential.issuanceDate;
|
|
111
|
+
} else {
|
|
112
|
+
credential.issuanceDate = now;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return credential;
|
|
116
|
+
};
|
|
117
|
+
|
|
90
118
|
export const convertCredentialToBoostTemplateJSON = (
|
|
91
119
|
credential: VC | UnsignedVC,
|
|
92
120
|
defaultIssuerDid?: string
|
|
@@ -255,6 +283,8 @@ export const issueClaimLinkBoost = async (
|
|
|
255
283
|
|
|
256
284
|
boostCredential.issuer = signingAuthorityForUser.relationship.did;
|
|
257
285
|
|
|
286
|
+
stampIssuanceDate(boostCredential);
|
|
287
|
+
|
|
258
288
|
setCredentialSubjectIds(boostCredential, getDidWeb(domain, to.profileId));
|
|
259
289
|
|
|
260
290
|
// Embed the boostURI into the boost credential for verification purposes.
|
|
@@ -331,17 +361,20 @@ export const prepareCredentialFromBoost = async (
|
|
|
331
361
|
boostJsonString = renderBoostTemplate(boostJsonString, mergedTemplateData);
|
|
332
362
|
}
|
|
333
363
|
|
|
334
|
-
|
|
364
|
+
let credential: UnsignedVC;
|
|
365
|
+
try {
|
|
366
|
+
credential = materializeCredentialFields(
|
|
367
|
+
parseRenderedTemplate<UnsignedVC>(boostJsonString)
|
|
368
|
+
) as UnsignedVC;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (error instanceof CredentialFieldError)
|
|
371
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: error.message, cause: error });
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
335
374
|
|
|
336
375
|
appendTemplateEvidenceToCredential(credential, mergedTemplateData, allowAutoAppendEvidence);
|
|
337
376
|
|
|
338
|
-
|
|
339
|
-
const now = new Date().toISOString();
|
|
340
|
-
if (isVC2Format(credential)) {
|
|
341
|
-
credential.validFrom = now;
|
|
342
|
-
} else {
|
|
343
|
-
credential.issuanceDate = now;
|
|
344
|
-
}
|
|
377
|
+
stampIssuanceDate(credential);
|
|
345
378
|
|
|
346
379
|
// Set issuer if provided
|
|
347
380
|
if (issuerDid) {
|
|
@@ -432,7 +465,16 @@ export const appendTemplateEvidenceToCredential = (
|
|
|
432
465
|
// EvidenceFile subterms — otherwise the signer fails later with an opaque
|
|
433
466
|
// JSON-LD key-expansion error. Surface a clear message instead so the
|
|
434
467
|
// caller can prompt the user to republish the boost.
|
|
435
|
-
|
|
468
|
+
const usesEvidenceExtensions = normalizedTemplateEvidence.some(item => {
|
|
469
|
+
if (!item || typeof item !== 'object') return false;
|
|
470
|
+
const evidence = item as Record<string, unknown>;
|
|
471
|
+
const types = Array.isArray(evidence.type) ? evidence.type : [evidence.type];
|
|
472
|
+
return (
|
|
473
|
+
types.includes('EvidenceFile') ||
|
|
474
|
+
['fileName', 'fileType', 'fileSize'].some(key => key in evidence)
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
if (usesEvidenceExtensions && !credentialContextSupportsDynamicEvidence(credential)) {
|
|
436
478
|
throw new TRPCError({
|
|
437
479
|
code: 'PRECONDITION_FAILED',
|
|
438
480
|
message:
|