@learncard/helpers 1.4.1 → 1.5.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/dist/credential-refresh.d.ts +58 -0
- package/dist/credential-refresh.d.ts.map +1 -0
- package/dist/helpers.cjs.development.cjs +3836 -3526
- package/dist/helpers.cjs.development.cjs.map +4 -4
- package/dist/helpers.cjs.production.min.cjs +11 -11
- package/dist/helpers.cjs.production.min.cjs.map +4 -4
- package/dist/helpers.esm.js +282 -6
- package/dist/helpers.esm.js.map +4 -4
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/credential-refresh.ts +128 -0
- package/src/index.ts +1 -0
package/dist/index.d.cts
CHANGED
|
@@ -28,6 +28,7 @@ export * from './bitstring-status-list';
|
|
|
28
28
|
export * from './Utilities';
|
|
29
29
|
export * from './app-install';
|
|
30
30
|
export * from './credential-format';
|
|
31
|
+
export * from './credential-refresh';
|
|
31
32
|
export * from './did';
|
|
32
33
|
export * from './environment';
|
|
33
34
|
export * from './credential-format';
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from './bitstring-status-list';
|
|
|
28
28
|
export * from './Utilities';
|
|
29
29
|
export * from './app-install';
|
|
30
30
|
export * from './credential-format';
|
|
31
|
+
export * from './credential-refresh';
|
|
31
32
|
export * from './did';
|
|
32
33
|
export * from './environment';
|
|
33
34
|
export * from './credential-format';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAgB,UAAU,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAI,KAAK,MAAM,YAA6B,CAAC;AAE/D,8DAA8D;AAC9D,eAAO,MAAM,WAAW,GAAI,MAAM,OAAO,KAAG,IAAI,IAAI,GAEnD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,eA8B/B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,YAAY,OAAO,KAAG,OAQjD,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,GAAI,KAAK,EAAE,GAAG,UAAU,QAMzD,CAAC;AAGF,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,yBAAyB,CAAC;AAGxC,cAAc,aAAa,CAAC;AAG5B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAG9B,cAAc,qBAAqB,CAAC;AAEpC;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,OAK1C,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAgB,UAAU,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,KAAK,GAAI,KAAK,MAAM,YAA6B,CAAC;AAE/D,8DAA8D;AAC9D,eAAO,MAAM,WAAW,GAAI,MAAM,OAAO,KAAG,IAAI,IAAI,GAEnD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,eA8B/B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,YAAY,OAAO,KAAG,OAQjD,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,qBAAqB,GAAI,KAAK,EAAE,GAAG,UAAU,QAMzD,CAAC;AAGF,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,yBAAyB,CAAC;AAGxC,cAAc,aAAa,CAAC;AAG5B,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,OAAO,CAAC;AACtB,cAAc,eAAe,CAAC;AAG9B,cAAc,qBAAqB,CAAC;AAEpC;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,OAK1C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/helpers",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Shared helpers for LearnCard packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@noble/hashes": "^1.8.0",
|
|
56
56
|
"@sd-jwt/decode": "^0.19.0",
|
|
57
|
-
"@learncard/types": "5.
|
|
57
|
+
"@learncard/types": "5.19.0",
|
|
58
58
|
"@trpc/server": "11.8.0",
|
|
59
59
|
"immer": "^10.0.3",
|
|
60
60
|
"use-immer": "^0.9.0",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SupportedCredentialRefreshServiceValidator,
|
|
3
|
+
type SupportedCredentialRefreshService,
|
|
4
|
+
} from '@learncard/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Canonical helpers for credential refresh (LC-2117, LC-2135, LC-2136).
|
|
8
|
+
*
|
|
9
|
+
* These helpers are storage-independent: they select a supported refresh service,
|
|
10
|
+
* normalize issuer/effective-time identity, and provide deterministic canonicalization
|
|
11
|
+
* plus a proof-insensitive content comparison used by both the holder SDK and the
|
|
12
|
+
* brain-service publication pipeline.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type RefreshableCredential = Record<string, unknown> & {
|
|
16
|
+
refreshService?: unknown;
|
|
17
|
+
issuer?: string | { id?: unknown } | null;
|
|
18
|
+
validFrom?: unknown;
|
|
19
|
+
issuanceDate?: unknown;
|
|
20
|
+
proof?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Selects the first supported refresh service from a credential's `refreshService`.
|
|
25
|
+
*
|
|
26
|
+
* Accepts a single service object or an array. An array is treated as ordered: the
|
|
27
|
+
* first entry whose type is supported is selected. Supported types are
|
|
28
|
+
* `1EdTechCredentialRefresh` and `LearnCardCredentialRefresh2026`.
|
|
29
|
+
*
|
|
30
|
+
* @returns the supported service, or `undefined` when none is present/supported
|
|
31
|
+
*/
|
|
32
|
+
export const getSupportedRefreshService = (
|
|
33
|
+
vc: RefreshableCredential
|
|
34
|
+
): SupportedCredentialRefreshService | undefined => {
|
|
35
|
+
const refreshService = vc?.refreshService;
|
|
36
|
+
|
|
37
|
+
if (!refreshService) return undefined;
|
|
38
|
+
|
|
39
|
+
const services = Array.isArray(refreshService) ? refreshService : [refreshService];
|
|
40
|
+
|
|
41
|
+
for (const service of services) {
|
|
42
|
+
const parsed = SupportedCredentialRefreshServiceValidator.safeParse(service);
|
|
43
|
+
|
|
44
|
+
if (parsed.success) return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalizes a credential's issuer to its identifier.
|
|
52
|
+
*
|
|
53
|
+
* Handles both the string form (`issuer: 'did:example:x'`) and the object form
|
|
54
|
+
* (`issuer: { id: 'did:example:x', ... }`).
|
|
55
|
+
*/
|
|
56
|
+
export const getCredentialIssuerId = (vc: RefreshableCredential): string | undefined => {
|
|
57
|
+
const issuer = vc?.issuer;
|
|
58
|
+
|
|
59
|
+
if (!issuer) return undefined;
|
|
60
|
+
if (typeof issuer === 'string') return issuer;
|
|
61
|
+
|
|
62
|
+
return typeof issuer.id === 'string' ? issuer.id : undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns the credential's effective timestamp in milliseconds since the epoch.
|
|
67
|
+
*
|
|
68
|
+
* Prefers VCDM 2.0 `validFrom` and falls back to VCDM 1.1 `issuanceDate`. Returns
|
|
69
|
+
* `undefined` when no parseable timestamp exists.
|
|
70
|
+
*/
|
|
71
|
+
export const getCredentialEffectiveTime = (vc: RefreshableCredential): number | undefined => {
|
|
72
|
+
const raw = vc?.validFrom ?? vc?.issuanceDate;
|
|
73
|
+
|
|
74
|
+
if (typeof raw !== 'string' || raw.length === 0) return undefined;
|
|
75
|
+
|
|
76
|
+
const parsed = Date.parse(raw);
|
|
77
|
+
|
|
78
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Deterministically canonicalizes a JSON-like value: object keys are recursively
|
|
83
|
+
* sorted, array order is preserved, and primitives pass through unchanged.
|
|
84
|
+
*/
|
|
85
|
+
export const canonicalizeCredentialContent = <T>(value: T): T => {
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value.map(entry => canonicalizeCredentialContent(entry)) as T;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (value !== null && typeof value === 'object') {
|
|
91
|
+
const source = value as Record<string, unknown>;
|
|
92
|
+
const sorted: Record<string, unknown> = {};
|
|
93
|
+
|
|
94
|
+
for (const key of Object.keys(source).sort()) {
|
|
95
|
+
sorted[key] = canonicalizeCredentialContent(source[key]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return sorted as T;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return value;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/** Serializes a value to a deterministic canonical JSON string */
|
|
105
|
+
export const canonicalizeCredentialJson = (value: unknown): string =>
|
|
106
|
+
JSON.stringify(canonicalizeCredentialContent(value));
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Proof-insensitive content comparison for refresh changed-content detection.
|
|
110
|
+
*
|
|
111
|
+
* Only the top-level `proof` property is excluded; everything else (subject claims,
|
|
112
|
+
* identifiers, timestamps, services) participates in the comparison.
|
|
113
|
+
*/
|
|
114
|
+
export const credentialContentsEqual = (
|
|
115
|
+
first: RefreshableCredential,
|
|
116
|
+
second: RefreshableCredential
|
|
117
|
+
): boolean => {
|
|
118
|
+
const stripProof = (vc: RefreshableCredential) => {
|
|
119
|
+
const { proof: _proof, ...rest } = vc ?? {};
|
|
120
|
+
|
|
121
|
+
return rest;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
canonicalizeCredentialJson(stripProof(first)) ===
|
|
126
|
+
canonicalizeCredentialJson(stripProof(second))
|
|
127
|
+
);
|
|
128
|
+
};
|
package/src/index.ts
CHANGED