@fedify/vocab-runtime 2.4.0-pr.934.40 → 2.4.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.
Files changed (140) hide show
  1. package/deno.json +4 -2
  2. package/dist/{tests/docloader-Xi61Le8w.mjs → contexts-BK1CqBR5.cjs} +164 -269
  3. package/dist/{tests/docloader-BFedvemb.cjs → contexts-DPuJ4UYL.js} +160 -288
  4. package/dist/{docloader-DnUMWHaJ.d.cts → docloader-CYwqh5Df.d.cts} +68 -2
  5. package/dist/{docloader-xRGn1azD.d.ts → docloader-CYwqh5Df.d.ts} +68 -2
  6. package/dist/internal/jsonld-cache.cjs +70 -1
  7. package/dist/internal/jsonld-cache.d.cts +35 -2
  8. package/dist/internal/jsonld-cache.d.ts +35 -2
  9. package/dist/internal/jsonld-cache.js +67 -2
  10. package/dist/internal/portable-dereference.cjs +721 -0
  11. package/dist/internal/portable-dereference.d.cts +349 -0
  12. package/dist/internal/portable-dereference.d.ts +349 -0
  13. package/dist/internal/portable-dereference.js +702 -0
  14. package/dist/internal/signed-representation.cjs +378 -0
  15. package/dist/internal/signed-representation.d.cts +144 -0
  16. package/dist/internal/signed-representation.d.ts +144 -0
  17. package/dist/internal/signed-representation.js +371 -0
  18. package/dist/jsonld.cjs +2 -2
  19. package/dist/mod.cjs +715 -4523
  20. package/dist/mod.d.cts +406 -6
  21. package/dist/mod.d.ts +406 -7
  22. package/dist/mod.js +694 -4518
  23. package/dist/portable-DqtfLy_1.d.cts +160 -0
  24. package/dist/portable-DtWsu2yU.d.ts +160 -0
  25. package/dist/tests/body-CYiu9CO-.mjs +130 -0
  26. package/dist/tests/body-CkEwROsn.cjs +153 -0
  27. package/dist/tests/body.test.cjs +89 -0
  28. package/dist/tests/body.test.d.cts +1 -0
  29. package/dist/tests/body.test.d.mts +1 -0
  30. package/dist/tests/body.test.mjs +90 -0
  31. package/dist/tests/contexts-CIKsin4e.mjs +4512 -0
  32. package/dist/tests/contexts-DizzBjz4.cjs +4523 -0
  33. package/dist/tests/decimal.test.cjs +8 -7
  34. package/dist/tests/decimal.test.mjs +8 -6
  35. package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
  36. package/dist/tests/digest-COC7xDiQ.mjs +141 -0
  37. package/dist/tests/digest.test.cjs +102 -0
  38. package/dist/tests/digest.test.d.cts +1 -0
  39. package/dist/tests/digest.test.d.mts +1 -0
  40. package/dist/tests/digest.test.mjs +103 -0
  41. package/dist/tests/docloader-C3YFl6y4.cjs +396 -0
  42. package/dist/tests/docloader-CvmS4sVq.mjs +373 -0
  43. package/dist/tests/docloader.test.cjs +692 -39
  44. package/dist/tests/docloader.test.mjs +686 -34
  45. package/dist/tests/internal/multicodec.test.cjs +2 -3
  46. package/dist/tests/internal/multicodec.test.mjs +2 -2
  47. package/dist/tests/internal/portable-dereference.test.cjs +172 -0
  48. package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
  49. package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
  50. package/dist/tests/internal/portable-dereference.test.mjs +173 -0
  51. package/dist/tests/jsonld-cache-BPQmOZWD.mjs +342 -0
  52. package/dist/tests/jsonld-cache-C07AyNOY.cjs +397 -0
  53. package/dist/tests/jsonld-cache.test.cjs +101 -298
  54. package/dist/tests/jsonld-cache.test.mjs +94 -289
  55. package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
  56. package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
  57. package/dist/tests/key.test.cjs +6 -5
  58. package/dist/tests/key.test.mjs +6 -4
  59. package/dist/tests/langstr.test.cjs +4 -4
  60. package/dist/tests/langstr.test.mjs +2 -2
  61. package/dist/tests/link.test.cjs +2 -3
  62. package/dist/tests/link.test.mjs +2 -2
  63. package/dist/tests/multibase/multibase.test.cjs +8 -9
  64. package/dist/tests/multibase/multibase.test.mjs +6 -6
  65. package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
  66. package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
  67. package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
  68. package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
  69. package/dist/tests/portable-media-BLKllU56.cjs +153 -0
  70. package/dist/tests/portable-media-DRSGBORB.mjs +148 -0
  71. package/dist/tests/portable-media.test.cjs +222 -0
  72. package/dist/tests/portable-media.test.d.cts +1 -0
  73. package/dist/tests/portable-media.test.d.mts +1 -0
  74. package/dist/tests/portable-media.test.mjs +223 -0
  75. package/dist/tests/portable-workers.test.cjs +36 -0
  76. package/dist/tests/portable-workers.test.d.cts +2 -0
  77. package/dist/tests/portable-workers.test.d.mts +2 -0
  78. package/dist/tests/portable-workers.test.mjs +35 -0
  79. package/dist/tests/{request-cCPgOxYG.cjs → request-8vPWtV1-.cjs} +10 -4
  80. package/dist/tests/{request-BOS-hNaf.mjs → request-DRaOaTqD.mjs} +8 -2
  81. package/dist/tests/request.test.cjs +7 -4
  82. package/dist/tests/request.test.mjs +5 -2
  83. package/dist/tests/signed-representation.test.cjs +588 -0
  84. package/dist/tests/signed-representation.test.d.cts +1 -0
  85. package/dist/tests/signed-representation.test.d.mts +1 -0
  86. package/dist/tests/signed-representation.test.mjs +589 -0
  87. package/dist/tests/temporal.test.cjs +1 -2
  88. package/dist/tests/temporal.test.mjs +1 -1
  89. package/dist/tests/url-BNakuZ8k.cjs +998 -0
  90. package/dist/tests/url-DMxmp7ZG.mjs +859 -0
  91. package/dist/tests/url.test.cjs +503 -3
  92. package/dist/tests/url.test.mjs +503 -2
  93. package/dist/url-DrGTR8yv.cjs +993 -0
  94. package/dist/url-Dzyp-NsC.js +860 -0
  95. package/package.json +27 -4
  96. package/scripts/test-bun.mjs +17 -0
  97. package/src/body.test.ts +125 -0
  98. package/src/body.ts +152 -0
  99. package/src/contexts/cid-v1.json +114 -0
  100. package/src/contexts/fep-22cd.json +21 -0
  101. package/src/contexts/fep-ef61.json +10 -0
  102. package/src/contexts/miscellany.json +17 -0
  103. package/src/contexts/security-data-integrity-v1.json +0 -4
  104. package/src/contexts.ts +40 -0
  105. package/src/digest.test.ts +220 -0
  106. package/src/digest.ts +229 -0
  107. package/src/docloader.test.ts +918 -27
  108. package/src/docloader.ts +322 -105
  109. package/src/internal/jsonld-cache.ts +97 -1
  110. package/src/internal/portable-dereference.test.ts +254 -0
  111. package/src/internal/portable-dereference.ts +1000 -0
  112. package/src/internal/signed-representation.ts +565 -0
  113. package/src/jsonld-cache.test.ts +89 -0
  114. package/src/key.test.ts +9 -0
  115. package/src/key.ts +11 -1
  116. package/src/mod.ts +30 -0
  117. package/src/multibase/multibase.test.ts +5 -5
  118. package/src/portable-media.test.ts +293 -0
  119. package/src/portable-media.ts +264 -0
  120. package/src/portable-workers.test.ts +81 -0
  121. package/src/portable.ts +180 -0
  122. package/src/preprocessor.ts +7 -0
  123. package/src/request.test.ts +10 -1
  124. package/src/request.ts +9 -1
  125. package/src/signed-representation.test.ts +338 -0
  126. package/src/url.test.ts +844 -1
  127. package/src/url.ts +717 -17
  128. package/tsdown.config.ts +2 -0
  129. package/dist/tests/url-BvjYQdxL.cjs +0 -456
  130. package/dist/tests/url-a2D8NAgh.mjs +0 -378
  131. package/dist/url-Ck3dGEwH.cjs +0 -457
  132. package/dist/url-m1YxGNZ0.js +0 -379
  133. /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
  134. /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-C4Fl80ae.cjs} +0 -0
  135. /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-CQ26J_L7.mjs} +0 -0
  136. /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
  137. /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
  138. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
  139. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
  140. /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
