@fedify/fedify 2.4.0-dev.1988 → 2.4.0-dev.1995

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 (62) hide show
  1. package/dist/{builder-Cts78G9N.mjs → builder-D0DxgOV5.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/compound-proof-CJnWc6c-.mjs +495 -0
  4. package/dist/{deno-BgMEefEr.mjs → deno-CABhzCb_.mjs} +1 -1
  5. package/dist/{docloader-DEYs99FZ.mjs → docloader-BbGSL0Xq.mjs} +2 -2
  6. package/dist/federation/builder.test.mjs +1 -1
  7. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  8. package/dist/federation/handler-compound-proof.test.mjs +152 -0
  9. package/dist/federation/handler.test.mjs +5 -5
  10. package/dist/federation/idempotency.test.mjs +2 -2
  11. package/dist/federation/metrics.test.mjs +1 -1
  12. package/dist/federation/middleware.test.mjs +7 -7
  13. package/dist/federation/mod.cjs +1 -1
  14. package/dist/federation/mod.js +1 -1
  15. package/dist/federation/send.test.mjs +3 -3
  16. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  17. package/dist/federation/tasks/tasks.test.mjs +2 -2
  18. package/dist/federation/temporal.test.mjs +1 -1
  19. package/dist/federation/webfinger.test.mjs +1 -1
  20. package/dist/{federation-DzPUG_AP.cjs → federation-DCEHQk4G.cjs} +542 -43
  21. package/dist/{http-CgwqtxAy.js → http-9WyuldYh.js} +2 -2
  22. package/dist/{http-Z3anvCd-.cjs → http-CRFns-UE.cjs} +2 -2
  23. package/dist/{http-CtidZS_t.mjs → http-D9urSO2D.mjs} +3 -3
  24. package/dist/{key-Cwbg5ggV.mjs → key-Ccs8cnd7.mjs} +2 -2
  25. package/dist/{kv-cache-BM2-w-Wq.cjs → kv-cache-DXT9vmIg.cjs} +1 -1
  26. package/dist/{kv-cache-CykuL0ya.js → kv-cache-DtmXhPXG.js} +1 -1
  27. package/dist/{kv-cache-DHiR8yTf.mjs → kv-cache-lKyFn_Dn.mjs} +1 -1
  28. package/dist/{ld-CzhdWyTt.mjs → ld-qup__MAW.mjs} +3 -3
  29. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  30. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  31. package/dist/{metrics-CrqsHP0l.mjs → metrics-CISXCvB1.mjs} +1 -1
  32. package/dist/{middleware-DCpiAsZi.mjs → middleware-CzvJvPHK.mjs} +52 -20
  33. package/dist/{middleware-CZpYxSKD.mjs → middleware-DkrU2WhY.mjs} +1 -1
  34. package/dist/{middleware-CeCj5caM.js → middleware-DsfWajaD.js} +511 -12
  35. package/dist/mod.cjs +19 -19
  36. package/dist/mod.js +4 -4
  37. package/dist/nodeinfo/handler.test.mjs +1 -1
  38. package/dist/{proof-CUghqhAf.js → owner-BIPEZGiU.js} +1239 -1139
  39. package/dist/{owner-BqVXJLW8.mjs → owner-CRoq8Spb.mjs} +2 -2
  40. package/dist/{proof-DHu5zI0Z.cjs → owner-CVA7L9MA.cjs} +1252 -1140
  41. package/dist/{proof-DL8Iid3a.mjs → proof-BDxNVZ3E.mjs} +176 -76
  42. package/dist/{send-6jS4xFsX.mjs → send-BzM5GLyn.mjs} +3 -3
  43. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  44. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  45. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  46. package/dist/sig/compound-proof-verification.test.mjs +465 -0
  47. package/dist/sig/compound-proof.test.d.mts +2 -0
  48. package/dist/sig/compound-proof.test.mjs +324 -0
  49. package/dist/sig/http.test.mjs +2 -2
  50. package/dist/sig/key.test.mjs +1 -1
  51. package/dist/sig/ld.test.mjs +2 -2
  52. package/dist/sig/mod.cjs +17 -17
  53. package/dist/sig/mod.js +2 -2
  54. package/dist/sig/owner.test.mjs +1 -1
  55. package/dist/sig/proof.test.mjs +1 -1
  56. package/dist/{temporal-DcyzeytP.mjs → temporal-Dv6Qv-8k.mjs} +1 -1
  57. package/dist/testing/kv.test.mjs +1 -1
  58. package/dist/utils/docloader.test.mjs +2 -2
  59. package/dist/utils/kv-cache.test.mjs +1 -1
  60. package/dist/utils/mod.cjs +1 -1
  61. package/dist/utils/mod.js +1 -1
  62. package/package.json +6 -6
@@ -1,12 +1,12 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CrqsHP0l.mjs";
6
- import { r as fetchKey, s as validateCryptoKey } from "./key-Cwbg5ggV.mjs";
4
+ import { n as version, t as name } from "./deno-CABhzCb_.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CISXCvB1.mjs";
6
+ import { r as fetchKey, s as validateCryptoKey } from "./key-Ccs8cnd7.mjs";
7
7
  import { n as preloadedOnlyDocumentLoader } from "./public-audience-Cc2YJ3ip.mjs";
8
8
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-B5Ne7JoL.mjs";
9
- import { c as getNormalizationContextLoader } from "./ld-CzhdWyTt.mjs";
9
+ import { c as getNormalizationContextLoader } from "./ld-qup__MAW.mjs";
10
10
  import { getLogger } from "@logtape/logtape";
11
11
  import { Activity, DataIntegrityProof, Multikey, getTypeId } from "@fedify/vocab";
12
12
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -164,6 +164,30 @@ async function signObject(object, privateKey, keyId, options = {}) {
164
164
  async function verifyProof(jsonLd, proof, options = {}) {
165
165
  return await verifyProofWithMessageDigestCache(jsonLd, proof, options);
166
166
  }
167
+ /**
168
+ * Verifies the one direct literal proof on a map-local secured document.
169
+ *
170
+ * Unlike {@link verifyProof}, this removes only the literal `proof` member
171
+ * from the message digest. JSON-LD aliases remain part of the signed input,
172
+ * and the document's received `@context` is not replaced by the proof
173
+ * configuration context.
174
+ *
175
+ * @internal
176
+ */
177
+ async function verifyMapLocalProof(jsonLd, options = {}) {
178
+ if (!isJsonLdNode(jsonLd) || !globalThis.Object.hasOwn(jsonLd, "proof") || !isJsonLdNode(jsonLd.proof)) return null;
179
+ const proofContextLoader = getNormalizationContextLoader(preloadedOnlyDocumentLoader);
180
+ try {
181
+ const [candidate] = await parseRawProofCandidates(jsonLd, [jsonLd.proof], options, proofContextLoader);
182
+ if (candidate.proof == null) return null;
183
+ return await verifyProofWithMessageDigestCache(jsonLd, candidate.proof, options, {
184
+ proofContextLoader,
185
+ proofPropertyMode: "literal"
186
+ }, candidate);
187
+ } catch {
188
+ return null;
189
+ }
190
+ }
167
191
  async function verifyProofWithMessageDigestCache(jsonLd, proof, options, messageDigestCache = {}, rawProofCandidate, keyIdBoundByCaller = false) {
168
192
  return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("object_integrity_proofs.verify", async (span) => {
169
193
  const start = performance.now();
@@ -234,10 +258,11 @@ async function getJsonLdPropertyNames(jsonLd, propertyIri, defaults, documentLoa
234
258
  async function getProofPropertyNames(jsonLd, documentLoader = preloadedOnlyDocumentLoader) {
235
259
  return await getJsonLdPropertyNames(jsonLd, SECURITY_PROOF, ["proof", SECURITY_PROOF], documentLoader) ?? /* @__PURE__ */ new Set(["proof", SECURITY_PROOF]);
236
260
  }
237
- async function createProofMessageDigests(jsonLd, proofContextLoader, context) {
261
+ async function createProofMessageDigests(jsonLd, proofContextLoader, context, proofPropertyMode = "jsonLd") {
238
262
  const msg = { ...jsonLd };
239
- for (const property of await getProofPropertyNames(msg, proofContextLoader)) delete msg[property];
240
- if (context != null) msg["@context"] = structuredClone(context);
263
+ if (proofPropertyMode === "literal") delete msg.proof;
264
+ else for (const property of await getProofPropertyNames(msg, proofContextLoader)) delete msg[property];
265
+ if (proofPropertyMode === "jsonLd" && context != null) msg["@context"] = structuredClone(context);
241
266
  const encoder = new TextEncoder();
242
267
  const digest = async (value) => {
243
268
  const bytes = encoder.encode(serialize(value));
@@ -554,13 +579,15 @@ async function verifyProofInternal(jsonLd, proof, options, messageDigestCache, r
554
579
  const messageDigestValues = messageDigestCache.values ??= /* @__PURE__ */ new Map();
555
580
  let messageDigestsPromise = messageDigestValues.get(messageDigestKey);
556
581
  if (messageDigestsPromise == null) {
557
- messageDigestsPromise = createProofMessageDigests(jsonLd, messageDigestCache.proofContextLoader, proofConfiguration.context);
582
+ messageDigestsPromise = createProofMessageDigests(jsonLd, messageDigestCache.proofContextLoader, proofConfiguration.context, messageDigestCache.proofPropertyMode);
558
583
  messageDigestValues.set(messageDigestKey, messageDigestsPromise);
559
584
  }
560
585
  const messageDigests = await messageDigestsPromise;
561
586
  if (await verifyCandidate(messageDigests.onWire)) return publicKey;
562
- const normalizedDigest = await messageDigests.normalized();
563
- if (normalizedDigest != null && await verifyCandidate(normalizedDigest)) return publicKey;
587
+ if (messageDigestCache.proofPropertyMode !== "literal") {
588
+ const normalizedDigest = await messageDigests.normalized();
589
+ if (normalizedDigest != null && await verifyCandidate(normalizedDigest)) return publicKey;
590
+ }
564
591
  if (fetchedKey.cached) {
565
592
  logger.debug("Failed to verify the proof with the cached key {keyId}; retrying with the freshly fetched key...", {
566
593
  keyId: proof.verificationMethodId.href,
@@ -652,59 +679,55 @@ async function expandPortableObjectRoot(jsonLd, contextLoader) {
652
679
  }
653
680
  };
654
681
  }
655
- /**
656
- * Verifies the FEP-ef61 Object Integrity Proof policy for a portable object.
657
- *
658
- * This applies the FEP-2277 core-type classification to the top-level JSON-LD
659
- * node. Portable actors, activities, and objects require proofs. A portable
660
- * collection without a proof is reported separately so a caller can apply a
661
- * gateway trust policy. Embedded portable objects are not traversed.
662
- *
663
- * Every proof must use a DID URL whose DID matches the portable object's
664
- * authority, and every proof must pass {@link verifyProof}.
665
- *
666
- * @param jsonLd The JSON-LD document to verify.
667
- * @param options Additional options. See also
668
- * {@link VerifyPortableObjectProofOptions}.
669
- * @returns The detailed portable proof-policy result.
670
- * @throws {TypeError} If the input is not a single JSON-LD object or has a
671
- * malformed portable ID.
672
- * @since 2.4.0
673
- */
674
- async function verifyPortableObjectProof(jsonLd, options = {}) {
682
+ async function preparePortableObjectProof(jsonLd, options) {
675
683
  if (isJsonLdNode(jsonLd) && typeof jsonLd["@id"] === "string" && !PORTABLE_OBJECT_ID_PATTERN.test(jsonLd["@id"])) return {
676
- verified: false,
677
- reason: { type: "notPortableObject" }
684
+ prepared: false,
685
+ result: {
686
+ verified: false,
687
+ reason: { type: "notPortableObject" }
688
+ }
678
689
  };
679
690
  const { root, proofContextLoader } = await expandPortableObjectRoot(jsonLd, options.contextLoader);
680
691
  const id = root["@id"];
681
692
  if (typeof id !== "string" || !PORTABLE_OBJECT_ID_PATTERN.test(id)) return {
682
- verified: false,
683
- reason: { type: "notPortableObject" }
693
+ prepared: false,
694
+ result: {
695
+ verified: false,
696
+ reason: { type: "notPortableObject" }
697
+ }
684
698
  };
685
699
  const objectId = parseIri(id);
686
700
  getFe34Origin(objectId);
687
701
  const objectType = classifyFep2277CoreType(root);
688
702
  if (objectType === "verificationMethod" || objectType === "publicKey" || objectType === "link") return {
689
- verified: false,
690
- reason: {
691
- type: "unsupportedObjectType",
692
- objectType
703
+ prepared: false,
704
+ result: {
705
+ verified: false,
706
+ reason: {
707
+ type: "unsupportedObjectType",
708
+ objectType
709
+ }
693
710
  }
694
711
  };
695
712
  const proofValues = root[SECURITY_PROOF];
696
- if (proofValues == null || Array.isArray(proofValues) && proofValues.length < 1) return objectType === "collection" ? {
697
- verified: false,
698
- reason: { type: "unsecuredCollection" }
699
- } : {
700
- verified: false,
701
- reason: { type: "missingProof" }
713
+ if (proofValues == null || Array.isArray(proofValues) && proofValues.length < 1) return {
714
+ prepared: false,
715
+ result: objectType === "collection" ? {
716
+ verified: false,
717
+ reason: { type: "unsecuredCollection" }
718
+ } : {
719
+ verified: false,
720
+ reason: { type: "missingProof" }
721
+ }
702
722
  };
703
723
  if (!Array.isArray(proofValues)) return {
704
- verified: false,
705
- reason: {
706
- type: "invalidProof",
707
- proofIndex: 0
724
+ prepared: false,
725
+ result: {
726
+ verified: false,
727
+ reason: {
728
+ type: "invalidProof",
729
+ proofIndex: 0
730
+ }
708
731
  }
709
732
  };
710
733
  const rawProofValues = isJsonLdNode(jsonLd) ? await getRawProofValues(jsonLd, proofContextLoader) : [];
@@ -712,10 +735,13 @@ async function verifyPortableObjectProof(jsonLd, options = {}) {
712
735
  for (let proofIndex = 0; proofIndex < proofValues.length; proofIndex++) {
713
736
  const proofValue = proofValues[proofIndex];
714
737
  if (!hasValidPortableProofShape(proofValue)) return {
715
- verified: false,
716
- reason: {
717
- type: "invalidProof",
718
- proofIndex
738
+ prepared: false,
739
+ result: {
740
+ verified: false,
741
+ reason: {
742
+ type: "invalidProof",
743
+ proofIndex
744
+ }
719
745
  }
720
746
  };
721
747
  let proof;
@@ -723,10 +749,13 @@ async function verifyPortableObjectProof(jsonLd, options = {}) {
723
749
  proof = await DataIntegrityProof.fromJsonLd(proofValue, options);
724
750
  } catch {
725
751
  return {
726
- verified: false,
727
- reason: {
728
- type: "invalidProof",
729
- proofIndex
752
+ prepared: false,
753
+ result: {
754
+ verified: false,
755
+ reason: {
756
+ type: "invalidProof",
757
+ proofIndex
758
+ }
730
759
  }
731
760
  };
732
761
  }
@@ -735,18 +764,24 @@ async function verifyPortableObjectProof(jsonLd, options = {}) {
735
764
  for (let proofIndex = 0; proofIndex < proofs.length; proofIndex++) {
736
765
  const verificationMethod = proofs[proofIndex].verificationMethodId;
737
766
  if (verificationMethod == null) return {
738
- verified: false,
739
- reason: {
740
- type: "invalidProof",
741
- proofIndex
767
+ prepared: false,
768
+ result: {
769
+ verified: false,
770
+ reason: {
771
+ type: "invalidProof",
772
+ proofIndex
773
+ }
742
774
  }
743
775
  };
744
776
  if (verificationMethod.protocol !== "did:") return {
745
- verified: false,
746
- reason: {
747
- type: "unsupportedVerificationMethod",
748
- proofIndex,
749
- verificationMethod
777
+ prepared: false,
778
+ result: {
779
+ verified: false,
780
+ reason: {
781
+ type: "unsupportedVerificationMethod",
782
+ proofIndex,
783
+ verificationMethod
784
+ }
750
785
  }
751
786
  };
752
787
  try {
@@ -754,24 +789,89 @@ async function verifyPortableObjectProof(jsonLd, options = {}) {
754
789
  } catch (error) {
755
790
  if (!(error instanceof TypeError)) throw error;
756
791
  return {
792
+ prepared: false,
793
+ result: {
794
+ verified: false,
795
+ reason: {
796
+ type: "unsupportedVerificationMethod",
797
+ proofIndex,
798
+ verificationMethod
799
+ }
800
+ }
801
+ };
802
+ }
803
+ if (!haveSameFe34Origin(objectId, verificationMethod)) return {
804
+ prepared: false,
805
+ result: {
757
806
  verified: false,
758
807
  reason: {
759
- type: "unsupportedVerificationMethod",
808
+ type: "verificationMethodMismatch",
760
809
  proofIndex,
810
+ objectId,
761
811
  verificationMethod
762
812
  }
763
- };
764
- }
765
- if (!haveSameFe34Origin(objectId, verificationMethod)) return {
766
- verified: false,
767
- reason: {
768
- type: "verificationMethodMismatch",
769
- proofIndex,
770
- objectId,
771
- verificationMethod
772
813
  }
773
814
  };
774
815
  }
816
+ return {
817
+ prepared: true,
818
+ objectId,
819
+ proofs,
820
+ rawProofValues,
821
+ proofContextLoader
822
+ };
823
+ }
824
+ /**
825
+ * Applies portable-object proof policy to one map-local cryptographic result.
826
+ *
827
+ * @internal
828
+ */
829
+ async function verifyPortableObjectProofPolicy(jsonLd, key, options = {}) {
830
+ const policyOptions = {
831
+ ...options,
832
+ contextLoader: preloadedOnlyDocumentLoader
833
+ };
834
+ const prepared = await preparePortableObjectProof(jsonLd, policyOptions);
835
+ if (!prepared.prepared) return prepared.result;
836
+ const policyProof = prepared.proofs[0];
837
+ const literalProof = isJsonLdNode(jsonLd) && isJsonLdNode(jsonLd.proof) ? (await parseRawProofCandidates(jsonLd, [jsonLd.proof], policyOptions, prepared.proofContextLoader))[0]?.proof : null;
838
+ const verificationMethod = policyProof?.verificationMethodId;
839
+ if (prepared.proofs.length !== 1 || key == null || policyProof == null || literalProof == null || !sameProof(policyProof, literalProof) || verificationMethod == null || key.id?.href !== verificationMethod.href) return {
840
+ verified: false,
841
+ reason: {
842
+ type: "invalidProof",
843
+ proofIndex: prepared.proofs.length > 1 ? 1 : 0
844
+ }
845
+ };
846
+ return {
847
+ verified: true,
848
+ keys: [key],
849
+ objectId: formatIri(prepared.objectId)
850
+ };
851
+ }
852
+ /**
853
+ * Verifies the FEP-ef61 Object Integrity Proof policy for a portable object.
854
+ *
855
+ * This applies the FEP-2277 core-type classification to the top-level JSON-LD
856
+ * node. Portable actors, activities, and objects require proofs. A portable
857
+ * collection without a proof is reported separately so a caller can apply a
858
+ * gateway trust policy. Embedded portable objects are not traversed.
859
+ *
860
+ * Every proof must use a DID URL whose DID matches the portable object's
861
+ * authority, and every proof must pass {@link verifyProof}.
862
+ *
863
+ * @param jsonLd The JSON-LD document to verify.
864
+ * @param options Additional options. See also
865
+ * {@link VerifyPortableObjectProofOptions}.
866
+ * @returns The detailed portable proof-policy result.
867
+ * @throws {TypeError} If the input is not a single JSON-LD object or has a
868
+ * malformed portable ID.
869
+ * @since 2.4.0
870
+ */
871
+ async function verifyPortableObjectProof(jsonLd, options = {}) {
872
+ const prepared = await preparePortableObjectProof(jsonLd, options);
873
+ if (!prepared.prepared) return prepared.result;
874
+ const { proofs, rawProofValues, proofContextLoader } = prepared;
775
875
  const keys = [];
776
876
  const rawProofCandidatePool = {
777
877
  candidates: await parseRawProofCandidates(jsonLd, rawProofValues, options, proofContextLoader),
@@ -890,4 +990,4 @@ function hasCryptographicOrigin(iri) {
890
990
  return /^did:/i.test(iri) || /^ap(?:\+ef61)?:\/\//i.test(iri);
891
991
  }
892
992
  //#endregion
893
- export { verifyPortableObjectProof as a, verifyObject as i, hasProofLike as n, verifyProof as o, signObject as r, createProof as t };
993
+ export { verifyObject as a, verifyProof as c, verifyMapLocalProof as i, hasProofLike as n, verifyPortableObjectProof as o, signObject as r, verifyPortableObjectProofPolicy as s, createProof as t };
@@ -1,9 +1,9 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-BgMEefEr.mjs";
5
- import { n as getDurationMs, r as getFederationMetrics } from "./metrics-CrqsHP0l.mjs";
6
- import { n as doubleKnock } from "./http-CtidZS_t.mjs";
4
+ import { n as version, t as name } from "./deno-CABhzCb_.mjs";
5
+ import { n as getDurationMs, r as getFederationMetrics } from "./metrics-CISXCvB1.mjs";
6
+ import { n as doubleKnock } from "./http-D9urSO2D.mjs";
7
7
  import { getLogger } from "@logtape/logtape";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
9
9
  import { FetchError, UrlError, validatePublicUrl } from "@fedify/vocab-runtime";
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";
@@ -0,0 +1,331 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as assertEquals } from "../assert_equals-CBlRfA-_.mjs";
5
+ import "../std__assert-CxFT0MNt.mjs";
6
+ import { n as inspectCompoundPortableObjectApplicability, t as discoverCompoundProofDocuments } from "../compound-proof-CJnWc6c-.mjs";
7
+ import { t as map_local_create_note_default } from "../map-local-create-note-B0SeXJms.mjs";
8
+ import { test } from "@fedify/fixture";
9
+ //#region src/sig/compound-proof-discovery.test.ts
10
+ const limits = {
11
+ maxDepth: 16,
12
+ maxMaps: 32,
13
+ maxProofs: 8,
14
+ maxBytes: 16384
15
+ };
16
+ test("portable applicability is bounded and ignores metadata", () => {
17
+ assertEquals(inspectCompoundPortableObjectApplicability({
18
+ "@context": { id: "ap://did:key:context/object" },
19
+ proof: { id: "ap://did:key:proof/object" },
20
+ object: { id: "https://example.com/objects/1" }
21
+ }, limits), "absent");
22
+ assertEquals(inspectCompoundPortableObjectApplicability({ object: [{ "@id": "AP+EF61://did:key:child/objects/1" }] }, limits), "present");
23
+ assertEquals(inspectCompoundPortableObjectApplicability({ values: new Array(2e4).fill(0) }, {
24
+ ...limits,
25
+ maxBytes: 1e3
26
+ }), "indeterminate");
27
+ });
28
+ test("discoverCompoundProofDocuments() returns immutable deepest-first candidates", () => {
29
+ const input = {
30
+ id: "ap://example.test/activities/1",
31
+ object: {
32
+ id: "ap://example.test/objects/1",
33
+ content: "one",
34
+ proof: {
35
+ type: "DataIntegrityProof",
36
+ proofValue: "zInner"
37
+ }
38
+ },
39
+ items: [{
40
+ id: "ap://example.test/objects/2",
41
+ content: "two",
42
+ proof: {
43
+ type: "DataIntegrityProof",
44
+ proofValue: "zArray"
45
+ }
46
+ }],
47
+ "a/b~c": {
48
+ "@id": "ap://example.test/objects/3",
49
+ proof: {
50
+ type: "DataIntegrityProof",
51
+ proofValue: "zEscaped"
52
+ }
53
+ },
54
+ proof: {
55
+ type: "DataIntegrityProof",
56
+ proofValue: "zOuter",
57
+ proof: {
58
+ type: "DataIntegrityProof",
59
+ proofValue: "zNotADocument"
60
+ }
61
+ }
62
+ };
63
+ const before = structuredClone(input);
64
+ const result = discoverCompoundProofDocuments(input, limits);
65
+ assertEquals(input, before);
66
+ assertEquals(result.status, "ok");
67
+ if (result.status !== "ok") return;
68
+ assertEquals(result.documents.map(({ path, id, depth }) => ({
69
+ path,
70
+ id,
71
+ depth
72
+ })), [
73
+ {
74
+ path: "/items/0",
75
+ id: "ap://example.test/objects/2",
76
+ depth: 2
77
+ },
78
+ {
79
+ path: "/a~1b~0c",
80
+ id: "ap://example.test/objects/3",
81
+ depth: 1
82
+ },
83
+ {
84
+ path: "/object",
85
+ id: "ap://example.test/objects/1",
86
+ depth: 1
87
+ },
88
+ {
89
+ path: "",
90
+ id: "ap://example.test/activities/1",
91
+ depth: 0
92
+ }
93
+ ]);
94
+ assertEquals(result.documents[0].securedDocument, input.items[0]);
95
+ assertEquals(result.documents[0].proof, input.items[0].proof);
96
+ assertEquals(result.statistics.proofCount, 4);
97
+ input.items[0].content = "mutated after discovery";
98
+ assertEquals(result.documents[0].securedDocument.content, "two");
99
+ assertEquals(result.snapshot.items[0].content, "two");
100
+ });
101
+ test("discoverCompoundProofDocuments() is independent of member order", () => {
102
+ const first = {
103
+ z: {
104
+ id: "z",
105
+ proof: { proofValue: "z" }
106
+ },
107
+ a: {
108
+ id: "a",
109
+ proof: { proofValue: "a" }
110
+ },
111
+ proof: { proofValue: "root" }
112
+ };
113
+ const second = {
114
+ proof: { proofValue: "root" },
115
+ a: {
116
+ proof: { proofValue: "a" },
117
+ id: "a"
118
+ },
119
+ z: {
120
+ proof: { proofValue: "z" },
121
+ id: "z"
122
+ }
123
+ };
124
+ const firstResult = discoverCompoundProofDocuments(first, limits);
125
+ const secondResult = discoverCompoundProofDocuments(second, limits);
126
+ assertEquals(firstResult.status, "ok");
127
+ assertEquals(secondResult.status, "ok");
128
+ if (firstResult.status !== "ok" || secondResult.status !== "ok") return;
129
+ assertEquals(firstResult.documents.map((candidate) => candidate.path), [
130
+ "/a",
131
+ "/z",
132
+ ""
133
+ ]);
134
+ assertEquals(secondResult.documents.map((candidate) => candidate.path), [
135
+ "/a",
136
+ "/z",
137
+ ""
138
+ ]);
139
+ });
140
+ test("discoverCompoundProofDocuments() does not inspect context definitions", () => {
141
+ const result = discoverCompoundProofDocuments({
142
+ "@context": {
143
+ proof: "https://w3id.org/security#proof",
144
+ portable: {
145
+ "@id": "ap://did:key:z6MkContext/objects/1",
146
+ "@context": { proof: "https://example.com/nested-proof" }
147
+ }
148
+ },
149
+ id: "https://social.example/objects/1",
150
+ type: "Note"
151
+ }, limits);
152
+ assertEquals(result.status, "ok");
153
+ if (result.status !== "ok") return;
154
+ assertEquals(result.documents, []);
155
+ assertEquals(result.statistics.proofCount, 0);
156
+ });
157
+ test("discoverCompoundProofDocuments() preserves the recorded compound vector", () => {
158
+ const result = discoverCompoundProofDocuments(map_local_create_note_default.documents.finalSecuredCompound, limits);
159
+ assertEquals(result.status, "ok");
160
+ if (result.status !== "ok") return;
161
+ assertEquals(result.documents.map(({ path }) => path), ["/object", ""]);
162
+ assertEquals(result.documents[0].securedDocument, map_local_create_note_default.documents.securedInner);
163
+ assertEquals(result.documents[1].securedDocument, map_local_create_note_default.documents.finalSecuredCompound);
164
+ });
165
+ test("discoverCompoundProofDocuments() rejects unsupported proof shapes atomically", () => {
166
+ for (const proof of [
167
+ null,
168
+ "remote-proof",
169
+ [{ proofValue: "z" }]
170
+ ]) assertEquals(discoverCompoundProofDocuments({
171
+ object: { proof: { proofValue: "valid candidate" } },
172
+ other: { proof }
173
+ }, limits), {
174
+ status: "unsupported",
175
+ reason: {
176
+ type: "unsupportedProofShape",
177
+ path: "/other/proof"
178
+ }
179
+ });
180
+ });
181
+ test("discoverCompoundProofDocuments() applies resource limits atomically", () => {
182
+ const cases = [
183
+ {
184
+ input: { child: { child: { proof: {} } } },
185
+ limits: {
186
+ ...limits,
187
+ maxDepth: 1
188
+ },
189
+ limit: "depth",
190
+ maximum: 1
191
+ },
192
+ {
193
+ input: {
194
+ first: {},
195
+ second: { proof: {} }
196
+ },
197
+ limits: {
198
+ ...limits,
199
+ maxMaps: 2
200
+ },
201
+ limit: "maps",
202
+ maximum: 2
203
+ },
204
+ {
205
+ input: {
206
+ first: { proof: {} },
207
+ second: { proof: {} }
208
+ },
209
+ limits: {
210
+ ...limits,
211
+ maxProofs: 1
212
+ },
213
+ limit: "proofs",
214
+ maximum: 1
215
+ },
216
+ {
217
+ input: {
218
+ content: "too large",
219
+ proof: {}
220
+ },
221
+ limits: {
222
+ ...limits,
223
+ maxBytes: 8
224
+ },
225
+ limit: "bytes",
226
+ maximum: 8
227
+ }
228
+ ];
229
+ for (const fixture of cases) {
230
+ const result = discoverCompoundProofDocuments(fixture.input, fixture.limits);
231
+ assertEquals(result.status, "unsupported");
232
+ if (result.status !== "unsupported") continue;
233
+ assertEquals(result.reason.type, "limitExceeded");
234
+ if (result.reason.type !== "limitExceeded") continue;
235
+ assertEquals(result.reason.limit, fixture.limit);
236
+ assertEquals(result.reason.maximum, fixture.maximum);
237
+ }
238
+ });
239
+ test("discoverCompoundProofDocuments() rejects values that are not JSON trees", () => {
240
+ const cyclic = { proof: {} };
241
+ cyclic.self = cyclic;
242
+ const shared = { proof: {} };
243
+ const symbolKeyed = { [Symbol("hidden")]: { proof: {} } };
244
+ for (const input of [
245
+ [],
246
+ { value: void 0 },
247
+ { value: NaN },
248
+ cyclic,
249
+ {
250
+ first: shared,
251
+ second: shared
252
+ },
253
+ symbolKeyed
254
+ ]) {
255
+ const result = discoverCompoundProofDocuments(input, limits);
256
+ assertEquals(result.status, "unsupported");
257
+ if (result.status !== "unsupported") continue;
258
+ assertEquals(result.reason.type, "invalidJsonTree");
259
+ }
260
+ });
261
+ test("discoverCompoundProofDocuments() handles wide JSON containers", () => {
262
+ const input = { values: new Array(2e5).fill(0) };
263
+ const result = discoverCompoundProofDocuments(input, {
264
+ maxDepth: 2,
265
+ maxMaps: 1,
266
+ maxProofs: 0,
267
+ maxBytes: 1048576
268
+ });
269
+ assertEquals(result.status, "ok");
270
+ if (result.status !== "ok") return;
271
+ assertEquals(result.statistics.byteLength, JSON.stringify(input).length);
272
+ });
273
+ test("discoverCompoundProofDocuments() rejects accessors without invoking them", () => {
274
+ let getterCalls = 0;
275
+ const accessorMap = {};
276
+ Object.defineProperty(accessorMap, "object", {
277
+ enumerable: true,
278
+ get() {
279
+ getterCalls++;
280
+ return { proof: {} };
281
+ }
282
+ });
283
+ const accessorArray = [{}];
284
+ Object.defineProperty(accessorArray, 0, {
285
+ enumerable: true,
286
+ get() {
287
+ getterCalls++;
288
+ return { proof: {} };
289
+ }
290
+ });
291
+ for (const input of [accessorMap, { items: accessorArray }]) {
292
+ const result = discoverCompoundProofDocuments(input, limits);
293
+ assertEquals(result.status, "unsupported");
294
+ if (result.status !== "unsupported") continue;
295
+ assertEquals(result.reason.type, "invalidJsonTree");
296
+ }
297
+ assertEquals(getterCalls, 0);
298
+ });
299
+ test("discoverCompoundProofDocuments() rejects non-enumerable map properties", () => {
300
+ const input = {};
301
+ Object.defineProperty(input, "proof", {
302
+ value: {},
303
+ enumerable: false
304
+ });
305
+ const result = discoverCompoundProofDocuments(input, limits);
306
+ assertEquals(result.status, "unsupported");
307
+ if (result.status !== "unsupported") return;
308
+ assertEquals(result.reason.type, "invalidJsonTree");
309
+ });
310
+ test("discoverCompoundProofDocuments() rejects non-JSON array properties", () => {
311
+ const namedProperty = Object.assign([], { extra: { proof: {} } });
312
+ const sparse = new Array(2);
313
+ sparse[1] = null;
314
+ const nonEnumerableIndex = [];
315
+ Object.defineProperty(nonEnumerableIndex, 0, {
316
+ value: { proof: {} },
317
+ enumerable: false
318
+ });
319
+ for (const input of [
320
+ { items: namedProperty },
321
+ { items: sparse },
322
+ { items: nonEnumerableIndex }
323
+ ]) {
324
+ const result = discoverCompoundProofDocuments(input, limits);
325
+ assertEquals(result.status, "unsupported");
326
+ if (result.status !== "unsupported") continue;
327
+ assertEquals(result.reason.type, "invalidJsonTree");
328
+ }
329
+ });
330
+ //#endregion
331
+ export {};