@learncard/network-plugin 2.14.0 → 2.14.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.
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQvD,OAAO,EACH,sCAAsC,EACtC,sBAAsB,EACtB,iBAAiB,EAEpB,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAyaD,cAAc,SAAS,CAAC;AAExB,MAAM,MAAM,sBAAsB,GAAG,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAE5F;;GAEG;AACH,+CAA+C;AAC/C,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACnE,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,wBAAsB,yBAAyB,CAC3C,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,sCAAsC,CAAC,EACvE,GAAG,EAAE,MAAM,EACX,iBAAiB,CAAC,EAAE,6BAA6B,GAClD,OAAO,CAAC,sBAAsB,CAAC,CAAC;AACnC,wBAAsB,yBAAyB,CAC3C,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,sCAAsC,CAAC,EACtE,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,6BAA6B,GACxC,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAk3EnC;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAC7B,WAAW,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,eAAe,CAAC,EAC/C,0BAA0B,MAAM,KACjC,OAAO,CAAC,iBAAiB,CA8G3B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAQvD,OAAO,EACH,sCAAsC,EACtC,sBAAsB,EACtB,iBAAiB,EAEpB,MAAM,SAAS,CAAC;AAUjB;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAyaD,cAAc,SAAS,CAAC;AAExB,MAAM,MAAM,sBAAsB,GAAG,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAE5F;;GAEG;AACH,+CAA+C;AAC/C,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACnE,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,wBAAsB,yBAAyB,CAC3C,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,sCAAsC,CAAC,EACvE,GAAG,EAAE,MAAM,EACX,iBAAiB,CAAC,EAAE,6BAA6B,GAClD,OAAO,CAAC,sBAAsB,CAAC,CAAC;AACnC,wBAAsB,yBAAyB,CAC3C,SAAS,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,sCAAsC,CAAC,EACtE,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,6BAA6B,GACxC,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAk3EnC;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAC7B,WAAW,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,eAAe,CAAC,EAC/C,0BAA0B,MAAM,KACjC,OAAO,CAAC,iBAAiB,CAgJ3B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=verify-boost-regression.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-boost-regression.test.d.ts","sourceRoot":"","sources":["../../src/test/verify-boost-regression.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/network-plugin",
3
- "version": "2.14.0",
3
+ "version": "2.14.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -39,10 +39,10 @@
39
39
  "url": "https://github.com/learningeconomy/LearnCard/issues"
40
40
  },
41
41
  "devDependencies": {
42
- "@learncard/didkey-plugin": "^1.1.34",
43
- "@learncard/didkit-plugin": "^1.9.14",
44
- "@learncard/types": "5.19.0",
45
- "@learncard/vc-plugin": "^1.6.0",
42
+ "@learncard/didkey-plugin": "^1.1.35",
43
+ "@learncard/didkit-plugin": "^1.10.0",
44
+ "@learncard/types": "5.20.0",
45
+ "@learncard/vc-plugin": "^1.6.1",
46
46
  "@types/node": "^17.0.31",
47
47
  "aqu": "0.4.3",
48
48
  "esbuild": "^0.27.1",
@@ -52,9 +52,9 @@
52
52
  },
53
53
  "types": "./dist/index.d.ts",
54
54
  "dependencies": {
55
- "@learncard/core": "9.4.34",
56
- "@learncard/helpers": "1.5.0",
57
- "@learncard/network-brain-client": "2.5.55",
55
+ "@learncard/core": "9.4.35",
56
+ "@learncard/helpers": "1.5.1",
57
+ "@learncard/network-brain-client": "2.5.56",
58
58
  "mustache": "^4.2.0"
59
59
  },
60
60
  "sideEffects": false
package/src/plugin.ts CHANGED
@@ -2974,6 +2974,13 @@ export const getVerifyBoostPlugin = async (
2974
2974
  if (!issuerDID) return;
2975
2975
  return boostRegistry.find(o => o.did === issuerDID);
2976
2976
  };