@@ -0,0 +1,589 @@
1
+ import { t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
2
+ import { test } from "node:test";
3
+ import { deepStrictEqual, ok, strictEqual, throws } from "node:assert";
4
+ //#region src/internal/signed-representation.ts
5
+ const RETAINED_SIGNED_REPRESENTATION = Symbol.for("@fedify/vocab-runtime.signedRepresentation.v1");
6
+ const SIGNED_VALUE_SCOPE = Symbol.for("@fedify/vocab-runtime.signedValueScope.v1");
7
+ /**
8
+ * The IRI namespace of the placeholder node references that stand in for a
9
+ * retained signed representation until the owning `toJsonLd()` frame puts the
10
+ * representation back.
11
+ */
12
+ const MARKER_NAMESPACE = "urn:x-fedify-signed-value:";
13
+ /** Bounds applied when validating a retained signed representation. */
14
+ const MAX_VALIDATION_DEPTH = 64;
15
+ const MAX_VALIDATION_NODES = 1e5;
16
+ /**
17
+ * The depth to which placeholders are put back. It has to exceed
18
+ * {@link MAX_VALIDATION_DEPTH}, because a restored document is itself nested
19
+ * inside the document that embeds it. Running out of depth is an error
20
+ * rather than a stopping point: a placeholder left in an unvisited subtree
21
+ * would reach the wire in place of the secured child.
22
+ */
23
+ const MAX_RESTORATION_DEPTH = 256;
24
+ /**
25
+ * JSON-LD keywords that may appear at the top level of a context definition
26
+ * without endangering placeholder recovery. `@vocab` and `@base` are checked
27
+ * separately because they can rewrite the placeholder IRI.
28
+ */
29
+ const HARMLESS_CONTEXT_KEYWORDS = /* @__PURE__ */ new Set([
30
+ "@protected",
31
+ "@version",
32
+ "@propagate"
33
+ ]);
34
+ /**
35
+ * Keyword aliases that are safe to leave in place because the placeholder
36
+ * recovery walker understands them.
37
+ */
38
+ const ALLOWED_KEYWORD_ALIASES = /* @__PURE__ */ new Map([["id", "@id"], ["type", "@type"]]);
39
+ function isJsonMap(value) {
40
+ if (value == null || typeof value !== "object" || Array.isArray(value)) return false;
41
+ const prototype = Object.getPrototypeOf(value);
42
+ return prototype === Object.prototype || prototype === null;
43
+ }
44
+ /**
45
+ * Checks that a value is a plain JSON tree that is safe to embed verbatim in
46
+ * a serialized document: plain object and array prototypes only, string keys
47
+ * only, own enumerable data properties only, finite numbers, and no
48
+ * `undefined`.
49
+ *
50
+ * @internal Technically exported for generated vocabulary classes, but not
51
+ * part of the public API contract. This is not considered public API for
52
+ * Semantic Versioning decisions.
53
+ */
54
+ function isPlainJsonTree(value) {
55
+ let nodes = 0;
56
+ return check(value, 0);
57
+ function check(current, depth) {
58
+ if (depth > MAX_VALIDATION_DEPTH) return false;
59
+ if (++nodes > MAX_VALIDATION_NODES) return false;
60
+ if (current === null) return true;
61
+ switch (typeof current) {
62
+ case "string":
63
+ case "boolean": return true;
64
+ case "number": return Number.isFinite(current);
65
+ case "object": break;
66
+ default: return false;
67
+ }
68
+ if (Array.isArray(current)) {
69
+ if (Object.getPrototypeOf(current) !== Array.prototype) return false;
70
+ if (Object.getOwnPropertySymbols(current).length > 0) return false;
71
+ for (let i = 0; i < current.length; i++) {
72
+ const descriptor = Object.getOwnPropertyDescriptor(current, i);
73
+ if (descriptor == null || !("value" in descriptor)) return false;
74
+ if (!check(descriptor.value, depth + 1)) return false;
75
+ }
76
+ return true;
77
+ }
78
+ if (!isJsonMap(current)) return false;
79
+ if (Object.getOwnPropertySymbols(current).length > 0) return false;
80
+ for (const key of Object.getOwnPropertyNames(current)) {
81
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
82
+ if (descriptor == null || !("value" in descriptor)) return false;
83
+ if (!descriptor.enumerable) return false;
84
+ if (!check(descriptor.value, depth + 1)) return false;
85
+ }
86
+ return true;
87
+ }
88
+ }
89
+ function deepFreeze(value) {
90
+ if (value == null || typeof value !== "object") return value;
91
+ if (Object.isFrozen(value)) return value;
92
+ Object.freeze(value);
93
+ for (const child of Object.values(value)) deepFreeze(child);
94
+ return value;
95
+ }
96
+ /**
97
+ * Attaches the secured JSON document that a signer captured for `target`.
98
+ *
99
+ * The document is the exact JSON value that the object's own Object Integrity
100
+ * Proof covers. Nested serialization embeds it verbatim instead of
101
+ * reconstructing the object under the parent's JSON-LD context. The value is
102
+ * deep-frozen, so it is independent of anything done to `target` afterwards.
103
+ *
104
+ * `clone()` never carries the attachment, because a clone is a fresh instance.
105
+ *
106
+ * @internal Technically exported for `@fedify/fedify` and generated
107
+ * vocabulary classes, but not part of the public API contract. This is not
108
+ * considered public API for Semantic Versioning decisions.
109
+ */
110
+ function retainSignedRepresentation(target, securedDocument) {
111
+ if (!isJsonMap(securedDocument) || !isPlainJsonTree(securedDocument)) throw new TypeError(`The secured document must be a plain JSON map within the validation bounds (at most ${MAX_VALIDATION_DEPTH} levels deep and ${MAX_VALIDATION_NODES} nodes).`);
112
+ Object.defineProperty(target, RETAINED_SIGNED_REPRESENTATION, {
113
+ value: deepFreeze(securedDocument),
114
+ enumerable: false,
115
+ writable: false,
116
+ configurable: true
117
+ });
118
+ }
119
+ /**
120
+ * Returns the secured JSON document retained for `target`, if any.
121
+ *
122
+ * @internal Technically exported for generated vocabulary classes, but not
123
+ * part of the public API contract. This is not considered public API for
124
+ * Semantic Versioning decisions.
125
+ */
126
+ function getRetainedSignedRepresentation(target) {
127
+ if (target == null || typeof target !== "object") return void 0;
128
+ const retained = target[RETAINED_SIGNED_REPRESENTATION];
129
+ return isJsonMap(retained) ? retained : void 0;
130
+ }
131
+ function resolveContextEntry(entry) {
132
+ if (typeof entry !== "string") return entry;
133
+ if (!Object.hasOwn(preloadedContexts, entry)) return void 0;
134
+ const document = preloadedContexts[entry];
135
+ if (!isJsonMap(document)) return void 0;
136
+ return document["@context"];
137
+ }
138
+ /**
139
+ * Checks whether a JSON-LD context leaves placeholder node references
140
+ * recoverable after compaction.
141
+ *
142
+ * Placeholder recovery replaces a bare IRI string, `{"@id": …}`, or
143
+ * `{"id": …}`. A context that aliases `@id` under a different term, declares
144
+ * `@nest`, uses an `@id` or `@type` container, rebases or re-vocabularies the
145
+ * placeholder namespace, or defines a prefix that shortens the placeholder
146
+ * IRI could hide the placeholder from that walker or make the walker rewrite
147
+ * the wrong node. A context that cannot be resolved offline cannot be vetted
148
+ * at all. Any of those makes the context unsafe, and retention is then not
149
+ * used.
150
+ *
151
+ * @internal Technically exported for generated vocabulary classes, but not
152
+ * part of the public API contract. This is not considered public API for
153
+ * Semantic Versioning decisions.
154
+ */
155
+ function isMarkerSafeContext(context) {
156
+ return check(context, 0);
157
+ function check(current, depth) {
158
+ if (depth > 8) return false;
159
+ if (Array.isArray(current)) return current.every((entry) => check(entry, depth + 1));
160
+ if (typeof current === "string") {
161
+ const resolved = resolveContextEntry(current);
162
+ if (resolved === void 0) return false;
163
+ return check(resolved, depth + 1);
164
+ }
165
+ if (current == null) return true;
166
+ if (!isJsonMap(current)) return false;
167
+ for (const [term, definition] of Object.entries(current)) {
168
+ if (term === "@vocab" || term === "@base") {
169
+ if (typeof definition !== "string") return false;
170
+ if (sharesMarkerNamespace(definition)) return false;
171
+ continue;
172
+ }
173
+ if (term.startsWith("@")) {
174
+ if (!HARMLESS_CONTEXT_KEYWORDS.has(term)) return false;
175
+ continue;
176
+ }
177
+ if (MARKER_NAMESPACE.startsWith(`${term}:`)) return false;
178
+ if (!checkTerm(term, definition, depth)) return false;
179
+ }
180
+ return true;
181
+ }
182
+ function checkTerm(term, definition, depth) {
183
+ if (definition == null) return true;
184
+ if (typeof definition === "string") return checkTermTarget(term, definition);
185
+ if (!isJsonMap(definition)) return false;
186
+ const target = definition["@id"];
187
+ if (target != null) {
188
+ if (typeof target !== "string") return false;
189
+ if (!checkTermTarget(term, target)) return false;
190
+ }
191
+ if ("@nest" in definition) return false;
192
+ if ("@reverse" in definition) return false;
193
+ const container = definition["@container"];
194
+ const containers = Array.isArray(container) ? container : container == null ? [] : [container];
195
+ for (const entry of containers) if (entry === "@id" || entry === "@type") return false;
196
+ if ("@context" in definition) {
197
+ if (!check(definition["@context"], depth + 1)) return false;
198
+ }
199
+ return true;
200
+ }
201
+ function checkTermTarget(term, target) {
202
+ if (target.startsWith("@")) return ALLOWED_KEYWORD_ALIASES.get(term) === target;
203
+ if (!target.includes(":")) return false;
204
+ return !sharesMarkerNamespace(target);
205
+ }
206
+ }
207
+ /**
208
+ * Reports whether an IRI prefix could shorten, relativize or otherwise hide a
209
+ * placeholder IRI. Both directions matter: a prefix shorter than the
210
+ * namespace covers every marker, and a longer one covers the markers whose
211
+ * random suffix happens to start with it.
212
+ */
213
+ function sharesMarkerNamespace(prefix) {
214
+ return MARKER_NAMESPACE.startsWith(prefix) || prefix.startsWith(MARKER_NAMESPACE);
215
+ }
216
+ /**
217
+ * Joins the signed-value scope of the enclosing `toJsonLd()` frame, or starts
218
+ * one when this frame is the outermost.
219
+ *
220
+ * `options` must already be the internal copy that the generated encoder
221
+ * makes, never the object a caller passed in: the scope is attached to it as
222
+ * an enumerable symbol-keyed property so that every `{...options}` spread in
223
+ * the generated code propagates it to nested and inherited frames.
224
+ *
225
+ * @internal Technically exported for generated vocabulary classes, but not
226
+ * part of the public API contract. This is not considered public API for
227
+ * Semantic Versioning decisions.
228
+ */
229
+ function enterSignedValueScope(options) {
230
+ const existing = options[SIGNED_VALUE_SCOPE];
231
+ if (existing != null) return {
232
+ scope: existing,
233
+ owner: false
234
+ };
235
+ const scope = {
236
+ enabled: options.format !== "expand" && (options.context == null || isMarkerSafeContext(options.context)),
237
+ markers: /* @__PURE__ */ new Map(),
238
+ documents: /* @__PURE__ */ new Map()
239
+ };
240
+ Object.defineProperty(options, SIGNED_VALUE_SCOPE, {
241
+ value: scope,
242
+ enumerable: true,
243
+ writable: false,
244
+ configurable: true
245
+ });
246
+ return {
247
+ scope,
248
+ owner: true
249
+ };
250
+ }
251
+ /**
252
+ * Emits a placeholder node reference for a value that carries a retained
253
+ * signed representation, or `undefined` when it does not carry one.
254
+ *
255
+ * Both encoder paths use placeholders, never the retained document itself:
256
+ * an ancestor frame may still expand or compact the value, which would
257
+ * destroy an embedded document but leaves a node reference recoverable.
258
+ *
259
+ * @internal Technically exported for generated vocabulary classes, but not
260
+ * part of the public API contract. This is not considered public API for
261
+ * Semantic Versioning decisions.
262
+ */
263
+ function retainedSignedValueRef(value, scope) {
264
+ if (!scope.enabled) return void 0;
265
+ if (value == null || typeof value !== "object") return void 0;
266
+ const retained = getRetainedSignedRepresentation(value);
267
+ if (retained == null) return void 0;
268
+ let marker = scope.markers.get(value);
269
+ if (marker == null) {
270
+ if (!isPlainJsonTree(retained)) return void 0;
271
+ marker = `${MARKER_NAMESPACE}${crypto.randomUUID()}`;
272
+ scope.markers.set(value, marker);
273
+ scope.documents.set(marker, retained);
274
+ }
275
+ return { "@id": marker };
276
+ }
277
+ function markerOf(value, scope) {
278
+ return typeof value === "string" && scope.documents.has(value) ? value : void 0;
279
+ }
280
+ /**
281
+ * Puts every retained signed representation back in place of its placeholder
282
+ * node reference.
283
+ *
284
+ * Only the frame that owns the scope calls this, and only after compaction
285
+ * and context embedding are complete.
286
+ *
287
+ * @throws {TypeError} If a retained document was not put back anywhere, or if
288
+ * any trace of a placeholder survives. Both mean the serialized
289
+ * document does not contain the secured child it was told to embed,
290
+ * and emitting it would produce a signed child that silently fails
291
+ * verification.
292
+ *
293
+ * @internal Technically exported for generated vocabulary classes, but not
294
+ * part of the public API contract. This is not considered public API for
295
+ * Semantic Versioning decisions.
296
+ */
297
+ function resolveSignedValues(document, scope) {
298
+ if (scope.documents.size < 1) return document;
299
+ const restored = /* @__PURE__ */ new Set();
300
+ const resolved = restore(document, 0);
301
+ for (const marker of scope.documents.keys()) {
302
+ if (restored.has(marker)) continue;
303
+ throw new TypeError(`Failed to preserve a signed child representation: the placeholder ${marker} is not in the serialized document.`);
304
+ }
305
+ const trace = findMarkerTrace(resolved, scope);
306
+ if (trace != null) throw new TypeError(`Failed to preserve a signed child representation: a trace of the placeholder ${trace} survived serialization.`);
307
+ return resolved;
308
+ function restore(value, depth) {
309
+ if (depth > MAX_RESTORATION_DEPTH) throw new TypeError(`Failed to preserve a signed child representation: the serialized document is nested deeper than ${MAX_RESTORATION_DEPTH} levels, so a placeholder could be left unresolved.`);
310
+ const direct = markerOf(value, scope);
311
+ if (direct != null) return take(direct);
312
+ if (Array.isArray(value)) {
313
+ let changed = false;
314
+ const mapped = value.map((item) => {
315
+ const next = restore(item, depth + 1);
316
+ if (next !== item) changed = true;
317
+ return next;
318
+ });
319
+ return changed ? mapped : value;
320
+ }
321
+ if (!isJsonMap(value)) return value;
322
+ const keys = Object.keys(value);
323
+ if (keys.length === 1 && (keys[0] === "@id" || keys[0] === "id")) {
324
+ const marker = markerOf(value[keys[0]], scope);
325
+ if (marker != null) return take(marker);
326
+ }
327
+ let changed = false;
328
+ const mapped = Object.create(null);
329
+ for (const key of keys) {
330
+ const next = key === "@context" ? value[key] : restore(value[key], depth + 1);
331
+ if (next !== value[key]) changed = true;
332
+ mapped[key] = next;
333
+ }
334
+ if (!changed) return value;
335
+ return Object.setPrototypeOf(mapped, Object.prototype);
336
+ }
337
+ function take(marker) {
338
+ restored.add(marker);
339
+ return structuredClone(scope.documents.get(marker));
340
+ }
341
+ }
342
+ /**
343
+ * Finds any surviving trace of a placeholder IRI in a resolved document,
344
+ * matching the random suffix rather than the whole IRI so that a shortened,
345
+ * relativized or otherwise rewritten marker is caught too.
346
+ */
347
+ function findMarkerTrace(document, scope) {
348
+ const suffixes = /* @__PURE__ */ new Map();
349
+ for (const marker of scope.documents.keys()) suffixes.set(marker.slice(26), marker);
350
+ return search(document, 0);
351
+ function matched(value) {
352
+ for (const [suffix, marker] of suffixes) if (value.includes(suffix)) return marker;
353
+ }
354
+ function search(value, depth) {
355
+ if (depth > MAX_RESTORATION_DEPTH) throw new TypeError(`Failed to preserve a signed child representation: the serialized document is nested deeper than ${MAX_RESTORATION_DEPTH} levels, so a surviving placeholder could go unnoticed.`);
356
+ if (typeof value === "string") return matched(value);
357
+ if (Array.isArray(value)) {
358
+ for (const item of value) {
359
+ const found = search(item, depth + 1);
360
+ if (found != null) return found;
361
+ }
362
+ return;
363
+ }
364
+ if (value == null || typeof value !== "object") return void 0;
365
+ for (const [key, child] of Object.entries(value)) {
366
+ const found = matched(key) ?? search(child, depth + 1);
367
+ if (found != null) return found;
368
+ }
369
+ }
370
+ }
371
+ //#endregion
372
+ //#region src/signed-representation.test.ts
373
+ const securedDocument = {
374
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"],
375
+ id: "ap://did:key:z6Mkabc/objects/1",
376
+ type: "Note",
377
+ content: "Hello",
378
+ proof: {
379
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"],
380
+ type: "DataIntegrityProof",
381
+ cryptosuite: "eddsa-jcs-2022",
382
+ created: "2023-02-24T23:36:38Z",
383
+ verificationMethod: "did:key:z6Mkabc#z6Mkabc",
384
+ proofPurpose: "assertionMethod",
385
+ proofValue: "z3FXQ"
386
+ }
387
+ };
388
+ function newScope(options = {}) {
389
+ return enterSignedValueScope(options).scope;
390
+ }
391
+ function retainedSubject() {
392
+ const subject = {};
393
+ retainSignedRepresentation(subject, structuredClone(securedDocument));
394
+ return subject;
395
+ }
396
+ test("isPlainJsonTree() accepts plain JSON and rejects everything else", () => {
397
+ ok(isPlainJsonTree(securedDocument));
398
+ ok(isPlainJsonTree([
399
+ 1,
400
+ "two",
401
+ null,
402
+ { three: true }
403
+ ]));
404
+ ok(isPlainJsonTree(Object.create(null)));
405
+ ok(!isPlainJsonTree(void 0));
406
+ ok(!isPlainJsonTree(NaN));
407
+ ok(!isPlainJsonTree(Number.POSITIVE_INFINITY));
408
+ ok(!isPlainJsonTree(/* @__PURE__ */ new Date()));
409
+ ok(!isPlainJsonTree(new URL("https://example.com/")));
410
+ ok(!isPlainJsonTree({ nested: { deeper: void 0 } }));
411
+ const withSymbolKey = { a: 1 };
412
+ withSymbolKey[Symbol("b")] = 2;
413
+ ok(!isPlainJsonTree(withSymbolKey));
414
+ const withAccessor = {};
415
+ Object.defineProperty(withAccessor, "a", {
416
+ get: () => 1,
417
+ enumerable: true
418
+ });
419
+ ok(!isPlainJsonTree(withAccessor));
420
+ const withHiddenProperty = {};
421
+ Object.defineProperty(withHiddenProperty, "a", {
422
+ value: 1,
423
+ enumerable: false
424
+ });
425
+ ok(!isPlainJsonTree(withHiddenProperty));
426
+ });
427
+ test("retainSignedRepresentation() attaches a frozen, hidden snapshot", () => {
428
+ const subject = {};
429
+ retainSignedRepresentation(subject, structuredClone(securedDocument));
430
+ const retained = getRetainedSignedRepresentation(subject);
431
+ deepStrictEqual(retained, securedDocument);
432
+ ok(Object.isFrozen(retained));
433
+ ok(Object.isFrozen(retained.proof));
434
+ deepStrictEqual(Object.keys(subject), []);
435
+ strictEqual(JSON.stringify(subject), "{}");
436
+ strictEqual(getRetainedSignedRepresentation({}), void 0);
437
+ strictEqual(getRetainedSignedRepresentation(null), void 0);
438
+ strictEqual(getRetainedSignedRepresentation("string"), void 0);
439
+ });
440
+ test("retainSignedRepresentation() rejects a value that is not plain JSON", () => {
441
+ throws(() => retainSignedRepresentation({}, { created: /* @__PURE__ */ new Date() }), TypeError);
442
+ throws(() => retainSignedRepresentation({}, [1, 2]), TypeError);
443
+ });
444
+ test("isMarkerSafeContext() accepts contexts Fedify can vet offline", () => {
445
+ ok(isMarkerSafeContext(null));
446
+ ok(isMarkerSafeContext("https://www.w3.org/ns/activitystreams"));
447
+ ok(isMarkerSafeContext([
448
+ "https://www.w3.org/ns/activitystreams",
449
+ "https://w3id.org/security/data-integrity/v1",
450
+ "https://w3id.org/fep/ef61"
451
+ ]));
452
+ ok(isMarkerSafeContext(["https://www.w3.org/ns/activitystreams", { ex: "https://example.com/ns#" }]));
453
+ });
454
+ test("isMarkerSafeContext() rejects contexts that could hide a placeholder", () => {
455
+ ok(!isMarkerSafeContext("https://example.com/unknown-context"));
456
+ ok(!isMarkerSafeContext({ identifier: "@id" }));
457
+ ok(!isMarkerSafeContext({ identifier: { "@id": "@id" } }));
458
+ ok(!isMarkerSafeContext(["https://www.w3.org/ns/activitystreams", { i: "id" }]));
459
+ ok(!isMarkerSafeContext({ relative: { "@id": "ns#term" } }));
460
+ ok(!isMarkerSafeContext({ payload: "@nest" }));
461
+ ok(!isMarkerSafeContext({ object: {
462
+ "@id": "https://www.w3.org/ns/activitystreams#object",
463
+ "@nest": "payload"
464
+ } }));
465
+ ok(!isMarkerSafeContext({ object: {
466
+ "@id": "https://www.w3.org/ns/activitystreams#object",
467
+ "@container": "@id"
468
+ } }));
469
+ ok(!isMarkerSafeContext({ object: {
470
+ "@id": "https://www.w3.org/ns/activitystreams#object",
471
+ "@container": ["@set", "@type"]
472
+ } }));
473
+ ok(!isMarkerSafeContext({ urn: "urn:" }));
474
+ ok(!isMarkerSafeContext({ urn: "https://example.com/ns#" }));
475
+ ok(!isMarkerSafeContext({ urn: { "@id": "https://example.com/ns#urn" } }));
476
+ ok(!isMarkerSafeContext({ "@vocab": "urn:x-fedify-signed-value:" }));
477
+ ok(!isMarkerSafeContext({ "@base": "urn:" }));
478
+ ok(!isMarkerSafeContext({ everything: "" }));
479
+ ok(isMarkerSafeContext({ Note: {
480
+ "@id": "https://www.w3.org/ns/activitystreams#Note",
481
+ "@context": { ex: "https://example.com/ns#" }
482
+ } }));
483
+ ok(!isMarkerSafeContext({ Note: {
484
+ "@id": "https://www.w3.org/ns/activitystreams#Note",
485
+ "@context": { identifier: "@id" }
486
+ } }));
487
+ });
488
+ test("enterSignedValueScope() gives each call its own scope", () => {
489
+ const callerOptions = { format: "compact" };
490
+ const first = { ...callerOptions };
491
+ const second = { ...callerOptions };
492
+ const firstEntry = enterSignedValueScope(first);
493
+ const secondEntry = enterSignedValueScope(second);
494
+ ok(firstEntry.owner);
495
+ ok(secondEntry.owner);
496
+ ok(firstEntry.scope !== secondEntry.scope);
497
+ deepStrictEqual(Object.getOwnPropertySymbols(callerOptions), []);
498
+ const nested = enterSignedValueScope({
499
+ ...first,
500
+ format: "expand"
501
+ });
502
+ ok(!nested.owner);
503
+ strictEqual(nested.scope, firstEntry.scope);
504
+ });
505
+ test("enterSignedValueScope() disables retention where it cannot apply", () => {
506
+ ok(enterSignedValueScope({}).scope.enabled);
507
+ ok(enterSignedValueScope({ format: "compact" }).scope.enabled);
508
+ ok(!enterSignedValueScope({ format: "expand" }).scope.enabled);
509
+ ok(!enterSignedValueScope({
510
+ format: "compact",
511
+ context: { identifier: "@id" }
512
+ }).scope.enabled);
513
+ });
514
+ test("retainedSignedValueRef() mints one marker per instance", () => {
515
+ const scope = newScope();
516
+ const subject = retainedSubject();
517
+ const first = retainedSignedValueRef(subject, scope);
518
+ const second = retainedSignedValueRef(subject, scope);
519
+ ok(first != null);
520
+ deepStrictEqual(first, second);
521
+ ok(first["@id"].startsWith("urn:x-fedify-signed-value:"));
522
+ strictEqual(scope.documents.get(first["@id"]), scope.documents.get(second["@id"]));
523
+ strictEqual(retainedSignedValueRef({}, scope), void 0);
524
+ strictEqual(retainedSignedValueRef(null, scope), void 0);
525
+ strictEqual(retainedSignedValueRef(subject, newScope({ format: "expand" })), void 0);
526
+ });
527
+ test("resolveSignedValues() restores every supported reference shape", () => {
528
+ const scope = newScope();
529
+ const subject = retainedSubject();
530
+ const marker = retainedSignedValueRef(subject, scope)["@id"];
531
+ retainedSignedValueRef(subject, scope);
532
+ retainedSignedValueRef(subject, scope);
533
+ const resolved = resolveSignedValues({
534
+ "@context": ["https://www.w3.org/ns/activitystreams"],
535
+ object: marker,
536
+ attachment: [{ "@id": marker }],
537
+ tag: { id: marker }
538
+ }, scope);
539
+ deepStrictEqual(resolved.object, securedDocument);
540
+ deepStrictEqual(resolved.attachment[0], securedDocument);
541
+ deepStrictEqual(resolved.tag, securedDocument);
542
+ ok(resolved.object !== resolved.tag);
543
+ });
544
+ test("resolveSignedValues() leaves a document without placeholders alone", () => {
545
+ const scope = newScope();
546
+ const document = { id: "https://example.com/notes/1" };
547
+ strictEqual(resolveSignedValues(document, scope), document);
548
+ });
549
+ test("resolveSignedValues() does not rewrite a context definition", () => {
550
+ const scope = newScope();
551
+ const marker = retainedSignedValueRef(retainedSubject(), scope)["@id"];
552
+ throws(() => resolveSignedValues({ "@context": { marker } }, scope), TypeError);
553
+ });
554
+ test("resolveSignedValues() rejects a document that lost a placeholder", () => {
555
+ const scope = newScope();
556
+ retainedSignedValueRef(retainedSubject(), scope);
557
+ throws(() => resolveSignedValues({ id: "https://example.com/notes/1" }, scope), TypeError);
558
+ });
559
+ test("resolveSignedValues() rejects a document that kept a placeholder trace", () => {
560
+ const scope = newScope();
561
+ const marker = retainedSignedValueRef(retainedSubject(), scope)["@id"];
562
+ const uuid = marker.slice(26);
563
+ throws(() => resolveSignedValues({
564
+ object: marker,
565
+ note: `sv:${uuid}`
566
+ }, scope), TypeError);
567
+ });
568
+ test("resolveSignedValues() restores a placeholder only where it is a reference", () => {
569
+ const scope = newScope();
570
+ const marker = retainedSignedValueRef(retainedSubject(), scope)["@id"];
571
+ const object = resolveSignedValues({ object: {
572
+ "@id": marker,
573
+ type: "Note"
574
+ } }, scope).object;
575
+ deepStrictEqual(object["@id"], securedDocument);
576
+ strictEqual(object.type, "Note");
577
+ });
578
+ test("resolveSignedValues() refuses a document too deep to walk", () => {
579
+ const scope = newScope();
580
+ const marker = retainedSignedValueRef(retainedSubject(), scope)["@id"];
581
+ let deep = { "@id": marker };
582
+ for (let i = 0; i < 300; i++) deep = { attachment: deep };
583
+ throws(() => resolveSignedValues({
584
+ object: marker,
585
+ tag: deep
586
+ }, scope), TypeError);
587
+ });
588
+ //#endregion
589
+ export {};
@@ -1,6 +1,5 @@
1
- require("./chunk-C2EiDwsr.cjs");
2
- let node_assert = require("node:assert");
3
1
  let node_test = require("node:test");
2
+ let node_assert = require("node:assert");
4
3
  let _js_temporal_polyfill = require("@js-temporal/polyfill");
5
4
  //#region src/temporal.ts
6
5
  /**
@@ -1,5 +1,5 @@
1
- import { strictEqual } from "node:assert";
2
1
  import { test } from "node:test";
2
+ import { strictEqual } from "node:assert";
3
3
  import { Temporal } from "@js-temporal/polyfill";
4
4
  //#region src/temporal.ts
5
5
  /**