@learncard/helpers 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -41,4 +41,5 @@ export * from './credential-format';
41
41
  * @returns true if the DID is an app did:web, false otherwise
42
42
  */
43
43
  export declare const isAppDidWeb: (did?: string) => boolean;
44
+ export * from './share-content-canonical';
44
45
  //# sourceMappingURL=index.d.ts.map
package/dist/index.d.ts CHANGED
@@ -41,4 +41,5 @@ export * from './credential-format';
41
41
  * @returns true if the DID is an app did:web, false otherwise
42
42
  */
43
43
  export declare const isAppDidWeb: (did?: string) => boolean;
44
+ export * from './share-content-canonical';
44
45
  //# sourceMappingURL=index.d.ts.map
@@ -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,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"}
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;AAEF,cAAc,2BAA2B,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** C1 request serialization shared by the signing client and verifying service. */
2
+ export declare const DEFAULT_MAX_CANONICAL_BYTES: number;
3
+ export declare const DEFAULT_MAX_CANONICAL_DEPTH = 32;
4
+ export declare class ShareContentCanonicalizationError extends Error {
5
+ readonly code = "UNSUPPORTED_REQUEST_VALUE";
6
+ constructor(message: string);
7
+ }
8
+ /** Deterministic UTF-8 canonical serialization of a request body. */
9
+ export declare const canonicalizeShareContentRequestBody: (body: unknown, options?: {
10
+ maxBytes?: number;
11
+ maxDepth?: number;
12
+ }) => string;
13
+ //# sourceMappingURL=share-content-canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"share-content-canonical.d.ts","sourceRoot":"","sources":["../src/share-content-canonical.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,eAAO,MAAM,2BAA2B,QAAkB,CAAC;AAC3D,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAI9C,qBAAa,iCAAkC,SAAQ,KAAK;IACxD,QAAQ,CAAC,IAAI,+BAA+B;gBAEhC,OAAO,EAAE,MAAM;CAI9B;AAoGD,qEAAqE;AACrE,eAAO,MAAM,mCAAmC,GAC5C,MAAM,OAAO,EACb,UAAS;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,KACvD,MAUF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/helpers",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
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.21.0",
57
+ "@learncard/types": "5.22.0",
58
58
  "@trpc/server": "11.8.0",
59
59
  "immer": "^10.0.3",
60
60
  "use-immer": "^0.9.0",
package/src/index.ts CHANGED
@@ -106,3 +106,5 @@ export const isAppDidWeb = (did?: string): boolean => {
106
106
  const LCN_APP_DID_WEB_REGEX = /^did:web:.*:app:([^:]+)$/;
107
107
  return LCN_APP_DID_WEB_REGEX.test(did);
108
108
  };
109
+
110
+ export * from './share-content-canonical';
@@ -0,0 +1,128 @@
1
+ /** C1 request serialization shared by the signing client and verifying service. */
2
+ export const DEFAULT_MAX_CANONICAL_BYTES = 2 * 1024 * 1024;
3
+ export const DEFAULT_MAX_CANONICAL_DEPTH = 32;
4
+
5
+ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
6
+
7
+ export class ShareContentCanonicalizationError extends Error {
8
+ readonly code = 'UNSUPPORTED_REQUEST_VALUE';
9
+
10
+ constructor(message: string) {
11
+ super(message);
12
+ this.name = 'ShareContentCanonicalizationError';
13
+ }
14
+ }
15
+
16
+ const serializeCanonical = (
17
+ value: unknown,
18
+ seen: Set<object>,
19
+ depth: number,
20
+ maxDepth: number
21
+ ): string => {
22
+ if (value === null) return 'null';
23
+
24
+ switch (typeof value) {
25
+ case 'boolean':
26
+ return value ? 'true' : 'false';
27
+
28
+ case 'string': {
29
+ if (LONE_SURROGATE_RE.test(value)) {
30
+ throw new ShareContentCanonicalizationError(
31
+ 'string contains an unpaired surrogate'
32
+ );
33
+ }
34
+
35
+ return JSON.stringify(value);
36
+ }
37
+
38
+ case 'number': {
39
+ if (!Number.isSafeInteger(value) || Object.is(value, -0)) {
40
+ throw new ShareContentCanonicalizationError(
41
+ 'numbers must be safe integers without a negative-zero sign'
42
+ );
43
+ }
44
+
45
+ return String(value);
46
+ }
47
+
48
+ case 'object': {
49
+ if (depth >= maxDepth) {
50
+ throw new ShareContentCanonicalizationError(
51
+ 'value exceeds the maximum nesting depth'
52
+ );
53
+ }
54
+
55
+ if (seen.has(value)) {
56
+ throw new ShareContentCanonicalizationError('cyclic value is not supported');
57
+ }
58
+
59
+ seen.add(value);
60
+
61
+ try {
62
+ if (Array.isArray(value)) {
63
+ const items: string[] = [];
64
+
65
+ for (let index = 0; index < value.length; index += 1) {
66
+ if (!(index in value)) {
67
+ throw new ShareContentCanonicalizationError(
68
+ 'sparse arrays are not supported'
69
+ );
70
+ }
71
+
72
+ items.push(serializeCanonical(value[index], seen, depth + 1, maxDepth));
73
+ }
74
+
75
+ return `[${items.join(',')}]`;
76
+ }
77
+
78
+ const prototype = Object.getPrototypeOf(value);
79
+
80
+ if (prototype !== Object.prototype && prototype !== null) {
81
+ throw new ShareContentCanonicalizationError('only plain objects are supported');
82
+ }
83
+
84
+ const record = value as Record<string, unknown>;
85
+
86
+ const entries = Object.keys(record)
87
+ .sort()
88
+ .map(key => {
89
+ if (LONE_SURROGATE_RE.test(key)) {
90
+ throw new ShareContentCanonicalizationError(
91
+ 'object key contains an unpaired surrogate'
92
+ );
93
+ }
94
+
95
+ return `${JSON.stringify(key)}:${serializeCanonical(
96
+ record[key],
97
+ seen,
98
+ depth + 1,
99
+ maxDepth
100
+ )}`;
101
+ });
102
+
103
+ return `{${entries.join(',')}}`;
104
+ } finally {
105
+ seen.delete(value);
106
+ }
107
+ }
108
+
109
+ default:
110
+ throw new ShareContentCanonicalizationError(`unsupported value type: ${typeof value}`);
111
+ }
112
+ };
113
+
114
+ /** Deterministic UTF-8 canonical serialization of a request body. */
115
+ export const canonicalizeShareContentRequestBody = (
116
+ body: unknown,
117
+ options: { maxBytes?: number; maxDepth?: number } = {}
118
+ ): string => {
119
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_CANONICAL_BYTES;
120
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_CANONICAL_DEPTH;
121
+ const serialized = serializeCanonical(body, new Set(), 0, maxDepth);
122
+
123
+ if (new TextEncoder().encode(serialized).byteLength > maxBytes) {
124
+ throw new ShareContentCanonicalizationError('canonical request exceeds the maximum size');
125
+ }
126
+
127
+ return serialized;
128
+ };