2977
+ const getTrustedBoostNetwork = (boostId: unknown): TrustedBoostRegistryEntry | undefined => {
2978
+ if (typeof boostId !== 'string') return;
2979
+ const match = /^lc:network:([^?#\s]+)\/(?:trpc:)?boost:([^/?#\s]+)$/.exec(boostId);
2980
+ if (!match) return;
2981
+ const networkDid = `did:web:${match[1]!.replace(/\//g, ':')}`;
2982
+ return boostRegistry.find(entry => entry.did === networkDid);
2983
+ };
2977
2984
  return {
2978
2985
  name: 'VerifyBoost',
2979
2986
  displayName: 'Verify Boost Extension',
@@ -2984,20 +2991,31 @@ export const getVerifyBoostPlugin = async (
2984
2991
  credential,
2985
2992
  options
2986
2993
  );
2994
+ const hasOuterVerificationErrors = Boolean(verificationCheck.errors?.length);
2987
2995
  const boostCredential = credential?.boostCredential;
2988
2996
  try {
2989
- if (boostCredential) {
2990
- const verifyBoostCredential =
2991
- await learnCard.invoke.verifyCredential(boostCredential);
2992
- const boostCredentialErrors = verifyBoostCredential.errors ?? [];
2993
- if (verifyBoostCredential.status?.length) {
2997
+ // Legacy credentials contain a separately signed inner VC. New
2998
+ // credentials are the issuer-signed VC itself with boostId metadata.
2999
+ const boostId = boostCredential?.boostId ?? credential?.boostId;
3000
+ if (
3001
+ boostCredential ||
3002
+ credential?.boostId ||
3003
+ credential?.type?.includes('BoostCredential')
3004
+ ) {
3005
+ const verifyBoostCredential = boostCredential
3006
+ ? await learnCard.invoke.verifyCredential(boostCredential)
3007
+ : verificationCheck;
3008
+ const boostCredentialErrors = boostCredential
3009
+ ? (verifyBoostCredential.errors ?? [])
3010
+ : [];
3011
+ if (boostCredential && verifyBoostCredential.status?.length) {
2994
3012
  verificationCheck.status = [
2995
3013
  ...(verificationCheck.status ?? []),
2996
3014
  ...verifyBoostCredential.status,
2997
3015
  ];
2998
3016
  }
2999
3017
 
3000
- if (!boostCredential?.boostId && !credential?.boostId) {
3018
+ if (!boostId) {
3001
3019
  verificationCheck.warnings.push(
3002
3020
  'Boost Authenticity could not be verified: Boost ID metadata is missing.'
3003
3021
  );
@@ -3019,21 +3037,37 @@ export const getVerifyBoostPlugin = async (
3019
3037
  ...(verificationCheck.errors || []),
3020
3038
  'Boost Credential could not be verified.',
3021
3039
  ];
3022
- } else if (boostCredential?.boostId !== credential?.boostId) {
3040
+ } else if (hasOuterVerificationErrors) {
3041
+ verificationCheck.warnings.push(
3042
+ 'Boost Authenticity could not be verified: Credential verification failed.'
3043
+ );
3044
+ } else if (!boostId) {
3045
+ // The missing metadata warning above explains why trust is unknown.
3046
+ } else if (
3047
+ boostCredential &&
3048
+ boostCredential.boostId !== credential.boostId
3049
+ ) {
3023
3050
  verificationCheck.errors.push(
3024
3051
  'Boost Authenticity could not be verified: Boost ID metadata is mismatched.'
3025
3052
  );
3026
3053
  } else {
3027
- const trustedBoostIssuer = getTrustedBoostVerifier(credential?.issuer);
3054
+ // A direct VC is signed by the issuing authority, not by the
3055
+ // network. Its signed boostId identifies the associated network.
3056
+ // Legacy wrappers still establish trust through the outer issuer.
3057
+ const trustedBoostIssuer = boostCredential
3058
+ ? getTrustedBoostVerifier(credential?.issuer)
3059
+ : getTrustedBoostNetwork(boostId);
3028
3060
  if (trustedBoostIssuer) {
3029
3061
  verificationCheck.checks.push(
3030
3062
  `Boost is Authentic. Verified by ${trustedBoostIssuer.id}.`
3031
3063
  );
3032
3064
  } else {
3033
3065
  verificationCheck.warnings.push(
3034
- `Boost Authenticity could not be verified. Issuer is outside of trust network: ${getIssuerDID(
3035
- credential?.issuer
3036
- )}`
3066
+ boostCredential
3067
+ ? `Boost Authenticity could not be verified. Issuer is outside of trust network: ${getIssuerDID(
3068
+ credential?.issuer
3069
+ )}`
3070
+ : `Boost Authenticity could not be verified. Boost ID does not identify a trusted network: ${boostId}`
3037
3071
  );
3038
3072
  }
3039
3073
  }
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { VC } from '@learncard/types';
3
+
4
+ import { getVerifyBoostPlugin } from '../plugin';
5
+
6
+ const trustedIssuer = 'did:web:network.learncard.com';
7
+ const boostId = 'lc:network:network.learncard.com/trpc:boost:test';
8
+ const result = (errors: string[] = []) => ({ errors, warnings: [], checks: ['proof', 'status'] });
9
+
10
+ describe('VerifyBoost wrapper transition', () => {
11
+ it.each([
12
+ ['lc:network:localhost%3A4000/trpc:boost:test', 'did:web:localhost%3A4000'],
13
+ ['lc:network:preview.example/brain/trpc:boost:test', 'did:web:preview.example:brain'],
14
+ ['lc:network:preview.example/trpc/trpc:boost:test', 'did:web:preview.example:trpc'],
15
+ ['lc:network:network.learncard.com/boost:test', trustedIssuer],
16
+ ])('resolves the registry network from %s', async (boostId, did) => {
17
+ const card = {
18
+ invoke: { verifyCredential: vi.fn().mockResolvedValue(result()) },
19
+ } as unknown as Parameters<typeof getVerifyBoostPlugin>[0];
20
+ const registryUrl = `data:application/json,${encodeURIComponent(
21
+ JSON.stringify([{ id: 'Test Network', did, url: 'https://registry.example' }])
22
+ )}`;
23
+ const plugin = await getVerifyBoostPlugin(card, registryUrl);
24
+ const verified = await plugin.methods.verifyCredential(card, {
25
+ issuer: 'did:key:z6MkSigningAuthority',
26
+ boostId,
27
+ type: ['BoostCredential'],
28
+ } as unknown as VC);
29
+ expect(verified.checks).toContain('Boost is Authentic. Verified by Test Network.');
30
+ expect(verified.errors).toEqual([]);
31
+ expect(verified.warnings).toEqual([]);
32
+ });
33
+
34
+ it('does not infer authenticity for a pre-signed Boost without a signed association', async () => {
35
+ const card = {
36
+ invoke: { verifyCredential: vi.fn().mockResolvedValue(result()) },
37
+ } as unknown as Parameters<typeof getVerifyBoostPlugin>[0];
38
+ const plugin = await getVerifyBoostPlugin(card);
39
+ const verified = await plugin.methods.verifyCredential(card, {
40
+ issuer: trustedIssuer,
41
+ type: ['BoostCredential'],
42
+ } as unknown as VC);
43
+ expect(verified.errors).toEqual([]);
44
+ expect(verified.checks).toContain('proof');
45
+ expect(verified.checks.some(check => check.includes('Boost is Authentic'))).toBe(false);
46
+ expect(verified.warnings).toEqual([
47
+ 'Boost Authenticity could not be verified: Boost ID metadata is missing.',
48
+ ]);
49
+ });
50
+
51
+ it.each([
52
+ 'did:key:z6MkSigningAuthority',
53
+ 'did:web:network.learncard.com:users:issuer',
54
+ { id: 'did:web:network.learncard.com:users:issuer' },
55
+ ])('verifies a direct SA credential using its signed Boost network (%j)', async issuer => {
56
+ const verifyCredential = vi.fn().mockResolvedValue(result());
57
+ const card = { invoke: { verifyCredential } } as unknown as Parameters<
58
+ typeof getVerifyBoostPlugin
59
+ >[0];
60
+ const plugin = await getVerifyBoostPlugin(card);
61
+ const vc = { issuer, boostId, type: ['BoostCredential'] } as unknown as VC;
62
+ const verified = await plugin.methods.verifyCredential(card, vc);
63
+ expect(verifyCredential).toHaveBeenCalledTimes(1);
64
+ expect(verified.errors).toEqual([]);
65
+ expect(verified.checks).toContain('Boost is Authentic. Verified by LearnCard Network.');
66
+ });
67
+
68
+ it.each([
69
+ 'lc:network:untrusted.example/boost:test',
70
+ 'lc:network:network.learncard.com.evil.example/boost:test',
71
+ 'lc:network:network.learncard.com@evil.example/boost:test',
72
+ 'lc:network:network.learncard.com/credential:test',
73
+ 'lc:network:network.learncard.com/boost:',
74
+ 'lc:network:network.learncard.com/boost:test?network=trusted',
75
+ 'not-a-boost-uri',
76
+ ])(
77
+ 'does not mark a direct credential with an untrusted or invalid Boost URI authentic (%s)',
78
+ async boostId => {
79
+ const card = {
80
+ invoke: { verifyCredential: vi.fn().mockResolvedValue(result()) },
81
+ } as unknown as Parameters<typeof getVerifyBoostPlugin>[0];
82
+ const plugin = await getVerifyBoostPlugin(card);
83
+ const verified = await plugin.methods.verifyCredential(card, {
84
+ issuer: trustedIssuer,
85
+ boostId,
86
+ type: ['BoostCredential'],
87
+ } as unknown as VC);
88
+ expect(verified.checks.some(check => check.includes('Boost is Authentic'))).toBe(false);
89
+ expect(verified.warnings).toEqual([
90
+ `Boost Authenticity could not be verified. Boost ID does not identify a trusted network: ${boostId}`,
91
+ ]);
92
+ }
93
+ );
94
+
95
+ it.each(['Invalid signature', 'Credential revoked'])(
96
+ 'explains why a direct credential with %s cannot establish Boost authenticity',
97
+ async error => {
98
+ const card = {
99
+ invoke: { verifyCredential: vi.fn().mockResolvedValue(result([error])) },
100
+ } as unknown as Parameters<typeof getVerifyBoostPlugin>[0];
101
+ const plugin = await getVerifyBoostPlugin(card);
102
+ const verified = await plugin.methods.verifyCredential(card, {
103
+ issuer: trustedIssuer,
104
+ boostId,
105
+ type: ['BoostCredential'],
106
+ } as unknown as VC);
107
+ expect(verified.errors).toEqual([error]);
108
+ expect(verified.warnings).toContain(
109
+ 'Boost Authenticity could not be verified: Credential verification failed.'
110
+ );
111
+ expect(verified.checks.some(check => check.includes('Boost is Authentic'))).toBe(false);
112
+ }
113
+ );
114
+
115
+ it('preserves legacy inner verification failures even when both boost IDs are missing', async () => {
116
+ const verifyCredential = vi
117
+ .fn()
118
+ .mockResolvedValueOnce(result())
119
+ .mockResolvedValueOnce(result(['Credential revoked']));
120
+ const card = { invoke: { verifyCredential } } as unknown as Parameters<
121
+ typeof getVerifyBoostPlugin
122
+ >[0];
123
+ const plugin = await getVerifyBoostPlugin(card);
124
+ const verified = await plugin.methods.verifyCredential(card, {
125
+ issuer: trustedIssuer,
126
+ type: ['CertifiedBoostCredential'],
127
+ boostCredential: { issuer: 'did:example:issuer', type: ['BoostCredential'] },
128
+ } as unknown as VC);
129
+ expect(verifyCredential).toHaveBeenCalledTimes(2);
130
+ expect(verified.errors).toContain('Credential revoked');
131
+ expect(verified.checks).not.toContain('status');
132
+ });
133
+
134
+ it('continues to verify matching legacy wrappers', async () => {
135
+ const verifyCredential = vi.fn().mockImplementation(async () => result());
136
+ const card = { invoke: { verifyCredential } } as unknown as Parameters<
137
+ typeof getVerifyBoostPlugin
138
+ >[0];
139
+ const plugin = await getVerifyBoostPlugin(card);
140
+ const verified = await plugin.methods.verifyCredential(card, {
141
+ issuer: trustedIssuer,
142
+ boostId,
143
+ type: ['CertifiedBoostCredential'],
144
+ boostCredential: { issuer: 'did:example:issuer', boostId, type: ['BoostCredential'] },
145
+ } as unknown as VC);
146
+ expect(verifyCredential).toHaveBeenCalledTimes(2);
147
+ expect(verified.errors).toEqual([]);
148
+ expect(verified.checks).toContain('Boost is Authentic. Verified by LearnCard Network.');
149
+ });
150
+
151
+ it('identifies the issuer when a legacy wrapper is outside the trust network', async () => {
152
+ const card = {
153
+ invoke: { verifyCredential: vi.fn().mockImplementation(async () => result()) },
154
+ } as unknown as Parameters<typeof getVerifyBoostPlugin>[0];
155
+ const plugin = await getVerifyBoostPlugin(card);
156
+ const verified = await plugin.methods.verifyCredential(card, {
157
+ issuer: 'did:example:untrusted',
158
+ boostId,
159
+ type: ['CertifiedBoostCredential'],
160
+ boostCredential: { issuer: 'did:example:issuer', boostId, type: ['BoostCredential'] },
161
+ } as unknown as VC);
162
+ expect(verified.checks.some(check => check.includes('Boost is Authentic'))).toBe(false);
163
+ expect(verified.warnings).toEqual([
164
+ 'Boost Authenticity could not be verified. Issuer is outside of trust network: did:example:untrusted',
165
+ ]);
166
+ });
167
+ });