@fedify/fedify 2.4.0-dev.2084 → 2.4.0-dev.2099
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/{builder-BGlpUocK.mjs → builder-Bno54o_-.mjs} +34 -3
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-B6Aj2kGY.mjs → compound-proof-X3SxXVbc.mjs} +191 -13
- package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
- package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
- package/dist/{deno-Dn-py9Zy.mjs → deno-dxdvl2XZ.mjs} +1 -1
- package/dist/{docloader-BUq5bYJr.mjs → docloader-eBxQDsk9.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +254 -5
- package/dist/federation/portable-gateway-keys.test.mjs +72 -11
- package/dist/federation/portable-inbox.test.mjs +487 -10
- package/dist/federation/portable.test.mjs +182 -4
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +89 -1
- package/dist/{federation-BLGx8JUH.cjs → federation-C4Ef3JKa.cjs} +1487 -1070
- package/dist/{http-B5q1CHtL.cjs → http-C8cZTE3Y.cjs} +1224 -985
- package/dist/{http-4PCCvhsU.js → http-D-rbr_Sy.js} +1663 -1460
- package/dist/{http-B0VRNipE.mjs → http-DsgawTd8.mjs} +82 -7
- package/dist/{key-BJrp524f.mjs → key-DYfmjnmp.mjs} +39 -99
- package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DIxwMeNi.js} +1 -1
- package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
- package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
- package/dist/{ld-BmFz5NLA.mjs → ld-B00wWHzc.mjs} +3 -3
- package/dist/{metrics-Beod6tzO.mjs → metrics-BP3vdBpb.mjs} +1 -1
- package/dist/{middleware-BKeQYPm0.mjs → middleware-Br1_d-Ga.mjs} +606 -389
- package/dist/{middleware-D_9pNYe9.js → middleware-CcXMe400.js} +1602 -1185
- package/dist/{middleware-B7CNsyqt.mjs → middleware-CeBVBa61.mjs} +1 -1
- package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
- package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
- package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
- package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-CsKVgMzk.mjs → owner-BJPDRDRq.mjs} +5 -4
- package/dist/{owner-BQFZtFV4.cjs → owner-DSzH7wro.cjs} +2 -2
- package/dist/{owner-_pbbgLJK.js → owner-U12fDpQW.js} +2 -2
- package/dist/{portable-key-B4R-CiIV.mjs → portable-key-D0mWbV9G.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-ozG4HfcQ.mjs → proof-ChjBwnep.mjs} +59 -18
- package/dist/{send-ClPt8nYu.mjs → send-yFX0E-ye.mjs} +3 -3
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +160 -3
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +72 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +69 -5
- package/dist/sig/portable-key.test.mjs +176 -10
- package/dist/sig/proof.test.mjs +107 -4
- package/dist/{sig-k3C8sF7g.cjs → sig-B689Nj7y.cjs} +19 -3
- package/dist/{sig-DhdomAN0.js → sig-C85KqAcx.js} +19 -3
- package/dist/{temporal-Ba0EAZAI.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +5 -2
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
|
@@ -3,19 +3,149 @@ const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
|
|
|
3
3
|
let _logtape_logtape = require("@logtape/logtape");
|
|
4
4
|
let _fedify_vocab = require("@fedify/vocab");
|
|
5
5
|
let _opentelemetry_api = require("@opentelemetry/api");
|
|
6
|
-
let byte_encodings_hex = require("byte-encodings/hex");
|
|
7
6
|
let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
|
|
7
|
+
let _fedify_vocab_runtime_internal_portable_dereference = require("@fedify/vocab-runtime/internal/portable-dereference");
|
|
8
|
+
let byte_encodings_hex = require("byte-encodings/hex");
|
|
8
9
|
let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
|
|
9
10
|
_fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
|
|
10
11
|
let structured_field_values = require("structured-field-values");
|
|
12
|
+
let byte_encodings_base64 = require("byte-encodings/base64");
|
|
11
13
|
let _fedify_vocab_runtime_internal_signed_representation = require("@fedify/vocab-runtime/internal/signed-representation");
|
|
12
14
|
let json_canon = require("json-canon");
|
|
13
15
|
json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
|
|
14
|
-
let byte_encodings_base64 = require("byte-encodings/base64");
|
|
15
16
|
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
16
17
|
//#region deno.json
|
|
17
18
|
var name = "@fedify/fedify";
|
|
18
|
-
var version = "2.4.0-dev.
|
|
19
|
+
var version = "2.4.0-dev.2099+cd074fe2";
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/sig/portable-key-id.ts
|
|
22
|
+
const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
|
|
23
|
+
/**
|
|
24
|
+
* Checks whether the URL is an `ap:` or `ap+ef61:` URI.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
function isPortableUri(url) {
|
|
28
|
+
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Checks whether an ID identifies an [FEP-ef61] portable object, i.e., it is
|
|
32
|
+
* an `ap:` or `ap+ef61:` URI, or looks like a compatible identifier.
|
|
33
|
+
* A malformed compatible identifier counts too, so that it is rejected as
|
|
34
|
+
* a portable object that no proof can match, rather than trusted by its web
|
|
35
|
+
* origin.
|
|
36
|
+
*
|
|
37
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
38
|
+
* @internal
|
|
39
|
+
*/
|
|
40
|
+
function isPortableId(id) {
|
|
41
|
+
if (typeof id === "string") {
|
|
42
|
+
if (/^ap(?:\+ef61)?:\/\//i.test(id)) return true;
|
|
43
|
+
} else if (isPortableUri(id)) return true;
|
|
44
|
+
return (0, _fedify_vocab_runtime_internal_portable_dereference.isCompatibleEf61Iri)(id);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Gets the DID, i.e., the FEP-fe34 cryptographic origin, of an `ap:` or
|
|
48
|
+
* `ap+ef61:` URI, a DID URL, or a compatible identifier.
|
|
49
|
+
* @returns The DID, or `null` if the ID is none of them, or is malformed.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
function getPortableDid(id) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = typeof id === "string" ? id : id.href;
|
|
55
|
+
if (/^(?:did:|ap(?:\+ef61)?:\/\/)/i.test(raw)) return (0, _fedify_vocab_runtime.getFe34Origin)(id);
|
|
56
|
+
const portable = (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
|
|
57
|
+
return portable == null ? null : (0, _fedify_vocab_runtime.getFe34Origin)(portable);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof TypeError) return null;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Checks whether an activity is performed by an FEP-ef61 portable actor,
|
|
65
|
+
* i.e., any of its actors has an `ap:` or `ap+ef61:` ID, or a compatible
|
|
66
|
+
* identifier.
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
function hasPortableActor(activity) {
|
|
70
|
+
return activity.actorIds.some((id) => isPortableId(id));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Checks whether a key ID looks like an [FEP-ef61] compatible identifier,
|
|
74
|
+
* i.e., an HTTP(S) URL under a gateway's `/.well-known/apgateway/did:` path.
|
|
75
|
+
* Such a key ID names a key that a gateway holds for a portable actor, so it
|
|
76
|
+
* is resolved only as a gateway key, and never cached in a key cache.
|
|
77
|
+
*
|
|
78
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
function isCompatibleKeyId(keyId) {
|
|
82
|
+
return (keyId.protocol === "http:" || keyId.protocol === "https:") && COMPATIBLE_KEY_ID_PATH_PATTERN.test(keyId.pathname);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets the canonical portable ID of an actor ID that is either an `ap:` or
|
|
86
|
+
* `ap+ef61:` URI or a compatible identifier.
|
|
87
|
+
* @returns The canonical portable ID, or `null` if the ID is neither, or is
|
|
88
|
+
* malformed.
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
function getCanonicalPortableId(id) {
|
|
92
|
+
try {
|
|
93
|
+
if (isPortableUri(id)) return (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)(id));
|
|
94
|
+
const portable = (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
|
|
95
|
+
if (portable == null) return null;
|
|
96
|
+
return (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)(portable));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof TypeError) return null;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Builds the base of the key IDs of a gateway's keys for a portable actor:
|
|
104
|
+
* the actor's compatible identifier on the gateway.
|
|
105
|
+
* @param actorId The portable actor's ID, either an `ap:` or `ap+ef61:` URI
|
|
106
|
+
* or a compatible identifier. It must not have a fragment.
|
|
107
|
+
* @param gateway The gateway's origin, e.g., `https://example.com`.
|
|
108
|
+
* @returns The compatible identifier without query and fragment.
|
|
109
|
+
* @throws {TypeError} If the actor ID is not a portable ID, or has
|
|
110
|
+
* a fragment.
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
function getGatewayKeyBase(actorId, gateway) {
|
|
114
|
+
let portable;
|
|
115
|
+
if (isPortableUri(actorId)) portable = actorId;
|
|
116
|
+
else try {
|
|
117
|
+
portable = (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(actorId);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (!(error instanceof TypeError)) throw error;
|
|
120
|
+
portable = null;
|
|
121
|
+
}
|
|
122
|
+
if (portable == null) throw new TypeError(`The portable actor ID ${actorId.href} must be an ap: or ap+ef61: URI, or an FEP-ef61 compatible identifier.`);
|
|
123
|
+
if (portable.hash !== "") throw new TypeError(`The portable actor ID ${actorId.href} must not have a fragment.`);
|
|
124
|
+
const base = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(portable, gateway);
|
|
125
|
+
base.search = "";
|
|
126
|
+
base.hash = "";
|
|
127
|
+
return base;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Tells whether a parsed document is a portable actor, i.e., an actor whose
|
|
131
|
+
* ID is an `ap:` or `ap+ef61:` URI or a compatible identifier. Such an actor
|
|
132
|
+
* is never authenticated by the web origin that served it; at a compatible
|
|
133
|
+
* key ID, it makes the key a gateway key that only
|
|
134
|
+
* {@link verifyPortableGatewayKeyDocument} can vouch for.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
function isPortableActorDocument(object) {
|
|
138
|
+
return (0, _fedify_vocab.isActor)(object) && object.id != null && isPortableId(object.id);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Compares two public keys by their key material.
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
async function isSamePublicKey(a, b) {
|
|
145
|
+
if (a.algorithm.name !== b.algorithm.name) return false;
|
|
146
|
+
const [x, y] = await Promise.all([crypto.subtle.exportKey("jwk", a), crypto.subtle.exportKey("jwk", b)]);
|
|
147
|
+
return x.kty === y.kty && x.crv === y.crv && x.n === y.n && x.e === y.e && x.x === y.x && x.y === y.y;
|
|
148
|
+
}
|
|
19
149
|
//#endregion
|
|
20
150
|
//#region ../../node_modules/.pnpm/temporal-polyfill@1.0.1/node_modules/temporal-polyfill/chunks/root.js
|
|
21
151
|
const NativeTemporal = globalThis.Temporal;
|
|
@@ -3739,440 +3869,44 @@ const preloadedOnlyDocumentLoader = (url) => {
|
|
|
3739
3869
|
return Promise.reject(/* @__PURE__ */ new Error("Refusing to fetch a non-preloaded JSON-LD context: " + url));
|
|
3740
3870
|
};
|
|
3741
3871
|
//#endregion
|
|
3742
|
-
//#region src/
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
"to",
|
|
3781
|
-
"cc",
|
|
3782
|
-
"bto",
|
|
3783
|
-
"bcc",
|
|
3784
|
-
"audience"
|
|
3785
|
-
]);
|
|
3786
|
-
const AS_CONTEXT_URL$1 = "https://www.w3.org/ns/activitystreams";
|
|
3787
|
-
const MAX_TRAVERSAL_DEPTH$1 = 64;
|
|
3788
|
-
const KNOWN_SAFE_CONTEXT_URLS$1 = new Set(Object.keys(_fedify_vocab_runtime.preloadedContexts));
|
|
3789
|
-
function hasPublicCurieInAddressing(value, preserveSecured, parentKey, depth = 0) {
|
|
3790
|
-
if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
|
|
3791
|
-
if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
|
|
3792
|
-
if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, preserveSecured, parentKey, depth + 1));
|
|
3793
|
-
if (typeof value !== "object" || value == null) return false;
|
|
3794
|
-
if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return false;
|
|
3795
|
-
const record = value;
|
|
3796
|
-
for (const key of Object.keys(record)) {
|
|
3797
|
-
if (record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public")) return true;
|
|
3798
|
-
if (key === "@context") continue;
|
|
3799
|
-
if (hasPublicCurieInAddressing(record[key], preserveSecured, key, depth + 1)) return true;
|
|
3800
|
-
}
|
|
3801
|
-
return false;
|
|
3802
|
-
}
|
|
3803
|
-
function rewritePublicAudience(value, preserveSecured, parentKey, depth = 0) {
|
|
3804
|
-
if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return _fedify_vocab.PUBLIC_COLLECTION.href;
|
|
3805
|
-
if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
|
|
3806
|
-
if (Array.isArray(value)) {
|
|
3807
|
-
let changed = false;
|
|
3808
|
-
const mapped = value.map((item) => {
|
|
3809
|
-
const rewritten = rewritePublicAudience(item, preserveSecured, parentKey, depth + 1);
|
|
3810
|
-
if (rewritten !== item) changed = true;
|
|
3811
|
-
return rewritten;
|
|
3812
|
-
});
|
|
3813
|
-
return changed ? mapped : value;
|
|
3814
|
-
}
|
|
3815
|
-
if (typeof value !== "object" || value == null) return value;
|
|
3816
|
-
if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return value;
|
|
3817
|
-
const record = value;
|
|
3818
|
-
let changed = false;
|
|
3819
|
-
const normalized = Object.create(null);
|
|
3820
|
-
for (const key of Object.keys(record)) {
|
|
3821
|
-
const rewritten = key === "@context" ? record[key] : record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public") ? _fedify_vocab.PUBLIC_COLLECTION.href : rewritePublicAudience(record[key], preserveSecured, key, depth + 1);
|
|
3822
|
-
if (rewritten !== record[key]) changed = true;
|
|
3823
|
-
normalized[key] = rewritten;
|
|
3824
|
-
}
|
|
3825
|
-
return changed ? normalized : value;
|
|
3826
|
-
}
|
|
3827
|
-
/**
|
|
3828
|
-
* Reports whether `value` carries an `@context` property anywhere inside
|
|
3829
|
-
* its subtree (not counting the value itself). A nested `@context` can
|
|
3830
|
-
* introduce a local term-definition scope that redefines `as:` or `Public`
|
|
3831
|
-
* even when the top-level `@context` is safe, so the fast path must defer
|
|
3832
|
-
* to the URDNA2015 equivalence check whenever one is present.
|
|
3833
|
-
*/
|
|
3834
|
-
function hasNestedContext$1(value, depth = 0) {
|
|
3835
|
-
if (depth >= MAX_TRAVERSAL_DEPTH$1) return true;
|
|
3836
|
-
if (Array.isArray(value)) return value.some((item) => hasNestedContext$1(item, depth + 1));
|
|
3837
|
-
if (typeof value !== "object" || value == null) return false;
|
|
3838
|
-
const record = value;
|
|
3839
|
-
for (const key of Object.keys(record)) {
|
|
3840
|
-
if (key === "@context") return true;
|
|
3841
|
-
if (hasNestedContext$1(record[key], depth + 1)) return true;
|
|
3842
|
-
}
|
|
3843
|
-
return false;
|
|
3844
|
-
}
|
|
3845
|
-
/**
|
|
3846
|
-
* Checks whether the `@context` of a JSON-LD document is guaranteed not
|
|
3847
|
-
* to redefine the `as:` prefix or the bare `Public` term. Only documents
|
|
3848
|
-
* whose `@context` is a string, or an array of strings, drawn from Fedify's
|
|
3849
|
-
* preloaded context set AND including the ActivityStreams URL qualify,
|
|
3850
|
-
* AND no nested subtree carries its own `@context` that might redefine
|
|
3851
|
-
* those terms within a local scope. When all of that holds the rewrite
|
|
3852
|
-
* is provably semantics-preserving and the URDNA2015 equivalence check
|
|
3853
|
-
* can be skipped. Any other shape (unknown external URLs, inline
|
|
3854
|
-
* objects at the top level, nested `@context` blocks) is treated as
|
|
3855
|
-
* potentially unsafe.
|
|
3856
|
-
*/
|
|
3857
|
-
function hasKnownSafeContext$1(jsonLd) {
|
|
3858
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
3859
|
-
const record = jsonLd;
|
|
3860
|
-
if (!Object.hasOwn(record, "@context")) return false;
|
|
3861
|
-
const ctx = record["@context"];
|
|
3862
|
-
const entries = typeof ctx === "string" ? [ctx] : Array.isArray(ctx) ? ctx : null;
|
|
3863
|
-
if (entries == null || entries.length === 0) return false;
|
|
3864
|
-
let hasAs = false;
|
|
3865
|
-
for (const entry of entries) {
|
|
3866
|
-
if (typeof entry !== "string") return false;
|
|
3867
|
-
if (!KNOWN_SAFE_CONTEXT_URLS$1.has(entry)) return false;
|
|
3868
|
-
if (entry === AS_CONTEXT_URL$1) hasAs = true;
|
|
3869
|
-
}
|
|
3870
|
-
if (!hasAs) return false;
|
|
3871
|
-
for (const key of Object.keys(record)) {
|
|
3872
|
-
if (key === "@context") continue;
|
|
3873
|
-
if (hasNestedContext$1(record[key])) return false;
|
|
3874
|
-
}
|
|
3875
|
-
return true;
|
|
3876
|
-
}
|
|
3877
|
-
/**
|
|
3878
|
-
* Rewrites the compact `as:Public` / `Public` CURIE appearing in activity
|
|
3879
|
-
* addressing fields (`to`, `cc`, `bto`, `bcc`, `audience`) to the fully
|
|
3880
|
-
* expanded `https://www.w3.org/ns/activitystreams#Public` URI.
|
|
3881
|
-
*
|
|
3882
|
-
* Several ActivityPub implementations, Lemmy among them, match these
|
|
3883
|
-
* fields as plain URLs without running JSON-LD expansion, and silently
|
|
3884
|
-
* drop activities whose public addressing appears in CURIE form. This
|
|
3885
|
-
* helper works around that gap.
|
|
3886
|
-
*
|
|
3887
|
-
* For documents whose `@context` is drawn entirely from Fedify's
|
|
3888
|
-
* preloaded context set and includes the ActivityStreams URL, the
|
|
3889
|
-
* rewrite is applied directly: the content of every preloaded non-AS
|
|
3890
|
-
* context is known not to redefine the `as:` prefix or the bare `Public`
|
|
3891
|
-
* term, so the semantics are preserved by construction. Any other
|
|
3892
|
-
* shape (an inline object, an unknown external URL, and so on) is
|
|
3893
|
-
* treated as potentially unsafe and gated on a JSON-LD equivalence
|
|
3894
|
-
* check; both forms are canonicalized with URDNA2015 and the resulting
|
|
3895
|
-
* N-Quads are compared. When they differ, the original document is
|
|
3896
|
-
* returned unchanged. Canonicalization failures also fall back to the
|
|
3897
|
-
* original document.
|
|
3898
|
-
*
|
|
3899
|
-
* When no `contextLoader` is supplied the helper falls back to an
|
|
3900
|
-
* internal loader that resolves only the URLs in Fedify's
|
|
3901
|
-
* preloaded-contexts set and rejects every other URL without issuing a
|
|
3902
|
-
* network request. That behaviour is deliberately narrower than
|
|
3903
|
-
* `@fedify/vocab-runtime`'s `getDocumentLoader()`, which after its
|
|
3904
|
-
* `validatePublicUrl` check will happily fetch non-preloaded URLs: the
|
|
3905
|
-
* helper is reached from verification paths (`verifyProof()` /
|
|
3906
|
-
* `verifyObject()`) that operate on inbound, potentially adversarial
|
|
3907
|
-
* JSON-LD, and a default loader that fetches attacker-supplied
|
|
3908
|
-
* `@context` URLs on the caller's behalf would be an SSRF vector.
|
|
3909
|
-
* Canonicalization failures against the restricted loader fall back to
|
|
3910
|
-
* the original document, same as any other canonicalization error.
|
|
3911
|
-
* Callers that genuinely need the remote-fetch loader (for example
|
|
3912
|
-
* applications that sign local JSON-LD against a custom vocabulary)
|
|
3913
|
-
* should pass a `contextLoader` explicitly.
|
|
3914
|
-
*
|
|
3915
|
-
* Must be called before any signing step that canonicalizes the
|
|
3916
|
-
* compact form byte-for-byte (for example, Object Integrity Proofs
|
|
3917
|
-
* using the `eddsa-jcs-2022` cryptosuite), so the signed payload
|
|
3918
|
-
* matches what is sent on the wire.
|
|
3919
|
-
*/
|
|
3920
|
-
async function normalizePublicAudience(jsonLd, contextLoader, options = {}) {
|
|
3921
|
-
const preserveSecured = options.preserveNestedSecuredDocuments ?? false;
|
|
3922
|
-
if (!hasPublicCurieInAddressing(jsonLd, preserveSecured)) return jsonLd;
|
|
3923
|
-
const normalized = rewritePublicAudience(jsonLd, preserveSecured);
|
|
3924
|
-
if (hasKnownSafeContext$1(jsonLd)) return normalized;
|
|
3925
|
-
const loader = contextLoader ?? preloadedOnlyDocumentLoader;
|
|
3926
|
-
try {
|
|
3927
|
-
const [before, after] = await Promise.all([_fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
|
|
3928
|
-
format: "application/n-quads",
|
|
3929
|
-
documentLoader: loader
|
|
3930
|
-
}), _fedify_vocab_runtime_jsonld.default.canonize(normalized, {
|
|
3931
|
-
format: "application/n-quads",
|
|
3932
|
-
documentLoader: loader
|
|
3933
|
-
})]);
|
|
3934
|
-
if (before === after) return normalized;
|
|
3935
|
-
logger$4.warn("Expanding the public audience CURIE to its full URI would change the canonical form of the activity; sending the activity as is. This usually means the active JSON-LD context redefines the `as:` prefix or the bare `Public` term.");
|
|
3936
|
-
} catch (error) {
|
|
3937
|
-
logger$4.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { error });
|
|
3938
|
-
}
|
|
3939
|
-
return jsonLd;
|
|
3940
|
-
}
|
|
3941
|
-
//#endregion
|
|
3942
|
-
//#region src/compat/outgoing-jsonld.ts
|
|
3943
|
-
const logger$3 = (0, _logtape_logtape.getLogger)([
|
|
3944
|
-
"fedify",
|
|
3945
|
-
"compat",
|
|
3946
|
-
"outgoing-jsonld"
|
|
3947
|
-
]);
|
|
3948
|
-
const ATTACHMENT_FIELDS = /* @__PURE__ */ new Set(["attachment", "https://www.w3.org/ns/activitystreams#attachment"]);
|
|
3949
|
-
const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
|
|
3950
|
-
const KNOWN_SAFE_CONTEXT_URLS = getKnownSafeContextUrls();
|
|
3951
|
-
const MAX_TRAVERSAL_DEPTH = 64;
|
|
3952
|
-
function isJsonLdListObject(value) {
|
|
3953
|
-
return typeof value === "object" && value != null && Object.hasOwn(value, "@list");
|
|
3954
|
-
}
|
|
3955
|
-
function isJsonLdValueObject(value) {
|
|
3956
|
-
return typeof value === "object" && value != null && Object.hasOwn(value, "@value");
|
|
3957
|
-
}
|
|
3958
|
-
function* getContextObjects(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
3959
|
-
if (Array.isArray(value)) {
|
|
3960
|
-
if (seen.has(value)) return;
|
|
3961
|
-
seen.add(value);
|
|
3962
|
-
for (const item of value) yield* getContextObjects(item, seen);
|
|
3963
|
-
return;
|
|
3964
|
-
}
|
|
3965
|
-
if (typeof value === "object" && value != null) {
|
|
3966
|
-
if (seen.has(value)) return;
|
|
3967
|
-
seen.add(value);
|
|
3968
|
-
const record = value;
|
|
3969
|
-
yield record;
|
|
3970
|
-
for (const definition of Object.values(record)) {
|
|
3971
|
-
if (typeof definition !== "object" || definition == null) continue;
|
|
3972
|
-
const nestedContext = definition["@context"];
|
|
3973
|
-
if (nestedContext == null) continue;
|
|
3974
|
-
yield* getContextObjects(nestedContext, seen);
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
}
|
|
3978
|
-
function isActivityStreamsAttachmentTerm(value) {
|
|
3979
|
-
return typeof value === "object" && value != null && value["@id"] === "as:attachment" && value["@type"] === "@id";
|
|
3980
|
-
}
|
|
3981
|
-
/** @internal */
|
|
3982
|
-
function isPreloadedContextAttachmentSafe(document) {
|
|
3983
|
-
if (typeof document !== "object" || document == null) return true;
|
|
3984
|
-
const context = document["@context"];
|
|
3985
|
-
for (const contextObject of getContextObjects(context)) {
|
|
3986
|
-
if (!Object.hasOwn(contextObject, "attachment")) continue;
|
|
3987
|
-
if (isActivityStreamsAttachmentTerm(contextObject.attachment)) continue;
|
|
3988
|
-
return false;
|
|
3989
|
-
}
|
|
3990
|
-
return true;
|
|
3991
|
-
}
|
|
3992
|
-
function getKnownSafeContextUrls() {
|
|
3993
|
-
const urls = /* @__PURE__ */ new Set();
|
|
3994
|
-
for (const [url, document] of Object.entries(_fedify_vocab_runtime.preloadedContexts)) if (isPreloadedContextAttachmentSafe(document)) urls.add(url);
|
|
3995
|
-
else logger$3.warn("Preloaded JSON-LD context {contextUrl} redefines the `attachment` term incompatibly; attachment array normalization will require canonicalization for documents using it.", { contextUrl: url });
|
|
3996
|
-
return urls;
|
|
3997
|
-
}
|
|
3998
|
-
/**
|
|
3999
|
-
* Wraps scalar ActivityStreams attachment properties in arrays.
|
|
4000
|
-
*/
|
|
4001
|
-
function wrapScalarAttachments(jsonLd, preserveSecured, depth = 0) {
|
|
4002
|
-
if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd;
|
|
4003
|
-
if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(jsonLd)) return jsonLd;
|
|
4004
|
-
if (Array.isArray(jsonLd)) {
|
|
4005
|
-
let normalized = null;
|
|
4006
|
-
for (let i = 0; i < jsonLd.length; i++) {
|
|
4007
|
-
const item = jsonLd[i];
|
|
4008
|
-
const next = wrapScalarAttachments(item, preserveSecured, depth + 1);
|
|
4009
|
-
if (normalized == null && next !== item) normalized = jsonLd.slice(0, i);
|
|
4010
|
-
if (normalized != null) normalized[i] = next;
|
|
4011
|
-
}
|
|
4012
|
-
return normalized ?? jsonLd;
|
|
4013
|
-
}
|
|
4014
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
|
|
4015
|
-
const record = jsonLd;
|
|
4016
|
-
const keys = Object.keys(record);
|
|
4017
|
-
let normalized = null;
|
|
4018
|
-
for (let i = 0; i < keys.length; i++) {
|
|
4019
|
-
const key = keys[i];
|
|
4020
|
-
const value = record[key];
|
|
4021
|
-
const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, preserveSecured, depth + 1);
|
|
4022
|
-
const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next;
|
|
4023
|
-
if (normalized == null && output !== value) {
|
|
4024
|
-
const cloned = Object.create(null);
|
|
4025
|
-
for (let j = 0; j < i; j++) {
|
|
4026
|
-
const previousKey = keys[j];
|
|
4027
|
-
cloned[previousKey] = record[previousKey];
|
|
4028
|
-
}
|
|
4029
|
-
normalized = cloned;
|
|
4030
|
-
}
|
|
4031
|
-
if (normalized != null) normalized[key] = output;
|
|
4032
|
-
}
|
|
4033
|
-
return normalized ?? jsonLd;
|
|
4034
|
-
}
|
|
4035
|
-
function hasNestedContext(value, depth = 0) {
|
|
4036
|
-
if (depth >= MAX_TRAVERSAL_DEPTH) return true;
|
|
4037
|
-
if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
|
|
4038
|
-
if (typeof value !== "object" || value == null) return false;
|
|
4039
|
-
const record = value;
|
|
4040
|
-
for (const key of Object.keys(record)) {
|
|
4041
|
-
if (key === "@context") return true;
|
|
4042
|
-
if (key === "@value" && isJsonLdValueObject(value)) continue;
|
|
4043
|
-
if (hasNestedContext(record[key], depth + 1)) return true;
|
|
4044
|
-
}
|
|
4045
|
-
return false;
|
|
4046
|
-
}
|
|
4047
|
-
function exceedsTraversalDepth(value, depth = 0) {
|
|
4048
|
-
if (depth >= MAX_TRAVERSAL_DEPTH) return true;
|
|
4049
|
-
if (Array.isArray(value)) return value.some((item) => exceedsTraversalDepth(item, depth + 1));
|
|
4050
|
-
if (typeof value !== "object" || value == null) return false;
|
|
4051
|
-
const record = value;
|
|
4052
|
-
for (const key of Object.keys(record)) {
|
|
4053
|
-
if (key === "@context" || key === "@value" && isJsonLdValueObject(value)) continue;
|
|
4054
|
-
if (exceedsTraversalDepth(record[key], depth + 1)) return true;
|
|
4055
|
-
}
|
|
4056
|
-
return false;
|
|
4057
|
-
}
|
|
4058
|
-
function hasKnownSafeContext(jsonLd) {
|
|
4059
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
4060
|
-
const record = jsonLd;
|
|
4061
|
-
if (!Object.hasOwn(record, "@context")) return false;
|
|
4062
|
-
const context = record["@context"];
|
|
4063
|
-
const entries = typeof context === "string" ? [context] : Array.isArray(context) ? context : null;
|
|
4064
|
-
if (entries == null || entries.length < 1) return false;
|
|
4065
|
-
let hasActivityStreamsContext = false;
|
|
4066
|
-
for (const entry of entries) {
|
|
4067
|
-
if (typeof entry !== "string") return false;
|
|
4068
|
-
if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
|
|
4069
|
-
if (entry === AS_CONTEXT_URL) hasActivityStreamsContext = true;
|
|
4070
|
-
}
|
|
4071
|
-
if (!hasActivityStreamsContext) return false;
|
|
4072
|
-
for (const key of Object.keys(record)) {
|
|
4073
|
-
if (key === "@context") continue;
|
|
4074
|
-
if (hasNestedContext(record[key])) return false;
|
|
4075
|
-
}
|
|
4076
|
-
return true;
|
|
4077
|
-
}
|
|
4078
|
-
function getLogSafeJsonLdMetadata(jsonLd) {
|
|
4079
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return {};
|
|
4080
|
-
const record = jsonLd;
|
|
4081
|
-
const context = record["@context"];
|
|
4082
|
-
return {
|
|
4083
|
-
id: typeof record.id === "string" ? record.id : typeof record["@id"] === "string" ? record["@id"] : void 0,
|
|
4084
|
-
type: typeof record.type === "string" ? record.type : typeof record["@type"] === "string" ? record["@type"] : void 0,
|
|
4085
|
-
context: typeof context === "string" ? context : Array.isArray(context) ? context.filter((entry) => typeof entry === "string").slice(0, 4) : context == null ? void 0 : "[inline context]"
|
|
4086
|
-
};
|
|
4087
|
-
}
|
|
4088
|
-
/**
|
|
4089
|
-
* Ensures ActivityStreams attachment properties are represented as arrays
|
|
4090
|
-
* when doing so preserves the JSON-LD semantics.
|
|
4091
|
-
*
|
|
4092
|
-
* JSON-LD compaction collapses single-item arrays into scalar values by
|
|
4093
|
-
* default. Some ActivityPub implementations, Pixelfed among them, parse
|
|
4094
|
-
* `attachment` as a plain JSON array rather than a JSON-LD property and reject
|
|
4095
|
-
* otherwise valid objects whose single attachment is emitted as a scalar.
|
|
4096
|
-
*
|
|
4097
|
-
* When no `contextLoader` is supplied, the helper falls back to a restricted
|
|
4098
|
-
* loader that resolves only Fedify's preloaded JSON-LD contexts and rejects
|
|
4099
|
-
* every other URL without network access. Documents with custom, inline, or
|
|
4100
|
-
* otherwise uncached contexts should pass a real `contextLoader` if they need
|
|
4101
|
-
* the semantic-preservation check to succeed; otherwise canonicalization
|
|
4102
|
-
* failures leave the original document unchanged.
|
|
4103
|
-
*/
|
|
4104
|
-
async function normalizeAttachmentArrays(jsonLd, contextLoader, options = {}) {
|
|
4105
|
-
const normalized = wrapScalarAttachments(jsonLd, options.preserveNestedSecuredDocuments ?? false);
|
|
4106
|
-
if (normalized === jsonLd) return jsonLd;
|
|
4107
|
-
if (exceedsTraversalDepth(jsonLd)) {
|
|
4108
|
-
logger$3.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged.");
|
|
4109
|
-
return jsonLd;
|
|
4110
|
-
}
|
|
4111
|
-
if (hasKnownSafeContext(jsonLd)) return normalized;
|
|
4112
|
-
const loader = contextLoader ?? preloadedOnlyDocumentLoader;
|
|
4113
|
-
try {
|
|
4114
|
-
const [before, after] = await Promise.all([_fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
|
|
4115
|
-
format: "application/n-quads",
|
|
4116
|
-
documentLoader: loader
|
|
4117
|
-
}), _fedify_vocab_runtime_jsonld.default.canonize(normalized, {
|
|
4118
|
-
format: "application/n-quads",
|
|
4119
|
-
documentLoader: loader
|
|
4120
|
-
})]);
|
|
4121
|
-
if (before === after) return normalized;
|
|
4122
|
-
logger$3.warn("Wrapping scalar attachment values in arrays would change the canonical form of the JSON-LD document; leaving it unchanged. This usually means the active JSON-LD context redefines the `attachment` term. Document: {id}; type: {type}; context: {context}.", getLogSafeJsonLdMetadata(jsonLd));
|
|
4123
|
-
} catch (error) {
|
|
4124
|
-
logger$3.debug("Failed to verify attachment array normalization equivalence via JSON-LD canonicalization; leaving the JSON-LD document unchanged.\n{error}", { error });
|
|
4125
|
-
}
|
|
4126
|
-
return jsonLd;
|
|
4127
|
-
}
|
|
4128
|
-
/**
|
|
4129
|
-
* Applies Fedify's internal JSON-LD wire-format interoperability workarounds
|
|
4130
|
-
* to locally generated outgoing activities before they are signed, enqueued,
|
|
4131
|
-
* or sent.
|
|
4132
|
-
*/
|
|
4133
|
-
async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, options = {}) {
|
|
4134
|
-
jsonLd = await normalizePublicAudience(jsonLd, contextLoader, options);
|
|
4135
|
-
return await normalizeAttachmentArrays(jsonLd, contextLoader, options);
|
|
4136
|
-
}
|
|
4137
|
-
//#endregion
|
|
4138
|
-
//#region src/federation/metrics.ts
|
|
4139
|
-
var FederationMetrics = class {
|
|
4140
|
-
deliverySent;
|
|
4141
|
-
deliveryPermanentFailure;
|
|
4142
|
-
signatureVerificationFailure;
|
|
4143
|
-
signatureVerificationDuration;
|
|
4144
|
-
signatureKeyFetchDuration;
|
|
4145
|
-
deliveryDuration;
|
|
4146
|
-
inboxProcessingDuration;
|
|
4147
|
-
httpServerRequestCount;
|
|
4148
|
-
httpServerRequestDuration;
|
|
4149
|
-
queueTaskEnqueued;
|
|
4150
|
-
queueTaskStarted;
|
|
4151
|
-
queueTaskCompleted;
|
|
4152
|
-
queueTaskFailed;
|
|
4153
|
-
queueTaskDuration;
|
|
4154
|
-
queueTaskInFlight;
|
|
4155
|
-
queueDepth;
|
|
4156
|
-
fanoutRecipients;
|
|
4157
|
-
inboxActivity;
|
|
4158
|
-
outboxActivity;
|
|
4159
|
-
circuitBreakerStateChange;
|
|
4160
|
-
keyLookup;
|
|
4161
|
-
keyLookupDuration;
|
|
4162
|
-
documentFetch;
|
|
4163
|
-
documentFetchDuration;
|
|
4164
|
-
documentCache;
|
|
4165
|
-
webFingerHandle;
|
|
4166
|
-
webFingerHandleDuration;
|
|
4167
|
-
collectionRequest;
|
|
4168
|
-
collectionDispatchDuration;
|
|
4169
|
-
collectionPageItems;
|
|
4170
|
-
collectionTotalItems;
|
|
4171
|
-
constructor(meterProvider) {
|
|
4172
|
-
const meter = meterProvider.getMeter(name, version);
|
|
4173
|
-
this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
|
|
4174
|
-
description: "ActivityPub delivery attempts.",
|
|
4175
|
-
unit: "{attempt}"
|
|
3872
|
+
//#region src/federation/metrics.ts
|
|
3873
|
+
var FederationMetrics = class {
|
|
3874
|
+
deliverySent;
|
|
3875
|
+
deliveryPermanentFailure;
|
|
3876
|
+
signatureVerificationFailure;
|
|
3877
|
+
signatureVerificationDuration;
|
|
3878
|
+
signatureKeyFetchDuration;
|
|
3879
|
+
deliveryDuration;
|
|
3880
|
+
inboxProcessingDuration;
|
|
3881
|
+
httpServerRequestCount;
|
|
3882
|
+
httpServerRequestDuration;
|
|
3883
|
+
queueTaskEnqueued;
|
|
3884
|
+
queueTaskStarted;
|
|
3885
|
+
queueTaskCompleted;
|
|
3886
|
+
queueTaskFailed;
|
|
3887
|
+
queueTaskDuration;
|
|
3888
|
+
queueTaskInFlight;
|
|
3889
|
+
queueDepth;
|
|
3890
|
+
fanoutRecipients;
|
|
3891
|
+
inboxActivity;
|
|
3892
|
+
outboxActivity;
|
|
3893
|
+
circuitBreakerStateChange;
|
|
3894
|
+
keyLookup;
|
|
3895
|
+
keyLookupDuration;
|
|
3896
|
+
documentFetch;
|
|
3897
|
+
documentFetchDuration;
|
|
3898
|
+
documentCache;
|
|
3899
|
+
webFingerHandle;
|
|
3900
|
+
webFingerHandleDuration;
|
|
3901
|
+
collectionRequest;
|
|
3902
|
+
collectionDispatchDuration;
|
|
3903
|
+
collectionPageItems;
|
|
3904
|
+
collectionTotalItems;
|
|
3905
|
+
constructor(meterProvider) {
|
|
3906
|
+
const meter = meterProvider.getMeter(name, version);
|
|
3907
|
+
this.deliverySent = meter.createCounter("activitypub.delivery.sent", {
|
|
3908
|
+
description: "ActivityPub delivery attempts.",
|
|
3909
|
+
unit: "{attempt}"
|
|
4176
3910
|
});
|
|
4177
3911
|
this.deliveryPermanentFailure = meter.createCounter("activitypub.delivery.permanent_failure", {
|
|
4178
3912
|
description: "ActivityPub deliveries abandoned as permanent failures.",
|
|
@@ -4940,102 +4674,9 @@ function getRemoteHost(url) {
|
|
|
4940
4674
|
/**
|
|
4941
4675
|
* Gets an elapsed duration in milliseconds from a `performance.now()` value.
|
|
4942
4676
|
* @since 2.3.0
|
|
4943
|
-
*/
|
|
4944
|
-
function getDurationMs(start) {
|
|
4945
|
-
return Math.max(0, performance.now() - start);
|
|
4946
|
-
}
|
|
4947
|
-
//#endregion
|
|
4948
|
-
//#region src/sig/portable-key-id.ts
|
|
4949
|
-
const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
|
|
4950
|
-
/**
|
|
4951
|
-
* Checks whether the URL is an `ap:` or `ap+ef61:` URI.
|
|
4952
|
-
* @internal
|
|
4953
|
-
*/
|
|
4954
|
-
function isPortableUri(url) {
|
|
4955
|
-
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
4956
|
-
}
|
|
4957
|
-
/**
|
|
4958
|
-
* Checks whether an activity is performed by an FEP-ef61 portable actor,
|
|
4959
|
-
* i.e., any of its actors has an `ap:` or `ap+ef61:` ID.
|
|
4960
|
-
* @internal
|
|
4961
|
-
*/
|
|
4962
|
-
function hasPortableActor(activity) {
|
|
4963
|
-
return activity.actorIds.some(isPortableUri);
|
|
4964
|
-
}
|
|
4965
|
-
/**
|
|
4966
|
-
* Checks whether a key ID looks like an [FEP-ef61] compatible identifier,
|
|
4967
|
-
* i.e., an HTTP(S) URL under a gateway's `/.well-known/apgateway/did:` path.
|
|
4968
|
-
* Such a key ID names a key that a gateway holds for a portable actor, so it
|
|
4969
|
-
* is resolved only as a gateway key, and never cached in a key cache.
|
|
4970
|
-
*
|
|
4971
|
-
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
4972
|
-
* @internal
|
|
4973
|
-
*/
|
|
4974
|
-
function isCompatibleKeyId(keyId) {
|
|
4975
|
-
return (keyId.protocol === "http:" || keyId.protocol === "https:") && COMPATIBLE_KEY_ID_PATH_PATTERN.test(keyId.pathname);
|
|
4976
|
-
}
|
|
4977
|
-
/**
|
|
4978
|
-
* Gets the canonical portable ID of an actor ID that is either an `ap:` or
|
|
4979
|
-
* `ap+ef61:` URI or a compatible identifier.
|
|
4980
|
-
* @returns The canonical portable ID, or `null` if the ID is neither, or is
|
|
4981
|
-
* malformed.
|
|
4982
|
-
* @internal
|
|
4983
|
-
*/
|
|
4984
|
-
function getCanonicalPortableId(id) {
|
|
4985
|
-
try {
|
|
4986
|
-
if (isPortableUri(id)) return (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)(id));
|
|
4987
|
-
const portable = (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
|
|
4988
|
-
if (portable == null) return null;
|
|
4989
|
-
return (0, _fedify_vocab_runtime.canonicalizePortableUri)((0, _fedify_vocab_runtime.formatIri)(portable));
|
|
4990
|
-
} catch (error) {
|
|
4991
|
-
if (error instanceof TypeError) return null;
|
|
4992
|
-
throw error;
|
|
4993
|
-
}
|
|
4994
|
-
}
|
|
4995
|
-
/**
|
|
4996
|
-
* Builds the base of the key IDs of a gateway's keys for a portable actor:
|
|
4997
|
-
* the actor's compatible identifier on the gateway.
|
|
4998
|
-
* @param actorId The portable actor's ID, either an `ap:` or `ap+ef61:` URI
|
|
4999
|
-
* or a compatible identifier. It must not have a fragment.
|
|
5000
|
-
* @param gateway The gateway's origin, e.g., `https://example.com`.
|
|
5001
|
-
* @returns The compatible identifier without query and fragment.
|
|
5002
|
-
* @throws {TypeError} If the actor ID is not a portable ID, or has
|
|
5003
|
-
* a fragment.
|
|
5004
|
-
* @internal
|
|
5005
|
-
*/
|
|
5006
|
-
function getGatewayKeyBase(actorId, gateway) {
|
|
5007
|
-
let portable;
|
|
5008
|
-
if (isPortableUri(actorId)) portable = actorId;
|
|
5009
|
-
else try {
|
|
5010
|
-
portable = (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(actorId);
|
|
5011
|
-
} catch (error) {
|
|
5012
|
-
if (!(error instanceof TypeError)) throw error;
|
|
5013
|
-
portable = null;
|
|
5014
|
-
}
|
|
5015
|
-
if (portable == null) throw new TypeError(`The portable actor ID ${actorId.href} must be an ap: or ap+ef61: URI, or an FEP-ef61 compatible identifier.`);
|
|
5016
|
-
if (portable.hash !== "") throw new TypeError(`The portable actor ID ${actorId.href} must not have a fragment.`);
|
|
5017
|
-
const base = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(portable, gateway);
|
|
5018
|
-
base.search = "";
|
|
5019
|
-
base.hash = "";
|
|
5020
|
-
return base;
|
|
5021
|
-
}
|
|
5022
|
-
/**
|
|
5023
|
-
* Tells whether a parsed document at a compatible key ID is a portable actor,
|
|
5024
|
-
* which makes the key a gateway key that only
|
|
5025
|
-
* {@link verifyPortableGatewayKeyDocument} can vouch for.
|
|
5026
|
-
* @internal
|
|
5027
|
-
*/
|
|
5028
|
-
function isPortableActorDocument(object) {
|
|
5029
|
-
return (0, _fedify_vocab.isActor)(object) && object.id != null && isPortableUri(object.id);
|
|
5030
|
-
}
|
|
5031
|
-
/**
|
|
5032
|
-
* Compares two public keys by their key material.
|
|
5033
|
-
* @internal
|
|
5034
|
-
*/
|
|
5035
|
-
async function isSamePublicKey(a, b) {
|
|
5036
|
-
if (a.algorithm.name !== b.algorithm.name) return false;
|
|
5037
|
-
const [x, y] = await Promise.all([crypto.subtle.exportKey("jwk", a), crypto.subtle.exportKey("jwk", b)]);
|
|
5038
|
-
return x.kty === y.kty && x.crv === y.crv && x.n === y.n && x.e === y.e && x.x === y.x && x.y === y.y;
|
|
4677
|
+
*/
|
|
4678
|
+
function getDurationMs(start) {
|
|
4679
|
+
return Math.max(0, performance.now() - start);
|
|
5039
4680
|
}
|
|
5040
4681
|
//#endregion
|
|
5041
4682
|
//#region src/sig/key.ts
|
|
@@ -5138,6 +4779,10 @@ async function fetchActorDocument(actorId, options = {}) {
|
|
|
5138
4779
|
"sig",
|
|
5139
4780
|
"key"
|
|
5140
4781
|
]);
|
|
4782
|
+
if (isPortableId(actorId)) {
|
|
4783
|
+
logger.debug("The actor {actorId} is a portable actor, which is not authenticated by the web origin that serves it.", { actorId: actorId.href });
|
|
4784
|
+
return null;
|
|
4785
|
+
}
|
|
5141
4786
|
const documentLoader = options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
5142
4787
|
const contextLoader = options.contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
5143
4788
|
const { tracerProvider } = options;
|
|
@@ -5171,6 +4816,13 @@ async function fetchActorDocument(actorId, options = {}) {
|
|
|
5171
4816
|
return null;
|
|
5172
4817
|
}
|
|
5173
4818
|
if (!(0, _fedify_vocab.isActor)(object)) return null;
|
|
4819
|
+
if (object.id != null && isPortableId(object.id)) {
|
|
4820
|
+
logger.debug("The document served at {documentUrl} claims to be the portable actor {actorId}, which is not authenticated by the web origin that serves it.", {
|
|
4821
|
+
documentUrl: documentUrl.href,
|
|
4822
|
+
actorId: object.id?.href
|
|
4823
|
+
});
|
|
4824
|
+
return null;
|
|
4825
|
+
}
|
|
5174
4826
|
if (object.id == null || object.id.origin !== documentUrl.origin) {
|
|
5175
4827
|
logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
|
|
5176
4828
|
documentUrl: documentUrl.href,
|
|
@@ -5505,7 +5157,7 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
|
|
|
5505
5157
|
let ownerDocument = null;
|
|
5506
5158
|
if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
|
|
5507
5159
|
else if ((0, _fedify_vocab.isActor)(object)) {
|
|
5508
|
-
if (object.id == null || object.id.origin !== documentUrl.origin) {
|
|
5160
|
+
if (object.id == null || isPortableId(object.id) || object.id.origin !== documentUrl.origin) {
|
|
5509
5161
|
logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
|
|
5510
5162
|
keyId,
|
|
5511
5163
|
documentUrl: documentUrl.href,
|
|
@@ -5581,6 +5233,18 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
|
|
|
5581
5233
|
}
|
|
5582
5234
|
const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
|
|
5583
5235
|
if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
|
|
5236
|
+
if (claimedOwnerId != null && isPortableId(claimedOwnerId)) {
|
|
5237
|
+
logger.debug("Failed to verify; key {keyId} claims the portable actor {claimedOwnerId} as its owner, but is not its gateway key.", {
|
|
5238
|
+
keyId,
|
|
5239
|
+
claimedOwnerId: claimedOwnerId.href
|
|
5240
|
+
});
|
|
5241
|
+
await keyCache?.set(cacheKey, null);
|
|
5242
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5243
|
+
return {
|
|
5244
|
+
key: null,
|
|
5245
|
+
cached: false
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5584
5248
|
if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
|
|
5585
5249
|
else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
|
|
5586
5250
|
if (await verifyKeyOwnership(key, {
|
|
@@ -5610,6 +5274,15 @@ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
|
|
|
5610
5274
|
cached: false
|
|
5611
5275
|
};
|
|
5612
5276
|
}
|
|
5277
|
+
/**
|
|
5278
|
+
* Tells whether a key claims a portable actor as its owner. Such a key can
|
|
5279
|
+
* only be a gateway key, which is never cached, so a cached one was cached
|
|
5280
|
+
* by an older version that trusted web origins, and is fetched again.
|
|
5281
|
+
*/
|
|
5282
|
+
function claimsPortableOwner(key) {
|
|
5283
|
+
const ownerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key?.controllerId;
|
|
5284
|
+
return ownerId != null && isPortableId(ownerId);
|
|
5285
|
+
}
|
|
5613
5286
|
async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
|
|
5614
5287
|
const start = performance.now();
|
|
5615
5288
|
let outcome = { result: "error" };
|
|
@@ -5636,7 +5309,7 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
|
|
|
5636
5309
|
outcome = { result: "hit" };
|
|
5637
5310
|
return cachedUnavailable;
|
|
5638
5311
|
}
|
|
5639
|
-
if (cached != null) {
|
|
5312
|
+
if (cached != null && !claimsPortableOwner(cached.key)) {
|
|
5640
5313
|
outcome = { result: "hit" };
|
|
5641
5314
|
return cached;
|
|
5642
5315
|
}
|
|
@@ -5691,7 +5364,7 @@ async function fetchKeyInternal(keyId, cls, options = {}) {
|
|
|
5691
5364
|
}
|
|
5692
5365
|
//#endregion
|
|
5693
5366
|
//#region src/sig/ld.ts
|
|
5694
|
-
const logger$
|
|
5367
|
+
const logger$4 = (0, _logtape_logtape.getLogger)([
|
|
5695
5368
|
"fedify",
|
|
5696
5369
|
"sig",
|
|
5697
5370
|
"ld"
|
|
@@ -5861,445 +5534,841 @@ function getDirectContextTarget(definition) {
|
|
|
5861
5534
|
if (typeof id === "string") return id;
|
|
5862
5535
|
}
|
|
5863
5536
|
}
|
|
5864
|
-
function isJsonTypedDefinition(definition) {
|
|
5865
|
-
return typeof definition === "object" && definition != null && "@type" in definition && definition["@type"] === "@json";
|
|
5537
|
+
function isJsonTypedDefinition(definition) {
|
|
5538
|
+
return typeof definition === "object" && definition != null && "@type" in definition && definition["@type"] === "@json";
|
|
5539
|
+
}
|
|
5540
|
+
function resolveLocalContextTarget(target, state, localTargets, seen = /* @__PURE__ */ new Set()) {
|
|
5541
|
+
if (target === "@graph") return target;
|
|
5542
|
+
if (seen.has(target)) return target;
|
|
5543
|
+
seen.add(target);
|
|
5544
|
+
if (localTargets.has(target)) {
|
|
5545
|
+
const localTarget = localTargets.get(target);
|
|
5546
|
+
return localTarget == null ? target : resolveLocalContextTarget(localTarget, state, localTargets, seen);
|
|
5547
|
+
}
|
|
5548
|
+
return resolveContextTarget(target, state);
|
|
5549
|
+
}
|
|
5550
|
+
function refreshGraphAliases(state) {
|
|
5551
|
+
state.graphTerms.clear();
|
|
5552
|
+
for (const [term, target] of state.termTargets) if (target === "@graph") state.graphTerms.add(term);
|
|
5553
|
+
}
|
|
5554
|
+
function normalizeContextReference(reference, baseUrl) {
|
|
5555
|
+
if (baseUrl != null) return new URL(reference, baseUrl).href;
|
|
5556
|
+
return URL.canParse(reference) ? new URL(reference).href : reference;
|
|
5557
|
+
}
|
|
5558
|
+
/** @internal */
|
|
5559
|
+
function isInvalidUrlTypeError(error) {
|
|
5560
|
+
const code = error.code;
|
|
5561
|
+
return error instanceof TypeError && (code === "ERR_INVALID_URL" || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
|
|
5562
|
+
}
|
|
5563
|
+
async function applyGraphAliasContext(state, context, documentLoader, remoteContextCache, baseUrl = null, processingContexts = /* @__PURE__ */ new Set()) {
|
|
5564
|
+
if (context === null) return createGraphAliasContextState();
|
|
5565
|
+
let nextState = cloneGraphAliasContextState(state);
|
|
5566
|
+
if (Array.isArray(context)) {
|
|
5567
|
+
for (const item of context) nextState = await applyGraphAliasContext(nextState, item, documentLoader, remoteContextCache, baseUrl, processingContexts);
|
|
5568
|
+
return nextState;
|
|
5569
|
+
}
|
|
5570
|
+
if (typeof context === "string") {
|
|
5571
|
+
const reference = normalizeContextReference(context, baseUrl);
|
|
5572
|
+
const cacheKey = `${baseUrl ?? ""}\n${reference}`;
|
|
5573
|
+
if (processingContexts.has(cacheKey)) return nextState;
|
|
5574
|
+
processingContexts.add(cacheKey);
|
|
5575
|
+
try {
|
|
5576
|
+
let remoteContext = remoteContextCache.get(cacheKey);
|
|
5577
|
+
if (remoteContext == null) {
|
|
5578
|
+
remoteContext = (async () => {
|
|
5579
|
+
try {
|
|
5580
|
+
return getRemoteContext(await documentLoader(reference), reference);
|
|
5581
|
+
} catch (error) {
|
|
5582
|
+
if (reference === context && isInvalidUrlTypeError(error) && isClearlyMalformedContextReference(context)) throw new InvalidContextReferenceError(context);
|
|
5583
|
+
throw error;
|
|
5584
|
+
}
|
|
5585
|
+
})();
|
|
5586
|
+
remoteContextCache.set(cacheKey, remoteContext);
|
|
5587
|
+
}
|
|
5588
|
+
const loadedRemoteContext = await remoteContext;
|
|
5589
|
+
return await applyGraphAliasContext(nextState, loadedRemoteContext.context, documentLoader, remoteContextCache, loadedRemoteContext.baseUrl, processingContexts);
|
|
5590
|
+
} finally {
|
|
5591
|
+
processingContexts.delete(cacheKey);
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
if (typeof context === "object" && context != null) {
|
|
5595
|
+
if ("@import" in context && typeof context["@import"] === "string") nextState = await applyGraphAliasContext(nextState, context["@import"], documentLoader, remoteContextCache, baseUrl, processingContexts);
|
|
5596
|
+
const localTargets = /* @__PURE__ */ new Map();
|
|
5597
|
+
for (const [term, definition] of globalThis.Object.entries(context)) {
|
|
5598
|
+
if (term.startsWith("@")) continue;
|
|
5599
|
+
const target = getDirectContextTarget(definition);
|
|
5600
|
+
if (target == null) localTargets.set(term, null);
|
|
5601
|
+
else if (typeof target === "string") localTargets.set(term, target);
|
|
5602
|
+
else localTargets.delete(term);
|
|
5603
|
+
}
|
|
5604
|
+
for (const [term, definition] of globalThis.Object.entries(context)) {
|
|
5605
|
+
if (term.startsWith("@")) continue;
|
|
5606
|
+
if (localTargets.has(term)) {
|
|
5607
|
+
const directTarget = localTargets.get(term);
|
|
5608
|
+
if (directTarget == null) nextState.termTargets.set(term, null);
|
|
5609
|
+
else nextState.termTargets.set(term, resolveLocalContextTarget(directTarget, nextState, localTargets));
|
|
5610
|
+
} else nextState.termTargets.delete(term);
|
|
5611
|
+
if (typeof definition === "object" && definition != null && "@context" in definition) nextState.propertyContexts.set(term, {
|
|
5612
|
+
context: definition["@context"],
|
|
5613
|
+
baseUrl
|
|
5614
|
+
});
|
|
5615
|
+
else nextState.propertyContexts.delete(term);
|
|
5616
|
+
if (isJsonTypedDefinition(definition)) nextState.jsonTerms.add(term);
|
|
5617
|
+
else nextState.jsonTerms.delete(term);
|
|
5618
|
+
}
|
|
5619
|
+
refreshGraphAliases(nextState);
|
|
5620
|
+
}
|
|
5621
|
+
return nextState;
|
|
5622
|
+
}
|
|
5623
|
+
async function assertNoGraphBeforeCompaction(jsonLd, documentLoader, inheritedState = createGraphAliasContextState(), propertyContext, remoteContextCache = /* @__PURE__ */ new Map()) {
|
|
5624
|
+
if (Array.isArray(jsonLd)) {
|
|
5625
|
+
for (const item of jsonLd) await assertNoGraphBeforeCompaction(item, documentLoader, inheritedState, propertyContext, remoteContextCache);
|
|
5626
|
+
return;
|
|
5627
|
+
}
|
|
5628
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return;
|
|
5629
|
+
const jsonLiteralWrapper = isJsonLiteralWrapper(jsonLd);
|
|
5630
|
+
let state = inheritedState;
|
|
5631
|
+
if (propertyContext !== void 0) state = await applyGraphAliasContext(state, propertyContext.context, documentLoader, remoteContextCache, propertyContext.baseUrl);
|
|
5632
|
+
if ("@context" in jsonLd) state = await applyGraphAliasContext(state, jsonLd["@context"], documentLoader, remoteContextCache);
|
|
5633
|
+
for (const [key, value] of globalThis.Object.entries(jsonLd)) {
|
|
5634
|
+
if (key === "@context") continue;
|
|
5635
|
+
if (jsonLiteralWrapper && key === "@value") continue;
|
|
5636
|
+
if (key === "@graph" || state.graphTerms.has(key)) throw new UnsafeJsonLdError("@graph");
|
|
5637
|
+
if (state.jsonTerms.has(key)) continue;
|
|
5638
|
+
await assertNoGraphBeforeCompaction(value, documentLoader, state, state.propertyContexts.get(key), remoteContextCache);
|
|
5639
|
+
}
|
|
5640
|
+
}
|
|
5641
|
+
function isJsonLiteralWrapper(value) {
|
|
5642
|
+
return "@value" in value && (value["@type"] === "@json" || value.type === "@json");
|
|
5643
|
+
}
|
|
5644
|
+
/** @internal */
|
|
5645
|
+
function assertSafeJsonLd(jsonLd) {
|
|
5646
|
+
if (Array.isArray(jsonLd)) for (const item of jsonLd) assertSafeJsonLd(item);
|
|
5647
|
+
else if (typeof jsonLd === "object" && jsonLd != null) {
|
|
5648
|
+
const jsonLiteralWrapper = isJsonLiteralWrapper(jsonLd);
|
|
5649
|
+
for (const [key, value] of globalThis.Object.entries(jsonLd)) {
|
|
5650
|
+
if (disallowedJsonLdKeywords.has(key)) throw new UnsafeJsonLdError(key);
|
|
5651
|
+
if (jsonLiteralWrapper && key === "@value") continue;
|
|
5652
|
+
assertSafeJsonLd(value);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
/**
|
|
5657
|
+
* Attaches a LD signature to the given JSON-LD document.
|
|
5658
|
+
* @param jsonLd The JSON-LD document to attach the signature to. It is not
|
|
5659
|
+
* modified.
|
|
5660
|
+
* @param signature The signature to attach.
|
|
5661
|
+
* @returns The JSON-LD document with the attached signature.
|
|
5662
|
+
* @throws {TypeError} If the input document is not a valid JSON-LD document.
|
|
5663
|
+
* @since 1.0.0
|
|
5664
|
+
*/
|
|
5665
|
+
function attachSignature(jsonLd, signature) {
|
|
5666
|
+
if (typeof jsonLd !== "object" || jsonLd == null) throw new TypeError("Failed to attach signature; invalid JSON-LD document.");
|
|
5667
|
+
return {
|
|
5668
|
+
...jsonLd,
|
|
5669
|
+
signature
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
/**
|
|
5673
|
+
* Creates a LD signature for the given JSON-LD document.
|
|
5674
|
+
* @param jsonLd The JSON-LD document to sign.
|
|
5675
|
+
* @param privateKey The private key to sign the document.
|
|
5676
|
+
* @param keyId The ID of the public key that corresponds to the private key.
|
|
5677
|
+
* @param options Additional options for creating the signature.
|
|
5678
|
+
* See also {@link CreateSignatureOptions}.
|
|
5679
|
+
* @return The created signature.
|
|
5680
|
+
* @throws {TypeError} If the private key is invalid or unsupported.
|
|
5681
|
+
* @since 1.0.0
|
|
5682
|
+
*/
|
|
5683
|
+
async function createSignature(jsonLd, privateKey, keyId, { contextLoader, created } = {}) {
|
|
5684
|
+
validateCryptoKey(privateKey, "private");
|
|
5685
|
+
if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
|
|
5686
|
+
const options = {
|
|
5687
|
+
"@context": "https://w3id.org/identity/v1",
|
|
5688
|
+
creator: keyId.href,
|
|
5689
|
+
created: created?.toString() ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
5690
|
+
};
|
|
5691
|
+
const message = await hashJsonLd(options, contextLoader) + await hashJsonLd(jsonLd, contextLoader);
|
|
5692
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
5693
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, messageBytes);
|
|
5694
|
+
return {
|
|
5695
|
+
...options,
|
|
5696
|
+
type: "RsaSignature2017",
|
|
5697
|
+
signatureValue: (0, byte_encodings_base64.encodeBase64)(signature)
|
|
5698
|
+
};
|
|
5699
|
+
}
|
|
5700
|
+
/**
|
|
5701
|
+
* Signs the given JSON-LD document with the private key and returns the signed
|
|
5702
|
+
* JSON-LD document.
|
|
5703
|
+
* @param jsonLd The JSON-LD document to sign.
|
|
5704
|
+
* @param privateKey The private key to sign the document.
|
|
5705
|
+
* @param keyId The key ID to use in the signature. It will be used by the
|
|
5706
|
+
* verifier to fetch the corresponding public key.
|
|
5707
|
+
* @param options Additional options for signing the document.
|
|
5708
|
+
* See also {@link SignJsonLdOptions}.
|
|
5709
|
+
* @returns The signed JSON-LD document.
|
|
5710
|
+
* @throws {TypeError} If the private key is invalid or unsupported.
|
|
5711
|
+
* @since 1.0.0
|
|
5712
|
+
*/
|
|
5713
|
+
async function signJsonLd(jsonLd, privateKey, keyId, options) {
|
|
5714
|
+
return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("ld_signatures.sign", { attributes: { "ld_signatures.key_id": keyId.href } }, async (span) => {
|
|
5715
|
+
try {
|
|
5716
|
+
const signature = await createSignature(jsonLd, privateKey, keyId, options);
|
|
5717
|
+
if (span.isRecording()) {
|
|
5718
|
+
span.setAttribute("ld_signatures.type", signature.type);
|
|
5719
|
+
span.setAttribute("ld_signatures.signature", (0, byte_encodings_hex.encodeHex)((0, byte_encodings_base64.decodeBase64)(signature.signatureValue)));
|
|
5720
|
+
}
|
|
5721
|
+
return attachSignature(jsonLd, signature);
|
|
5722
|
+
} catch (error) {
|
|
5723
|
+
span.setStatus({
|
|
5724
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
5725
|
+
message: String(error)
|
|
5726
|
+
});
|
|
5727
|
+
throw error;
|
|
5728
|
+
} finally {
|
|
5729
|
+
span.end();
|
|
5730
|
+
}
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Checks if the given JSON-LD document has a Linked Data Signature-like
|
|
5735
|
+
* object, without restricting it to a single suite-specific shape.
|
|
5736
|
+
* @param jsonLd The JSON-LD document to check.
|
|
5737
|
+
* @returns `true` if the document has a signature-like object; `false`
|
|
5738
|
+
* otherwise.
|
|
5739
|
+
* @since 2.2.0
|
|
5740
|
+
*/
|
|
5741
|
+
function hasSignatureLike(jsonLd) {
|
|
5742
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
5743
|
+
const signature = jsonLd.signature;
|
|
5744
|
+
const hasReference = (value) => {
|
|
5745
|
+
if (typeof value === "string") return true;
|
|
5746
|
+
if (Array.isArray(value)) return value.some(hasReference);
|
|
5747
|
+
return typeof value === "object" && value != null && ("id" in value && typeof value.id === "string" || "@id" in value && typeof value["@id"] === "string");
|
|
5748
|
+
};
|
|
5749
|
+
const hasSignatureObject = (value) => {
|
|
5750
|
+
if (typeof value !== "object" || value == null) return false;
|
|
5751
|
+
const signatureRecord = value;
|
|
5752
|
+
return (typeof signatureRecord.type === "string" || Array.isArray(signatureRecord.type) && signatureRecord.type.some((item) => typeof item === "string")) && (hasReference(signatureRecord.creator) || hasReference(signatureRecord.verificationMethod)) && (typeof signatureRecord.signatureValue === "string" || typeof signatureRecord.jws === "string");
|
|
5753
|
+
};
|
|
5754
|
+
return Array.isArray(signature) ? signature.some(hasSignatureObject) : hasSignatureObject(signature);
|
|
5755
|
+
}
|
|
5756
|
+
/**
|
|
5757
|
+
* Checks if the given JSON-LD document has a Linked Data Signature.
|
|
5758
|
+
* @param jsonLd The JSON-LD document to check.
|
|
5759
|
+
* @returns `true` if the document has a signature; `false` otherwise.
|
|
5760
|
+
* @since 1.0.0
|
|
5761
|
+
*/
|
|
5762
|
+
function hasSignature(jsonLd) {
|
|
5763
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
5764
|
+
if ("signature" in jsonLd) {
|
|
5765
|
+
const signature = jsonLd.signature;
|
|
5766
|
+
if (typeof signature !== "object" || signature == null) return false;
|
|
5767
|
+
return "type" in signature && signature.type === "RsaSignature2017" && "creator" in signature && typeof signature.creator === "string" && "created" in signature && typeof signature.created === "string" && "signatureValue" in signature && typeof signature.signatureValue === "string";
|
|
5768
|
+
}
|
|
5769
|
+
return false;
|
|
5770
|
+
}
|
|
5771
|
+
/**
|
|
5772
|
+
* Detaches Linked Data Signatures from the given JSON-LD document.
|
|
5773
|
+
* @param jsonLd The JSON-LD document to modify.
|
|
5774
|
+
* @returns The modified JSON-LD document. If the input document does not
|
|
5775
|
+
* contain a signature, the original document is returned.
|
|
5776
|
+
* @since 1.0.0
|
|
5777
|
+
*/
|
|
5778
|
+
function detachSignature(jsonLd) {
|
|
5779
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
|
|
5780
|
+
const doc = { ...jsonLd };
|
|
5781
|
+
delete doc.signature;
|
|
5782
|
+
return doc;
|
|
5866
5783
|
}
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5784
|
+
/**
|
|
5785
|
+
* Verifies Linked Data Signatures of the given JSON-LD document.
|
|
5786
|
+
*
|
|
5787
|
+
* This is a low-level utility that only checks the cryptographic signature
|
|
5788
|
+
* and (optionally) the cached key. It does not run the JSON-LD parsing,
|
|
5789
|
+
* attribution, and owner checks that a complete inbound LD verification
|
|
5790
|
+
* needs. For incoming activities, prefer {@link verifyJsonLd}, which is
|
|
5791
|
+
* the public verification entry point and the one that emits the
|
|
5792
|
+
* `activitypub.signature.verification.duration` metric for the LD path.
|
|
5793
|
+
* `verifySignature` itself only emits
|
|
5794
|
+
* `activitypub.signature.key_fetch.duration`, since the rest of the work
|
|
5795
|
+
* that the verification-duration metric is meant to cover happens in
|
|
5796
|
+
* `verifyJsonLd`.
|
|
5797
|
+
* @param jsonLd The JSON-LD document to verify.
|
|
5798
|
+
* @param options Options for verifying the signature.
|
|
5799
|
+
* @returns The public key that signed the document or `null` if the signature
|
|
5800
|
+
* is invalid or the key is not found.
|
|
5801
|
+
* @since 1.0.0
|
|
5802
|
+
*/
|
|
5803
|
+
async function verifySignature(jsonLd, options = {}) {
|
|
5804
|
+
if (!hasSignature(jsonLd)) return null;
|
|
5805
|
+
const sig = jsonLd.signature;
|
|
5806
|
+
let signature;
|
|
5807
|
+
try {
|
|
5808
|
+
signature = (0, byte_encodings_base64.decodeBase64)(sig.signatureValue);
|
|
5809
|
+
} catch (error) {
|
|
5810
|
+
logger$4.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
|
|
5811
|
+
...sig,
|
|
5812
|
+
error
|
|
5813
|
+
});
|
|
5814
|
+
return null;
|
|
5874
5815
|
}
|
|
5875
|
-
|
|
5816
|
+
const { key, cached } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, options));
|
|
5817
|
+
if (key == null) return null;
|
|
5818
|
+
const sigOpts = {
|
|
5819
|
+
...sig,
|
|
5820
|
+
"@context": "https://w3id.org/identity/v1"
|
|
5821
|
+
};
|
|
5822
|
+
delete sigOpts.type;
|
|
5823
|
+
delete sigOpts.id;
|
|
5824
|
+
delete sigOpts.signatureValue;
|
|
5825
|
+
let sigOptsHash;
|
|
5826
|
+
try {
|
|
5827
|
+
sigOptsHash = await hashJsonLd(sigOpts, options.contextLoader);
|
|
5828
|
+
} catch (error) {
|
|
5829
|
+
logger$4.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
|
|
5830
|
+
signatureOptions: sigOpts,
|
|
5831
|
+
error
|
|
5832
|
+
});
|
|
5833
|
+
return null;
|
|
5834
|
+
}
|
|
5835
|
+
const document = { ...jsonLd };
|
|
5836
|
+
delete document.signature;
|
|
5837
|
+
let docHash;
|
|
5838
|
+
try {
|
|
5839
|
+
docHash = await hashJsonLd(document, options.contextLoader);
|
|
5840
|
+
} catch (error) {
|
|
5841
|
+
logger$4.warn("Failed to verify; failed to hash the document: {document}\n{error}", {
|
|
5842
|
+
document,
|
|
5843
|
+
error
|
|
5844
|
+
});
|
|
5845
|
+
return null;
|
|
5846
|
+
}
|
|
5847
|
+
const encoder = new TextEncoder();
|
|
5848
|
+
const message = sigOptsHash + docHash;
|
|
5849
|
+
const messageBytes = encoder.encode(message);
|
|
5850
|
+
if (await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes)) return key;
|
|
5851
|
+
if (cached) {
|
|
5852
|
+
logger$4.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
|
|
5853
|
+
keyId: sig.creator,
|
|
5854
|
+
...sig
|
|
5855
|
+
});
|
|
5856
|
+
const { key } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, {
|
|
5857
|
+
...options,
|
|
5858
|
+
keyCache: {
|
|
5859
|
+
get: () => Promise.resolve(void 0),
|
|
5860
|
+
set: async (keyId, key) => await options.keyCache?.set(keyId, key)
|
|
5861
|
+
}
|
|
5862
|
+
}));
|
|
5863
|
+
if (key == null) return null;
|
|
5864
|
+
return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
|
|
5865
|
+
}
|
|
5866
|
+
logger$4.debug("Failed to verify with the fetched key {keyId}; signature {signatureValue} is invalid. Check if the key is correct or if the signed message is correct. The message to sign is:\n{message}", {
|
|
5867
|
+
keyId: sig.creator,
|
|
5868
|
+
...sig,
|
|
5869
|
+
message
|
|
5870
|
+
});
|
|
5871
|
+
return null;
|
|
5876
5872
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5873
|
+
/**
|
|
5874
|
+
* Known Linked Data Signature `type` values, used to keep
|
|
5875
|
+
* `ld_signatures.type` on a bounded set of spec-defined string values.
|
|
5876
|
+
* Fedify only signs and verifies `RsaSignature2017`; other values come in
|
|
5877
|
+
* only from external documents and are dropped from the metric attribute to
|
|
5878
|
+
* avoid attacker-controlled cardinality.
|
|
5879
|
+
*/
|
|
5880
|
+
const LD_KNOWN_SIGNATURE_TYPES = /* @__PURE__ */ new Set(["RsaSignature2017"]);
|
|
5881
|
+
/**
|
|
5882
|
+
* Reports only whether a `signature` key is present on the document, with
|
|
5883
|
+
* no shape check on its value. This is intentionally looser than
|
|
5884
|
+
* {@link hasSignature} (which validates a full `RsaSignature2017` shape)
|
|
5885
|
+
* and {@link hasSignatureLike} (which structurally accepts several known
|
|
5886
|
+
* suites): `verifyJsonLd` needs to tell a document with a malformed or
|
|
5887
|
+
* unsupported signature payload (classified as `rejected`) apart from a
|
|
5888
|
+
* truly unsigned document (classified as `missing`), and only this
|
|
5889
|
+
* presence-only check captures both cases.
|
|
5890
|
+
*/
|
|
5891
|
+
function hasLdSignatureProperty(jsonLd) {
|
|
5892
|
+
return typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd;
|
|
5880
5893
|
}
|
|
5881
|
-
function
|
|
5882
|
-
if (
|
|
5883
|
-
|
|
5894
|
+
function getLdSignatureObject(jsonLd) {
|
|
5895
|
+
if (!hasLdSignatureProperty(jsonLd)) return void 0;
|
|
5896
|
+
const { signature } = jsonLd;
|
|
5897
|
+
if (typeof signature !== "object" || signature == null || Array.isArray(signature)) return;
|
|
5898
|
+
return signature;
|
|
5899
|
+
}
|
|
5900
|
+
/**
|
|
5901
|
+
* Verify the authenticity of the given JSON-LD document using Linked Data
|
|
5902
|
+
* Signatures. If the document is signed, this function verifies the signature
|
|
5903
|
+
* and checks if the document is attributed to the owner of the public key.
|
|
5904
|
+
* If the document is not signed, this function returns `false`.
|
|
5905
|
+
* @param jsonLd The JSON-LD document to verify.
|
|
5906
|
+
* @param options Options for verifying the document.
|
|
5907
|
+
* @returns `true` if the document is authentic; `false` otherwise.
|
|
5908
|
+
*/
|
|
5909
|
+
async function verifyJsonLd(jsonLd, options = {}) {
|
|
5910
|
+
return await verifyJsonLdInternal(jsonLd, options, true);
|
|
5884
5911
|
}
|
|
5885
5912
|
/** @internal */
|
|
5886
|
-
function
|
|
5887
|
-
|
|
5888
|
-
return error instanceof TypeError && (code === "ERR_INVALID_URL" || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
|
|
5913
|
+
async function verifyCompactJsonLd(jsonLd, options = {}) {
|
|
5914
|
+
return await verifyJsonLdInternal(jsonLd, options, false);
|
|
5889
5915
|
}
|
|
5890
|
-
async function
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
}
|
|
5897
|
-
if (typeof context === "string") {
|
|
5898
|
-
const reference = normalizeContextReference(context, baseUrl);
|
|
5899
|
-
const cacheKey = `${baseUrl ?? ""}\n${reference}`;
|
|
5900
|
-
if (processingContexts.has(cacheKey)) return nextState;
|
|
5901
|
-
processingContexts.add(cacheKey);
|
|
5916
|
+
async function verifyJsonLdInternal(jsonLd, options, compact) {
|
|
5917
|
+
return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("ld_signatures.verify", async (span) => {
|
|
5918
|
+
const start = performance.now();
|
|
5919
|
+
let verified = false;
|
|
5920
|
+
let threw = false;
|
|
5921
|
+
let signatureType;
|
|
5902
5922
|
try {
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5923
|
+
const verificationOptions = hasSignature(jsonLd) ? {
|
|
5924
|
+
...options,
|
|
5925
|
+
contextLoader: getNormalizationContextLoader(options.contextLoader)
|
|
5926
|
+
} : options;
|
|
5927
|
+
const compacted = compact ? hasSignature(jsonLd) ? await compactJsonLd(jsonLd, options.contextLoader) : jsonLd : jsonLd;
|
|
5928
|
+
const object = await _fedify_vocab.Object.fromJsonLd(compacted, verificationOptions);
|
|
5929
|
+
if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
|
|
5930
|
+
span.setAttribute("activitypub.object.type", (0, _fedify_vocab.getTypeId)(object).href);
|
|
5931
|
+
const sig = getLdSignatureObject(jsonLd);
|
|
5932
|
+
if (sig != null) {
|
|
5933
|
+
if (typeof sig.creator === "string") span.setAttribute("ld_signatures.key_id", sig.creator);
|
|
5934
|
+
if (typeof sig.signatureValue === "string") span.setAttribute("ld_signatures.signature", sig.signatureValue);
|
|
5935
|
+
if (typeof sig.type === "string") {
|
|
5936
|
+
span.setAttribute("ld_signatures.type", sig.type);
|
|
5937
|
+
if (LD_KNOWN_SIGNATURE_TYPES.has(sig.type)) signatureType = sig.type;
|
|
5938
|
+
}
|
|
5914
5939
|
}
|
|
5915
|
-
const
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
}
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
if (directTarget == null) nextState.termTargets.set(term, null);
|
|
5936
|
-
else nextState.termTargets.set(term, resolveLocalContextTarget(directTarget, nextState, localTargets));
|
|
5937
|
-
} else nextState.termTargets.delete(term);
|
|
5938
|
-
if (typeof definition === "object" && definition != null && "@context" in definition) nextState.propertyContexts.set(term, {
|
|
5939
|
-
context: definition["@context"],
|
|
5940
|
-
baseUrl
|
|
5940
|
+
const attributions = new Set(object.attributionIds.map((uri) => uri.href));
|
|
5941
|
+
if (object instanceof _fedify_vocab.Activity) for (const uri of object.actorIds) attributions.add(uri.href);
|
|
5942
|
+
const key = await verifySignature(compacted, verificationOptions);
|
|
5943
|
+
if (key == null) return false;
|
|
5944
|
+
if (key.ownerId == null) {
|
|
5945
|
+
logger$4.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
|
|
5946
|
+
return false;
|
|
5947
|
+
}
|
|
5948
|
+
attributions.delete(key.ownerId.href);
|
|
5949
|
+
if (attributions.size > 0) {
|
|
5950
|
+
logger$4.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
|
|
5951
|
+
return false;
|
|
5952
|
+
}
|
|
5953
|
+
verified = true;
|
|
5954
|
+
return true;
|
|
5955
|
+
} catch (error) {
|
|
5956
|
+
threw = true;
|
|
5957
|
+
span.setStatus({
|
|
5958
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
5959
|
+
message: String(error)
|
|
5941
5960
|
});
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5961
|
+
throw error;
|
|
5962
|
+
} finally {
|
|
5963
|
+
const classified = threw ? "error" : verified ? "verified" : hasLdSignatureProperty(jsonLd) ? "rejected" : "missing";
|
|
5964
|
+
getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "linked_data", classified, { ldType: signatureType });
|
|
5965
|
+
span.end();
|
|
5945
5966
|
}
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5967
|
+
});
|
|
5968
|
+
}
|
|
5969
|
+
async function hashJsonLd(jsonLd, contextLoader) {
|
|
5970
|
+
const canon = await _fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
|
|
5971
|
+
format: "application/n-quads",
|
|
5972
|
+
documentLoader: contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)()
|
|
5973
|
+
});
|
|
5974
|
+
const encoder = new TextEncoder();
|
|
5975
|
+
return (0, byte_encodings_hex.encodeHex)(await crypto.subtle.digest("SHA-256", encoder.encode(canon)));
|
|
5949
5976
|
}
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5977
|
+
//#endregion
|
|
5978
|
+
//#region src/compat/secured-document.ts
|
|
5979
|
+
function isJsonMap$1(value) {
|
|
5980
|
+
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
5981
|
+
}
|
|
5982
|
+
function isNonEmptyString(value) {
|
|
5983
|
+
return typeof value === "string" && value.length > 0;
|
|
5984
|
+
}
|
|
5985
|
+
function hasDataIntegrityProofType(value) {
|
|
5986
|
+
return (Array.isArray(value) ? value : [value]).some((type) => type === "DataIntegrityProof" || type === "https://w3id.org/security#DataIntegrityProof");
|
|
5987
|
+
}
|
|
5988
|
+
/**
|
|
5989
|
+
* Checks whether a JSON value is a complete, self-contained secured document:
|
|
5990
|
+
* a map with its own `@context` and exactly one direct, map-valued `proof`
|
|
5991
|
+
* that carries every member `eddsa-jcs-2022` proof generation produces.
|
|
5992
|
+
*
|
|
5993
|
+
* This is deliberately stricter than {@link hasProofLike}, which exists to
|
|
5994
|
+
* decide whether a document is worth verifying. Here a false positive would
|
|
5995
|
+
* silently suppress a wire-compatibility fix on an ordinary document, so a
|
|
5996
|
+
* partial or malformed proof does not qualify.
|
|
5997
|
+
*
|
|
5998
|
+
* @internal
|
|
5999
|
+
*/
|
|
6000
|
+
function isSelfContainedSecuredDocument(value) {
|
|
6001
|
+
if (!isJsonMap$1(value)) return false;
|
|
6002
|
+
if (!Object.hasOwn(value, "@context") || value["@context"] == null) return false;
|
|
6003
|
+
if (!Object.hasOwn(value, "proof")) return false;
|
|
6004
|
+
const proof = value.proof;
|
|
6005
|
+
if (!isJsonMap$1(proof)) return false;
|
|
6006
|
+
return hasDataIntegrityProofType(proof.type) && isNonEmptyString(proof.cryptosuite) && isNonEmptyString(proof.created) && isNonEmptyString(proof.verificationMethod) && isNonEmptyString(proof.proofPurpose) && isNonEmptyString(proof.proofValue);
|
|
6007
|
+
}
|
|
6008
|
+
//#endregion
|
|
6009
|
+
//#region src/compat/public-audience.ts
|
|
6010
|
+
const logger$3 = (0, _logtape_logtape.getLogger)([
|
|
6011
|
+
"fedify",
|
|
6012
|
+
"compat",
|
|
6013
|
+
"public-audience"
|
|
6014
|
+
]);
|
|
6015
|
+
const PUBLIC_ADDRESSING_FIELDS = /* @__PURE__ */ new Set([
|
|
6016
|
+
"to",
|
|
6017
|
+
"cc",
|
|
6018
|
+
"bto",
|
|
6019
|
+
"bcc",
|
|
6020
|
+
"audience"
|
|
6021
|
+
]);
|
|
6022
|
+
const AS_CONTEXT_URL$1 = "https://www.w3.org/ns/activitystreams";
|
|
6023
|
+
const MAX_TRAVERSAL_DEPTH$1 = 64;
|
|
6024
|
+
const KNOWN_SAFE_CONTEXT_URLS$1 = new Set(Object.keys(_fedify_vocab_runtime.preloadedContexts));
|
|
6025
|
+
function hasPublicCurieInAddressing(value, preserveSecured, parentKey, depth = 0) {
|
|
6026
|
+
if (typeof value === "string") return parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public");
|
|
6027
|
+
if (depth >= MAX_TRAVERSAL_DEPTH$1) return false;
|
|
6028
|
+
if (Array.isArray(value)) return value.some((item) => hasPublicCurieInAddressing(item, preserveSecured, parentKey, depth + 1));
|
|
6029
|
+
if (typeof value !== "object" || value == null) return false;
|
|
6030
|
+
if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return false;
|
|
6031
|
+
const record = value;
|
|
6032
|
+
for (const key of Object.keys(record)) {
|
|
6033
|
+
if (record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public")) return true;
|
|
5961
6034
|
if (key === "@context") continue;
|
|
5962
|
-
if (
|
|
5963
|
-
if (key === "@graph" || state.graphTerms.has(key)) throw new UnsafeJsonLdError("@graph");
|
|
5964
|
-
if (state.jsonTerms.has(key)) continue;
|
|
5965
|
-
await assertNoGraphBeforeCompaction(value, documentLoader, state, state.propertyContexts.get(key), remoteContextCache);
|
|
6035
|
+
if (hasPublicCurieInAddressing(record[key], preserveSecured, key, depth + 1)) return true;
|
|
5966
6036
|
}
|
|
6037
|
+
return false;
|
|
5967
6038
|
}
|
|
5968
|
-
function
|
|
5969
|
-
|
|
6039
|
+
function rewritePublicAudience(value, preserveSecured, parentKey, depth = 0) {
|
|
6040
|
+
if (typeof value === "string" && parentKey != null && PUBLIC_ADDRESSING_FIELDS.has(parentKey) && (value === "as:Public" || value === "Public")) return _fedify_vocab.PUBLIC_COLLECTION.href;
|
|
6041
|
+
if (depth >= MAX_TRAVERSAL_DEPTH$1) return value;
|
|
6042
|
+
if (Array.isArray(value)) {
|
|
6043
|
+
let changed = false;
|
|
6044
|
+
const mapped = value.map((item) => {
|
|
6045
|
+
const rewritten = rewritePublicAudience(item, preserveSecured, parentKey, depth + 1);
|
|
6046
|
+
if (rewritten !== item) changed = true;
|
|
6047
|
+
return rewritten;
|
|
6048
|
+
});
|
|
6049
|
+
return changed ? mapped : value;
|
|
6050
|
+
}
|
|
6051
|
+
if (typeof value !== "object" || value == null) return value;
|
|
6052
|
+
if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(value)) return value;
|
|
6053
|
+
const record = value;
|
|
6054
|
+
let changed = false;
|
|
6055
|
+
const normalized = Object.create(null);
|
|
6056
|
+
for (const key of Object.keys(record)) {
|
|
6057
|
+
const rewritten = key === "@context" ? record[key] : record.type === "Follow" && key === "object" && (record[key] === "as:Public" || record[key] === "Public") ? _fedify_vocab.PUBLIC_COLLECTION.href : rewritePublicAudience(record[key], preserveSecured, key, depth + 1);
|
|
6058
|
+
if (rewritten !== record[key]) changed = true;
|
|
6059
|
+
normalized[key] = rewritten;
|
|
6060
|
+
}
|
|
6061
|
+
return changed ? normalized : value;
|
|
5970
6062
|
}
|
|
5971
|
-
/**
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
6063
|
+
/**
|
|
6064
|
+
* Reports whether `value` carries an `@context` property anywhere inside
|
|
6065
|
+
* its subtree (not counting the value itself). A nested `@context` can
|
|
6066
|
+
* introduce a local term-definition scope that redefines `as:` or `Public`
|
|
6067
|
+
* even when the top-level `@context` is safe, so the fast path must defer
|
|
6068
|
+
* to the URDNA2015 equivalence check whenever one is present.
|
|
6069
|
+
*/
|
|
6070
|
+
function hasNestedContext$1(value, depth = 0) {
|
|
6071
|
+
if (depth >= MAX_TRAVERSAL_DEPTH$1) return true;
|
|
6072
|
+
if (Array.isArray(value)) return value.some((item) => hasNestedContext$1(item, depth + 1));
|
|
6073
|
+
if (typeof value !== "object" || value == null) return false;
|
|
6074
|
+
const record = value;
|
|
6075
|
+
for (const key of Object.keys(record)) {
|
|
6076
|
+
if (key === "@context") return true;
|
|
6077
|
+
if (hasNestedContext$1(record[key], depth + 1)) return true;
|
|
5981
6078
|
}
|
|
6079
|
+
return false;
|
|
5982
6080
|
}
|
|
5983
6081
|
/**
|
|
5984
|
-
*
|
|
5985
|
-
*
|
|
5986
|
-
*
|
|
5987
|
-
*
|
|
5988
|
-
*
|
|
5989
|
-
*
|
|
5990
|
-
*
|
|
6082
|
+
* Checks whether the `@context` of a JSON-LD document is guaranteed not
|
|
6083
|
+
* to redefine the `as:` prefix or the bare `Public` term. Only documents
|
|
6084
|
+
* whose `@context` is a string, or an array of strings, drawn from Fedify's
|
|
6085
|
+
* preloaded context set AND including the ActivityStreams URL qualify,
|
|
6086
|
+
* AND no nested subtree carries its own `@context` that might redefine
|
|
6087
|
+
* those terms within a local scope. When all of that holds the rewrite
|
|
6088
|
+
* is provably semantics-preserving and the URDNA2015 equivalence check
|
|
6089
|
+
* can be skipped. Any other shape (unknown external URLs, inline
|
|
6090
|
+
* objects at the top level, nested `@context` blocks) is treated as
|
|
6091
|
+
* potentially unsafe.
|
|
5991
6092
|
*/
|
|
5992
|
-
function
|
|
5993
|
-
if (typeof jsonLd !== "object" || jsonLd == null)
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
6093
|
+
function hasKnownSafeContext$1(jsonLd) {
|
|
6094
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
6095
|
+
const record = jsonLd;
|
|
6096
|
+
if (!Object.hasOwn(record, "@context")) return false;
|
|
6097
|
+
const ctx = record["@context"];
|
|
6098
|
+
const entries = typeof ctx === "string" ? [ctx] : Array.isArray(ctx) ? ctx : null;
|
|
6099
|
+
if (entries == null || entries.length === 0) return false;
|
|
6100
|
+
let hasAs = false;
|
|
6101
|
+
for (const entry of entries) {
|
|
6102
|
+
if (typeof entry !== "string") return false;
|
|
6103
|
+
if (!KNOWN_SAFE_CONTEXT_URLS$1.has(entry)) return false;
|
|
6104
|
+
if (entry === AS_CONTEXT_URL$1) hasAs = true;
|
|
6105
|
+
}
|
|
6106
|
+
if (!hasAs) return false;
|
|
6107
|
+
for (const key of Object.keys(record)) {
|
|
6108
|
+
if (key === "@context") continue;
|
|
6109
|
+
if (hasNestedContext$1(record[key])) return false;
|
|
6110
|
+
}
|
|
6111
|
+
return true;
|
|
5998
6112
|
}
|
|
5999
6113
|
/**
|
|
6000
|
-
*
|
|
6001
|
-
*
|
|
6002
|
-
*
|
|
6003
|
-
*
|
|
6004
|
-
*
|
|
6005
|
-
*
|
|
6006
|
-
*
|
|
6007
|
-
*
|
|
6008
|
-
*
|
|
6114
|
+
* Rewrites the compact `as:Public` / `Public` CURIE appearing in activity
|
|
6115
|
+
* addressing fields (`to`, `cc`, `bto`, `bcc`, `audience`) to the fully
|
|
6116
|
+
* expanded `https://www.w3.org/ns/activitystreams#Public` URI.
|
|
6117
|
+
*
|
|
6118
|
+
* Several ActivityPub implementations, Lemmy among them, match these
|
|
6119
|
+
* fields as plain URLs without running JSON-LD expansion, and silently
|
|
6120
|
+
* drop activities whose public addressing appears in CURIE form. This
|
|
6121
|
+
* helper works around that gap.
|
|
6122
|
+
*
|
|
6123
|
+
* For documents whose `@context` is drawn entirely from Fedify's
|
|
6124
|
+
* preloaded context set and includes the ActivityStreams URL, the
|
|
6125
|
+
* rewrite is applied directly: the content of every preloaded non-AS
|
|
6126
|
+
* context is known not to redefine the `as:` prefix or the bare `Public`
|
|
6127
|
+
* term, so the semantics are preserved by construction. Any other
|
|
6128
|
+
* shape (an inline object, an unknown external URL, and so on) is
|
|
6129
|
+
* treated as potentially unsafe and gated on a JSON-LD equivalence
|
|
6130
|
+
* check; both forms are canonicalized with URDNA2015 and the resulting
|
|
6131
|
+
* N-Quads are compared. When they differ, the original document is
|
|
6132
|
+
* returned unchanged. Canonicalization failures also fall back to the
|
|
6133
|
+
* original document.
|
|
6134
|
+
*
|
|
6135
|
+
* When no `contextLoader` is supplied the helper falls back to an
|
|
6136
|
+
* internal loader that resolves only the URLs in Fedify's
|
|
6137
|
+
* preloaded-contexts set and rejects every other URL without issuing a
|
|
6138
|
+
* network request. That behaviour is deliberately narrower than
|
|
6139
|
+
* `@fedify/vocab-runtime`'s `getDocumentLoader()`, which after its
|
|
6140
|
+
* `validatePublicUrl` check will happily fetch non-preloaded URLs: the
|
|
6141
|
+
* helper is reached from verification paths (`verifyProof()` /
|
|
6142
|
+
* `verifyObject()`) that operate on inbound, potentially adversarial
|
|
6143
|
+
* JSON-LD, and a default loader that fetches attacker-supplied
|
|
6144
|
+
* `@context` URLs on the caller's behalf would be an SSRF vector.
|
|
6145
|
+
* Canonicalization failures against the restricted loader fall back to
|
|
6146
|
+
* the original document, same as any other canonicalization error.
|
|
6147
|
+
* Callers that genuinely need the remote-fetch loader (for example
|
|
6148
|
+
* applications that sign local JSON-LD against a custom vocabulary)
|
|
6149
|
+
* should pass a `contextLoader` explicitly.
|
|
6150
|
+
*
|
|
6151
|
+
* Must be called before any signing step that canonicalizes the
|
|
6152
|
+
* compact form byte-for-byte (for example, Object Integrity Proofs
|
|
6153
|
+
* using the `eddsa-jcs-2022` cryptosuite), so the signed payload
|
|
6154
|
+
* matches what is sent on the wire.
|
|
6009
6155
|
*/
|
|
6010
|
-
async function
|
|
6011
|
-
|
|
6012
|
-
if (
|
|
6013
|
-
const
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6156
|
+
async function normalizePublicAudience(jsonLd, contextLoader, options = {}) {
|
|
6157
|
+
const preserveSecured = options.preserveNestedSecuredDocuments ?? false;
|
|
6158
|
+
if (!hasPublicCurieInAddressing(jsonLd, preserveSecured)) return jsonLd;
|
|
6159
|
+
const normalized = rewritePublicAudience(jsonLd, preserveSecured);
|
|
6160
|
+
if (hasKnownSafeContext$1(jsonLd)) return normalized;
|
|
6161
|
+
const loader = contextLoader ?? preloadedOnlyDocumentLoader;
|
|
6162
|
+
try {
|
|
6163
|
+
const [before, after] = await Promise.all([_fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
|
|
6164
|
+
format: "application/n-quads",
|
|
6165
|
+
documentLoader: loader
|
|
6166
|
+
}), _fedify_vocab_runtime_jsonld.default.canonize(normalized, {
|
|
6167
|
+
format: "application/n-quads",
|
|
6168
|
+
documentLoader: loader
|
|
6169
|
+
})]);
|
|
6170
|
+
if (before === after) return normalized;
|
|
6171
|
+
logger$3.warn("Expanding the public audience CURIE to its full URI would change the canonical form of the activity; sending the activity as is. This usually means the active JSON-LD context redefines the `as:` prefix or the bare `Public` term.");
|
|
6172
|
+
} catch (error) {
|
|
6173
|
+
logger$3.debug("Failed to verify public audience normalization equivalence via JSON-LD canonicalization; sending the activity as is.\n{error}", { error });
|
|
6174
|
+
}
|
|
6175
|
+
return jsonLd;
|
|
6026
6176
|
}
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6177
|
+
//#endregion
|
|
6178
|
+
//#region src/compat/outgoing-jsonld.ts
|
|
6179
|
+
const logger$2 = (0, _logtape_logtape.getLogger)([
|
|
6180
|
+
"fedify",
|
|
6181
|
+
"compat",
|
|
6182
|
+
"outgoing-jsonld"
|
|
6183
|
+
]);
|
|
6184
|
+
const ATTACHMENT_FIELDS = /* @__PURE__ */ new Set(["attachment", "https://www.w3.org/ns/activitystreams#attachment"]);
|
|
6185
|
+
const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams";
|
|
6186
|
+
const KNOWN_SAFE_CONTEXT_URLS = getKnownSafeContextUrls();
|
|
6187
|
+
const MAX_TRAVERSAL_DEPTH = 64;
|
|
6188
|
+
function isJsonLdListObject(value) {
|
|
6189
|
+
return typeof value === "object" && value != null && Object.hasOwn(value, "@list");
|
|
6190
|
+
}
|
|
6191
|
+
function isJsonLdValueObject(value) {
|
|
6192
|
+
return typeof value === "object" && value != null && Object.hasOwn(value, "@value");
|
|
6193
|
+
}
|
|
6194
|
+
function* getContextObjects(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
6195
|
+
if (Array.isArray(value)) {
|
|
6196
|
+
if (seen.has(value)) return;
|
|
6197
|
+
seen.add(value);
|
|
6198
|
+
for (const item of value) yield* getContextObjects(item, seen);
|
|
6199
|
+
return;
|
|
6200
|
+
}
|
|
6201
|
+
if (typeof value === "object" && value != null) {
|
|
6202
|
+
if (seen.has(value)) return;
|
|
6203
|
+
seen.add(value);
|
|
6204
|
+
const record = value;
|
|
6205
|
+
yield record;
|
|
6206
|
+
for (const definition of Object.values(record)) {
|
|
6207
|
+
if (typeof definition !== "object" || definition == null) continue;
|
|
6208
|
+
const nestedContext = definition["@context"];
|
|
6209
|
+
if (nestedContext == null) continue;
|
|
6210
|
+
yield* getContextObjects(nestedContext, seen);
|
|
6057
6211
|
}
|
|
6058
|
-
}
|
|
6212
|
+
}
|
|
6059
6213
|
}
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
* object, without restricting it to a single suite-specific shape.
|
|
6063
|
-
* @param jsonLd The JSON-LD document to check.
|
|
6064
|
-
* @returns `true` if the document has a signature-like object; `false`
|
|
6065
|
-
* otherwise.
|
|
6066
|
-
* @since 2.2.0
|
|
6067
|
-
*/
|
|
6068
|
-
function hasSignatureLike(jsonLd) {
|
|
6069
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
6070
|
-
const signature = jsonLd.signature;
|
|
6071
|
-
const hasReference = (value) => {
|
|
6072
|
-
if (typeof value === "string") return true;
|
|
6073
|
-
if (Array.isArray(value)) return value.some(hasReference);
|
|
6074
|
-
return typeof value === "object" && value != null && ("id" in value && typeof value.id === "string" || "@id" in value && typeof value["@id"] === "string");
|
|
6075
|
-
};
|
|
6076
|
-
const hasSignatureObject = (value) => {
|
|
6077
|
-
if (typeof value !== "object" || value == null) return false;
|
|
6078
|
-
const signatureRecord = value;
|
|
6079
|
-
return (typeof signatureRecord.type === "string" || Array.isArray(signatureRecord.type) && signatureRecord.type.some((item) => typeof item === "string")) && (hasReference(signatureRecord.creator) || hasReference(signatureRecord.verificationMethod)) && (typeof signatureRecord.signatureValue === "string" || typeof signatureRecord.jws === "string");
|
|
6080
|
-
};
|
|
6081
|
-
return Array.isArray(signature) ? signature.some(hasSignatureObject) : hasSignatureObject(signature);
|
|
6214
|
+
function isActivityStreamsAttachmentTerm(value) {
|
|
6215
|
+
return typeof value === "object" && value != null && value["@id"] === "as:attachment" && value["@type"] === "@id";
|
|
6082
6216
|
}
|
|
6083
|
-
/**
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
if ("signature" in jsonLd) {
|
|
6092
|
-
const signature = jsonLd.signature;
|
|
6093
|
-
if (typeof signature !== "object" || signature == null) return false;
|
|
6094
|
-
return "type" in signature && signature.type === "RsaSignature2017" && "creator" in signature && typeof signature.creator === "string" && "created" in signature && typeof signature.created === "string" && "signatureValue" in signature && typeof signature.signatureValue === "string";
|
|
6217
|
+
/** @internal */
|
|
6218
|
+
function isPreloadedContextAttachmentSafe(document) {
|
|
6219
|
+
if (typeof document !== "object" || document == null) return true;
|
|
6220
|
+
const context = document["@context"];
|
|
6221
|
+
for (const contextObject of getContextObjects(context)) {
|
|
6222
|
+
if (!Object.hasOwn(contextObject, "attachment")) continue;
|
|
6223
|
+
if (isActivityStreamsAttachmentTerm(contextObject.attachment)) continue;
|
|
6224
|
+
return false;
|
|
6095
6225
|
}
|
|
6096
|
-
return
|
|
6226
|
+
return true;
|
|
6097
6227
|
}
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
* @since 1.0.0
|
|
6104
|
-
*/
|
|
6105
|
-
function detachSignature(jsonLd) {
|
|
6106
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
|
|
6107
|
-
const doc = { ...jsonLd };
|
|
6108
|
-
delete doc.signature;
|
|
6109
|
-
return doc;
|
|
6228
|
+
function getKnownSafeContextUrls() {
|
|
6229
|
+
const urls = /* @__PURE__ */ new Set();
|
|
6230
|
+
for (const [url, document] of Object.entries(_fedify_vocab_runtime.preloadedContexts)) if (isPreloadedContextAttachmentSafe(document)) urls.add(url);
|
|
6231
|
+
else logger$2.warn("Preloaded JSON-LD context {contextUrl} redefines the `attachment` term incompatibly; attachment array normalization will require canonicalization for documents using it.", { contextUrl: url });
|
|
6232
|
+
return urls;
|
|
6110
6233
|
}
|
|
6111
6234
|
/**
|
|
6112
|
-
*
|
|
6113
|
-
*
|
|
6114
|
-
* This is a low-level utility that only checks the cryptographic signature
|
|
6115
|
-
* and (optionally) the cached key. It does not run the JSON-LD parsing,
|
|
6116
|
-
* attribution, and owner checks that a complete inbound LD verification
|
|
6117
|
-
* needs. For incoming activities, prefer {@link verifyJsonLd}, which is
|
|
6118
|
-
* the public verification entry point and the one that emits the
|
|
6119
|
-
* `activitypub.signature.verification.duration` metric for the LD path.
|
|
6120
|
-
* `verifySignature` itself only emits
|
|
6121
|
-
* `activitypub.signature.key_fetch.duration`, since the rest of the work
|
|
6122
|
-
* that the verification-duration metric is meant to cover happens in
|
|
6123
|
-
* `verifyJsonLd`.
|
|
6124
|
-
* @param jsonLd The JSON-LD document to verify.
|
|
6125
|
-
* @param options Options for verifying the signature.
|
|
6126
|
-
* @returns The public key that signed the document or `null` if the signature
|
|
6127
|
-
* is invalid or the key is not found.
|
|
6128
|
-
* @since 1.0.0
|
|
6235
|
+
* Wraps scalar ActivityStreams attachment properties in arrays.
|
|
6129
6236
|
*/
|
|
6130
|
-
|
|
6131
|
-
if (
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
}
|
|
6141
|
-
return
|
|
6237
|
+
function wrapScalarAttachments(jsonLd, preserveSecured, depth = 0) {
|
|
6238
|
+
if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd;
|
|
6239
|
+
if (preserveSecured && depth > 0 && isSelfContainedSecuredDocument(jsonLd)) return jsonLd;
|
|
6240
|
+
if (Array.isArray(jsonLd)) {
|
|
6241
|
+
let normalized = null;
|
|
6242
|
+
for (let i = 0; i < jsonLd.length; i++) {
|
|
6243
|
+
const item = jsonLd[i];
|
|
6244
|
+
const next = wrapScalarAttachments(item, preserveSecured, depth + 1);
|
|
6245
|
+
if (normalized == null && next !== item) normalized = jsonLd.slice(0, i);
|
|
6246
|
+
if (normalized != null) normalized[i] = next;
|
|
6247
|
+
}
|
|
6248
|
+
return normalized ?? jsonLd;
|
|
6142
6249
|
}
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
const
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
}
|
|
6160
|
-
|
|
6250
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
|
|
6251
|
+
const record = jsonLd;
|
|
6252
|
+
const keys = Object.keys(record);
|
|
6253
|
+
let normalized = null;
|
|
6254
|
+
for (let i = 0; i < keys.length; i++) {
|
|
6255
|
+
const key = keys[i];
|
|
6256
|
+
const value = record[key];
|
|
6257
|
+
const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, preserveSecured, depth + 1);
|
|
6258
|
+
const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next;
|
|
6259
|
+
if (normalized == null && output !== value) {
|
|
6260
|
+
const cloned = Object.create(null);
|
|
6261
|
+
for (let j = 0; j < i; j++) {
|
|
6262
|
+
const previousKey = keys[j];
|
|
6263
|
+
cloned[previousKey] = record[previousKey];
|
|
6264
|
+
}
|
|
6265
|
+
normalized = cloned;
|
|
6266
|
+
}
|
|
6267
|
+
if (normalized != null) normalized[key] = output;
|
|
6161
6268
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
return
|
|
6269
|
+
return normalized ?? jsonLd;
|
|
6270
|
+
}
|
|
6271
|
+
function hasNestedContext(value, depth = 0) {
|
|
6272
|
+
if (depth >= MAX_TRAVERSAL_DEPTH) return true;
|
|
6273
|
+
if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1));
|
|
6274
|
+
if (typeof value !== "object" || value == null) return false;
|
|
6275
|
+
const record = value;
|
|
6276
|
+
for (const key of Object.keys(record)) {
|
|
6277
|
+
if (key === "@context") return true;
|
|
6278
|
+
if (key === "@value" && isJsonLdValueObject(value)) continue;
|
|
6279
|
+
if (hasNestedContext(record[key], depth + 1)) return true;
|
|
6173
6280
|
}
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
if (
|
|
6178
|
-
if (
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6281
|
+
return false;
|
|
6282
|
+
}
|
|
6283
|
+
function exceedsTraversalDepth(value, depth = 0) {
|
|
6284
|
+
if (depth >= MAX_TRAVERSAL_DEPTH) return true;
|
|
6285
|
+
if (Array.isArray(value)) return value.some((item) => exceedsTraversalDepth(item, depth + 1));
|
|
6286
|
+
if (typeof value !== "object" || value == null) return false;
|
|
6287
|
+
const record = value;
|
|
6288
|
+
for (const key of Object.keys(record)) {
|
|
6289
|
+
if (key === "@context" || key === "@value" && isJsonLdValueObject(value)) continue;
|
|
6290
|
+
if (exceedsTraversalDepth(record[key], depth + 1)) return true;
|
|
6291
|
+
}
|
|
6292
|
+
return false;
|
|
6293
|
+
}
|
|
6294
|
+
function hasKnownSafeContext(jsonLd) {
|
|
6295
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return false;
|
|
6296
|
+
const record = jsonLd;
|
|
6297
|
+
if (!Object.hasOwn(record, "@context")) return false;
|
|
6298
|
+
const context = record["@context"];
|
|
6299
|
+
const entries = typeof context === "string" ? [context] : Array.isArray(context) ? context : null;
|
|
6300
|
+
if (entries == null || entries.length < 1) return false;
|
|
6301
|
+
let hasActivityStreamsContext = false;
|
|
6302
|
+
for (const entry of entries) {
|
|
6303
|
+
if (typeof entry !== "string") return false;
|
|
6304
|
+
if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false;
|
|
6305
|
+
if (entry === AS_CONTEXT_URL) hasActivityStreamsContext = true;
|
|
6192
6306
|
}
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
}
|
|
6198
|
-
return
|
|
6307
|
+
if (!hasActivityStreamsContext) return false;
|
|
6308
|
+
for (const key of Object.keys(record)) {
|
|
6309
|
+
if (key === "@context") continue;
|
|
6310
|
+
if (hasNestedContext(record[key])) return false;
|
|
6311
|
+
}
|
|
6312
|
+
return true;
|
|
6313
|
+
}
|
|
6314
|
+
function getLogSafeJsonLdMetadata(jsonLd) {
|
|
6315
|
+
if (typeof jsonLd !== "object" || jsonLd == null) return {};
|
|
6316
|
+
const record = jsonLd;
|
|
6317
|
+
const context = record["@context"];
|
|
6318
|
+
return {
|
|
6319
|
+
id: typeof record.id === "string" ? record.id : typeof record["@id"] === "string" ? record["@id"] : void 0,
|
|
6320
|
+
type: typeof record.type === "string" ? record.type : typeof record["@type"] === "string" ? record["@type"] : void 0,
|
|
6321
|
+
context: typeof context === "string" ? context : Array.isArray(context) ? context.filter((entry) => typeof entry === "string").slice(0, 4) : context == null ? void 0 : "[inline context]"
|
|
6322
|
+
};
|
|
6199
6323
|
}
|
|
6200
6324
|
/**
|
|
6201
|
-
*
|
|
6202
|
-
*
|
|
6203
|
-
*
|
|
6204
|
-
*
|
|
6205
|
-
*
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
*
|
|
6210
|
-
*
|
|
6211
|
-
*
|
|
6212
|
-
*
|
|
6213
|
-
*
|
|
6214
|
-
*
|
|
6215
|
-
* truly unsigned document (classified as `missing`), and only this
|
|
6216
|
-
* presence-only check captures both cases.
|
|
6325
|
+
* Ensures ActivityStreams attachment properties are represented as arrays
|
|
6326
|
+
* when doing so preserves the JSON-LD semantics.
|
|
6327
|
+
*
|
|
6328
|
+
* JSON-LD compaction collapses single-item arrays into scalar values by
|
|
6329
|
+
* default. Some ActivityPub implementations, Pixelfed among them, parse
|
|
6330
|
+
* `attachment` as a plain JSON array rather than a JSON-LD property and reject
|
|
6331
|
+
* otherwise valid objects whose single attachment is emitted as a scalar.
|
|
6332
|
+
*
|
|
6333
|
+
* When no `contextLoader` is supplied, the helper falls back to a restricted
|
|
6334
|
+
* loader that resolves only Fedify's preloaded JSON-LD contexts and rejects
|
|
6335
|
+
* every other URL without network access. Documents with custom, inline, or
|
|
6336
|
+
* otherwise uncached contexts should pass a real `contextLoader` if they need
|
|
6337
|
+
* the semantic-preservation check to succeed; otherwise canonicalization
|
|
6338
|
+
* failures leave the original document unchanged.
|
|
6217
6339
|
*/
|
|
6218
|
-
function
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
return
|
|
6340
|
+
async function normalizeAttachmentArrays(jsonLd, contextLoader, options = {}) {
|
|
6341
|
+
const normalized = wrapScalarAttachments(jsonLd, options.preserveNestedSecuredDocuments ?? false);
|
|
6342
|
+
if (normalized === jsonLd) return jsonLd;
|
|
6343
|
+
if (exceedsTraversalDepth(jsonLd)) {
|
|
6344
|
+
logger$2.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged.");
|
|
6345
|
+
return jsonLd;
|
|
6346
|
+
}
|
|
6347
|
+
if (hasKnownSafeContext(jsonLd)) return normalized;
|
|
6348
|
+
const loader = contextLoader ?? preloadedOnlyDocumentLoader;
|
|
6349
|
+
try {
|
|
6350
|
+
const [before, after] = await Promise.all([_fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
|
|
6351
|
+
format: "application/n-quads",
|
|
6352
|
+
documentLoader: loader
|
|
6353
|
+
}), _fedify_vocab_runtime_jsonld.default.canonize(normalized, {
|
|
6354
|
+
format: "application/n-quads",
|
|
6355
|
+
documentLoader: loader
|
|
6356
|
+
})]);
|
|
6357
|
+
if (before === after) return normalized;
|
|
6358
|
+
logger$2.warn("Wrapping scalar attachment values in arrays would change the canonical form of the JSON-LD document; leaving it unchanged. This usually means the active JSON-LD context redefines the `attachment` term. Document: {id}; type: {type}; context: {context}.", getLogSafeJsonLdMetadata(jsonLd));
|
|
6359
|
+
} catch (error) {
|
|
6360
|
+
logger$2.debug("Failed to verify attachment array normalization equivalence via JSON-LD canonicalization; leaving the JSON-LD document unchanged.\n{error}", { error });
|
|
6361
|
+
}
|
|
6362
|
+
return jsonLd;
|
|
6226
6363
|
}
|
|
6227
6364
|
/**
|
|
6228
|
-
*
|
|
6229
|
-
*
|
|
6230
|
-
*
|
|
6231
|
-
* If the document is not signed, this function returns `false`.
|
|
6232
|
-
* @param jsonLd The JSON-LD document to verify.
|
|
6233
|
-
* @param options Options for verifying the document.
|
|
6234
|
-
* @returns `true` if the document is authentic; `false` otherwise.
|
|
6365
|
+
* Applies Fedify's internal JSON-LD wire-format interoperability workarounds
|
|
6366
|
+
* to locally generated outgoing activities before they are signed, enqueued,
|
|
6367
|
+
* or sent.
|
|
6235
6368
|
*/
|
|
6236
|
-
async function
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
/** @internal */
|
|
6240
|
-
async function verifyCompactJsonLd(jsonLd, options = {}) {
|
|
6241
|
-
return await verifyJsonLdInternal(jsonLd, options, false);
|
|
6242
|
-
}
|
|
6243
|
-
async function verifyJsonLdInternal(jsonLd, options, compact) {
|
|
6244
|
-
return await (options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider()).getTracer(name, version).startActiveSpan("ld_signatures.verify", async (span) => {
|
|
6245
|
-
const start = performance.now();
|
|
6246
|
-
let verified = false;
|
|
6247
|
-
let threw = false;
|
|
6248
|
-
let signatureType;
|
|
6249
|
-
try {
|
|
6250
|
-
const verificationOptions = hasSignature(jsonLd) ? {
|
|
6251
|
-
...options,
|
|
6252
|
-
contextLoader: getNormalizationContextLoader(options.contextLoader)
|
|
6253
|
-
} : options;
|
|
6254
|
-
const compacted = compact ? hasSignature(jsonLd) ? await compactJsonLd(jsonLd, options.contextLoader) : jsonLd : jsonLd;
|
|
6255
|
-
const object = await _fedify_vocab.Object.fromJsonLd(compacted, verificationOptions);
|
|
6256
|
-
if (object.id != null) span.setAttribute("activitypub.object.id", object.id.href);
|
|
6257
|
-
span.setAttribute("activitypub.object.type", (0, _fedify_vocab.getTypeId)(object).href);
|
|
6258
|
-
const sig = getLdSignatureObject(jsonLd);
|
|
6259
|
-
if (sig != null) {
|
|
6260
|
-
if (typeof sig.creator === "string") span.setAttribute("ld_signatures.key_id", sig.creator);
|
|
6261
|
-
if (typeof sig.signatureValue === "string") span.setAttribute("ld_signatures.signature", sig.signatureValue);
|
|
6262
|
-
if (typeof sig.type === "string") {
|
|
6263
|
-
span.setAttribute("ld_signatures.type", sig.type);
|
|
6264
|
-
if (LD_KNOWN_SIGNATURE_TYPES.has(sig.type)) signatureType = sig.type;
|
|
6265
|
-
}
|
|
6266
|
-
}
|
|
6267
|
-
const attributions = new Set(object.attributionIds.map((uri) => uri.href));
|
|
6268
|
-
if (object instanceof _fedify_vocab.Activity) for (const uri of object.actorIds) attributions.add(uri.href);
|
|
6269
|
-
const key = await verifySignature(compacted, verificationOptions);
|
|
6270
|
-
if (key == null) return false;
|
|
6271
|
-
if (key.ownerId == null) {
|
|
6272
|
-
logger$2.debug("Key {keyId} has no owner.", { keyId: key.id?.href });
|
|
6273
|
-
return false;
|
|
6274
|
-
}
|
|
6275
|
-
attributions.delete(key.ownerId.href);
|
|
6276
|
-
if (attributions.size > 0) {
|
|
6277
|
-
logger$2.debug("Some attributions are not authenticated by the Linked Data Signatures: {attributions}.", { attributions: [...attributions] });
|
|
6278
|
-
return false;
|
|
6279
|
-
}
|
|
6280
|
-
verified = true;
|
|
6281
|
-
return true;
|
|
6282
|
-
} catch (error) {
|
|
6283
|
-
threw = true;
|
|
6284
|
-
span.setStatus({
|
|
6285
|
-
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
6286
|
-
message: String(error)
|
|
6287
|
-
});
|
|
6288
|
-
throw error;
|
|
6289
|
-
} finally {
|
|
6290
|
-
const classified = threw ? "error" : verified ? "verified" : hasLdSignatureProperty(jsonLd) ? "rejected" : "missing";
|
|
6291
|
-
getFederationMetrics(options.meterProvider).recordSignatureVerificationDuration(getDurationMs(start), "linked_data", classified, { ldType: signatureType });
|
|
6292
|
-
span.end();
|
|
6293
|
-
}
|
|
6294
|
-
});
|
|
6295
|
-
}
|
|
6296
|
-
async function hashJsonLd(jsonLd, contextLoader) {
|
|
6297
|
-
const canon = await _fedify_vocab_runtime_jsonld.default.canonize(jsonLd, {
|
|
6298
|
-
format: "application/n-quads",
|
|
6299
|
-
documentLoader: contextLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)()
|
|
6300
|
-
});
|
|
6301
|
-
const encoder = new TextEncoder();
|
|
6302
|
-
return (0, byte_encodings_hex.encodeHex)(await crypto.subtle.digest("SHA-256", encoder.encode(canon)));
|
|
6369
|
+
async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, options = {}) {
|
|
6370
|
+
jsonLd = await normalizePublicAudience(jsonLd, contextLoader, options);
|
|
6371
|
+
return await normalizeAttachmentArrays(jsonLd, contextLoader, options);
|
|
6303
6372
|
}
|
|
6304
6373
|
//#endregion
|
|
6305
6374
|
//#region src/sig/proof.ts
|
|
@@ -7024,8 +7093,37 @@ async function expandPortableObjectRoot(jsonLd, contextLoader) {
|
|
|
7024
7093
|
}
|
|
7025
7094
|
};
|
|
7026
7095
|
}
|
|
7096
|
+
/**
|
|
7097
|
+
* Gets the DID of a portable object ID, which is either an `ap:` or
|
|
7098
|
+
* `ap+ef61:` URI or an FEP-ef61 compatible identifier.
|
|
7099
|
+
* @throws {TypeError} If the ID is malformed.
|
|
7100
|
+
*/
|
|
7101
|
+
function getObjectDid(id) {
|
|
7102
|
+
try {
|
|
7103
|
+
(0, _fedify_vocab_runtime.parseIri)(id);
|
|
7104
|
+
if (PORTABLE_OBJECT_ID_PATTERN.test(id)) return (0, _fedify_vocab_runtime.getFe34Origin)(id);
|
|
7105
|
+
const portableId = (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
|
|
7106
|
+
if (portableId != null) return (0, _fedify_vocab_runtime.getFe34Origin)(portableId);
|
|
7107
|
+
} catch (error) {
|
|
7108
|
+
if (!(error instanceof TypeError)) throw error;
|
|
7109
|
+
throw new InvalidPortableObjectIdError(id, { cause: error });
|
|
7110
|
+
}
|
|
7111
|
+
throw new InvalidPortableObjectIdError(id);
|
|
7112
|
+
}
|
|
7113
|
+
/**
|
|
7114
|
+
* The error thrown when a document's ID looks portable, i.e., it is an `ap:`
|
|
7115
|
+
* or `ap+ef61:` URI or looks like an FEP-ef61 compatible identifier, but is
|
|
7116
|
+
* malformed.
|
|
7117
|
+
* @internal
|
|
7118
|
+
*/
|
|
7119
|
+
var InvalidPortableObjectIdError = class extends TypeError {
|
|
7120
|
+
constructor(id, options) {
|
|
7121
|
+
super(`Invalid portable object ID: ${id}`, options);
|
|
7122
|
+
this.name = "InvalidPortableObjectIdError";
|
|
7123
|
+
}
|
|
7124
|
+
};
|
|
7027
7125
|
async function preparePortableObjectProof(jsonLd, options) {
|
|
7028
|
-
if (isJsonLdNode(jsonLd) && typeof jsonLd["@id"] === "string" && !
|
|
7126
|
+
if (isJsonLdNode(jsonLd) && typeof jsonLd["@id"] === "string" && !isPortableId(jsonLd["@id"])) return {
|
|
7029
7127
|
prepared: false,
|
|
7030
7128
|
result: {
|
|
7031
7129
|
verified: false,
|
|
@@ -7034,15 +7132,15 @@ async function preparePortableObjectProof(jsonLd, options) {
|
|
|
7034
7132
|
};
|
|
7035
7133
|
const { root, proofContextLoader } = await expandPortableObjectRoot(jsonLd, options.contextLoader);
|
|
7036
7134
|
const id = root["@id"];
|
|
7037
|
-
if (typeof id !== "string" || !
|
|
7135
|
+
if (typeof id !== "string" || !isPortableId(id)) return {
|
|
7038
7136
|
prepared: false,
|
|
7039
7137
|
result: {
|
|
7040
7138
|
verified: false,
|
|
7041
7139
|
reason: { type: "notPortableObject" }
|
|
7042
7140
|
}
|
|
7043
7141
|
};
|
|
7142
|
+
const objectDid = getObjectDid(id);
|
|
7044
7143
|
const objectId = (0, _fedify_vocab_runtime.parseIri)(id);
|
|
7045
|
-
(0, _fedify_vocab_runtime.getFe34Origin)(objectId);
|
|
7046
7144
|
const objectType = classifyFep2277CoreType(root);
|
|
7047
7145
|
if (objectType === "verificationMethod" || objectType === "publicKey" || objectType === "link") return {
|
|
7048
7146
|
prepared: false,
|
|
@@ -7147,7 +7245,7 @@ async function preparePortableObjectProof(jsonLd, options) {
|
|
|
7147
7245
|
}
|
|
7148
7246
|
};
|
|
7149
7247
|
}
|
|
7150
|
-
if (
|
|
7248
|
+
if ((0, _fedify_vocab_runtime.getFe34Origin)(verificationMethod) !== objectDid) return {
|
|
7151
7249
|
prepared: false,
|
|
7152
7250
|
result: {
|
|
7153
7251
|
verified: false,
|
|
@@ -7206,15 +7304,24 @@ async function verifyPortableObjectProofPolicy(jsonLd, key, options = {}) {
|
|
|
7206
7304
|
* collection without a proof is reported separately so a caller can apply a
|
|
7207
7305
|
* gateway trust policy. Embedded portable objects are not traversed.
|
|
7208
7306
|
*
|
|
7307
|
+
* A portable object is a document whose ID is an `ap:` or `ap+ef61:` URI, or
|
|
7308
|
+
* an [FEP-ef61] compatible identifier such as
|
|
7309
|
+
* `https://gw.example/.well-known/apgateway/did:key:z6Mk…/actor`, which
|
|
7310
|
+
* stands for the portable ID `ap+ef61://did:key:z6Mk…/actor`.
|
|
7311
|
+
*
|
|
7209
7312
|
* Every proof must use a DID URL whose DID matches the portable object's
|
|
7210
|
-
* authority, and every proof must
|
|
7313
|
+
* authority, i.e., the DID of its canonical portable ID, and every proof must
|
|
7314
|
+
* pass {@link verifyProof}. The proofs are verified over the document as
|
|
7315
|
+
* given; a compatible identifier is never rewritten into a portable ID.
|
|
7316
|
+
*
|
|
7317
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
7211
7318
|
*
|
|
7212
7319
|
* @param jsonLd The JSON-LD document to verify.
|
|
7213
7320
|
* @param options Additional options. See also
|
|
7214
7321
|
* {@link VerifyPortableObjectProofOptions}.
|
|
7215
7322
|
* @returns The detailed portable proof-policy result.
|
|
7216
7323
|
* @throws {TypeError} If the input is not a single JSON-LD object or has a
|
|
7217
|
-
* malformed portable ID.
|
|
7324
|
+
* malformed portable ID or compatible identifier.
|
|
7218
7325
|
* @since 2.4.0
|
|
7219
7326
|
*/
|
|
7220
7327
|
async function verifyPortableObjectProof(jsonLd, options = {}) {
|
|
@@ -7314,7 +7421,7 @@ async function verifyObject(cls, jsonLd, options = {}) {
|
|
|
7314
7421
|
};
|
|
7315
7422
|
const proofDocumentLoader = async (url, loaderOptions) => {
|
|
7316
7423
|
const remoteDocument = await baseDocumentLoader(url, loaderOptions);
|
|
7317
|
-
if (
|
|
7424
|
+
if (isPortableId(url)) loadedPortableProofs++;
|
|
7318
7425
|
const reference = normalizeDocumentUrl(url);
|
|
7319
7426
|
const candidateIndex = rawProofCandidates.findIndex((candidate, index) => !hydratedCandidates.has(index) && !rawProofCandidatePool.used.has(index) && candidate.reference === reference);
|
|
7320
7427
|
if (candidateIndex >= 0) {
|
|
@@ -7364,11 +7471,13 @@ async function verifyObject(cls, jsonLd, options = {}) {
|
|
|
7364
7471
|
return object;
|
|
7365
7472
|
}
|
|
7366
7473
|
function deleteAuthenticatedAttribution(attributions, controllerId, verificationMethodId) {
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7474
|
+
if (!hasCryptographicOrigin(controllerId.href) && !isPortableId(controllerId)) {
|
|
7475
|
+
attributions.delete(controllerId.href);
|
|
7476
|
+
return;
|
|
7477
|
+
}
|
|
7478
|
+
const did = getPortableDid(controllerId);
|
|
7479
|
+
if (did == null || !hasCryptographicOrigin(verificationMethodId.href) || getPortableDid(verificationMethodId) !== did) return;
|
|
7480
|
+
for (const attribution of [...attributions]) if ((hasCryptographicOrigin(attribution) || isPortableId(attribution)) && getPortableDid(attribution) === did) attributions.delete(attribution);
|
|
7372
7481
|
}
|
|
7373
7482
|
function hasCryptographicOrigin(iri) {
|
|
7374
7483
|
return /^did:/i.test(iri) || /^ap(?:\+ef61)?:\/\//i.test(iri);
|
|
@@ -7387,13 +7496,19 @@ const SEC = "https://w3id.org/security#";
|
|
|
7387
7496
|
*
|
|
7388
7497
|
* A gateway key is accepted only if:
|
|
7389
7498
|
*
|
|
7390
|
-
* - the document is an actor whose portable ID
|
|
7391
|
-
* the compatible identifier
|
|
7499
|
+
* - the document is an actor whose portable ID, i.e., its `ap:` ID or
|
|
7500
|
+
* the canonical portable ID of its compatible identifier, is the one
|
|
7501
|
+
* the key ID is the compatible identifier of;
|
|
7392
7502
|
* - the document has a valid [FEP-8b32] Object Integrity Proof made by
|
|
7393
7503
|
* the DID of its ID, i.e., it satisfies the [FEP-ef61] proof policy;
|
|
7394
7504
|
* - the document embeds a `Multikey` with that ID in its `assertionMethod`,
|
|
7395
|
-
* whose `controller` is the actor, as [FEP-521a] requires
|
|
7396
|
-
* embedded in its `publicKey` with the same ID
|
|
7505
|
+
* whose `controller` is the actor, as [FEP-521a] requires, and a key
|
|
7506
|
+
* embedded in its `publicKey` with the same ID agrees with it; or, if
|
|
7507
|
+
* no `assertionMethod` entry has that ID, the document embeds
|
|
7508
|
+
* a `CryptographicKey` with that ID in its `publicKey`, whose `owner` is
|
|
7509
|
+
* the actor, as some publishers, e.g., tootik, list their RSA keys only
|
|
7510
|
+
* there;
|
|
7511
|
+
* - no more than one entry of either property has that ID; and
|
|
7397
7512
|
* - the key ID's origin is one of the actor's `gateways`.
|
|
7398
7513
|
*
|
|
7399
7514
|
* [FEP-8b32]: https://w3id.org/fep/8b32
|
|
@@ -7442,6 +7557,21 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
|
|
|
7442
7557
|
if (!result.verified) return reject("The actor document does not satisfy the FEP-ef61 proof policy: " + result.reason.type);
|
|
7443
7558
|
if (root == null) return reject("The actor document could not be expanded.");
|
|
7444
7559
|
const refuse = (url) => Promise.reject(/* @__PURE__ */ new Error(`Refusing to fetch ${url}.`));
|
|
7560
|
+
const parseEmbeddedPublicKey = async (node) => {
|
|
7561
|
+
if (isReference(node)) return null;
|
|
7562
|
+
let key;
|
|
7563
|
+
try {
|
|
7564
|
+
key = await _fedify_vocab.CryptographicKey.fromJsonLd(node, {
|
|
7565
|
+
documentLoader: refuse,
|
|
7566
|
+
contextLoader: options.contextLoader,
|
|
7567
|
+
tracerProvider: options.tracerProvider
|
|
7568
|
+
});
|
|
7569
|
+
} catch (error) {
|
|
7570
|
+
if (!(error instanceof TypeError)) throw error;
|
|
7571
|
+
return null;
|
|
7572
|
+
}
|
|
7573
|
+
return key.publicKey == null ? null : key;
|
|
7574
|
+
};
|
|
7445
7575
|
let verifiedActor;
|
|
7446
7576
|
try {
|
|
7447
7577
|
verifiedActor = await _fedify_vocab.Object.fromJsonLd(root, {
|
|
@@ -7455,41 +7585,42 @@ async function verifyPortableGatewayKeyDocument(document, actor, keyId, options
|
|
|
7455
7585
|
}
|
|
7456
7586
|
if (!isPortableActorDocument(verifiedActor) || getCanonicalPortableId(verifiedActor.id) !== claimedActorId) return reject("The key ID is not a compatible identifier of the actor whose document is signed.");
|
|
7457
7587
|
actor = verifiedActor;
|
|
7458
|
-
const
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
tracerProvider: options.tracerProvider
|
|
7466
|
-
});
|
|
7467
|
-
} catch (error) {
|
|
7468
|
-
if (!(error instanceof TypeError)) throw error;
|
|
7469
|
-
return reject("The key in the assertionMethod is malformed.");
|
|
7470
|
-
}
|
|
7471
|
-
if (multikey.publicKey == null) return reject("The key in the assertionMethod has no public key.");
|
|
7472
|
-
if (multikey.controllerId == null || getCanonicalPortableId(multikey.controllerId) !== actorId) return reject("The controller of the key is not the actor.");
|
|
7473
|
-
const publicKeyNode = findEmbeddedNode(root[`${SEC}publicKey`], keyId);
|
|
7474
|
-
if (publicKeyNode != null) {
|
|
7475
|
-
let publicKey;
|
|
7588
|
+
const assertionNodes = findNodes(root[`${SEC}assertionMethod`], keyId);
|
|
7589
|
+
const publicKeyNodes = findNodes(root[`${SEC}publicKey`], keyId);
|
|
7590
|
+
if (assertionNodes.length > 1 || publicKeyNodes.length > 1) return reject("The actor document has more than one key with the ID.");
|
|
7591
|
+
let publicKey;
|
|
7592
|
+
if (assertionNodes.length > 0) {
|
|
7593
|
+
if (isReference(assertionNodes[0])) return reject("The actor document does not embed the key in its assertionMethod.");
|
|
7594
|
+
let multikey;
|
|
7476
7595
|
try {
|
|
7477
|
-
|
|
7596
|
+
multikey = await _fedify_vocab.Multikey.fromJsonLd(assertionNodes[0], {
|
|
7478
7597
|
documentLoader: refuse,
|
|
7479
7598
|
contextLoader: options.contextLoader,
|
|
7480
7599
|
tracerProvider: options.tracerProvider
|
|
7481
7600
|
});
|
|
7482
7601
|
} catch (error) {
|
|
7483
7602
|
if (!(error instanceof TypeError)) throw error;
|
|
7484
|
-
return reject("The key in the
|
|
7603
|
+
return reject("The key in the assertionMethod is malformed.");
|
|
7485
7604
|
}
|
|
7486
|
-
if (
|
|
7605
|
+
if (multikey.publicKey == null) return reject("The key in the assertionMethod has no public key.");
|
|
7606
|
+
if (multikey.controllerId == null || getCanonicalPortableId(multikey.controllerId) !== actorId) return reject("The controller of the key is not the actor.");
|
|
7607
|
+
publicKey = multikey.publicKey;
|
|
7608
|
+
if (publicKeyNodes.length > 0 && !isReference(publicKeyNodes[0])) {
|
|
7609
|
+
const embedded = await parseEmbeddedPublicKey(publicKeyNodes[0]);
|
|
7610
|
+
if (embedded == null || !await isSamePublicKey(embedded.publicKey, publicKey) || embedded.ownerId != null && getCanonicalPortableId(embedded.ownerId) !== actorId) return reject("The key in the publicKey does not agree with the one in the assertionMethod.");
|
|
7611
|
+
}
|
|
7612
|
+
} else {
|
|
7613
|
+
if (publicKeyNodes.length < 1) return reject("The actor document embeds the key in neither its assertionMethod nor its publicKey.");
|
|
7614
|
+
const embedded = await parseEmbeddedPublicKey(publicKeyNodes[0]);
|
|
7615
|
+
if (embedded == null) return reject("The key in the publicKey is not embedded or malformed.");
|
|
7616
|
+
if (embedded.ownerId == null || getCanonicalPortableId(embedded.ownerId) !== actorId) return reject("The owner of the key is not the actor.");
|
|
7617
|
+
publicKey = embedded.publicKey;
|
|
7487
7618
|
}
|
|
7488
7619
|
if (!actor.gateways.some((gateway) => (0, _fedify_vocab_runtime.isGatewayUrl)(gateway) && gateway.origin === keyId.origin)) return reject(`The gateway ${keyId.origin} is not listed in the actor's gateways.`);
|
|
7489
7620
|
const key = new _fedify_vocab.CryptographicKey({
|
|
7490
7621
|
id: keyId,
|
|
7491
7622
|
owner: actor.id,
|
|
7492
|
-
publicKey
|
|
7623
|
+
publicKey
|
|
7493
7624
|
});
|
|
7494
7625
|
return {
|
|
7495
7626
|
type: "verified",
|
|
@@ -7539,24 +7670,21 @@ async function fetchPortableGatewayKey(keyId, options = {}) {
|
|
|
7539
7670
|
contextLoader
|
|
7540
7671
|
});
|
|
7541
7672
|
}
|
|
7542
|
-
function
|
|
7543
|
-
if (!Array.isArray(values)) return
|
|
7673
|
+
function findNodes(values, id) {
|
|
7674
|
+
if (!Array.isArray(values)) return [];
|
|
7675
|
+
const nodes = [];
|
|
7544
7676
|
for (const value of values) {
|
|
7545
7677
|
if (value == null || typeof value !== "object" || Array.isArray(value)) continue;
|
|
7546
7678
|
const node = value;
|
|
7547
7679
|
const nodeId = node["@id"];
|
|
7548
|
-
if (typeof nodeId !== "string") continue;
|
|
7549
|
-
|
|
7550
|
-
try {
|
|
7551
|
-
parsed = new URL(nodeId);
|
|
7552
|
-
} catch {
|
|
7553
|
-
continue;
|
|
7554
|
-
}
|
|
7555
|
-
if (parsed.href !== id.href) continue;
|
|
7556
|
-
if (Object.keys(node).every((key) => key === "@id")) continue;
|
|
7557
|
-
return node;
|
|
7680
|
+
if (typeof nodeId !== "string" || !URL.canParse(nodeId)) continue;
|
|
7681
|
+
if (new URL(nodeId).href === id.href) nodes.push(node);
|
|
7558
7682
|
}
|
|
7559
|
-
return
|
|
7683
|
+
return nodes;
|
|
7684
|
+
}
|
|
7685
|
+
/** A node with nothing but an `@id` is a reference, not an embedded key. */
|
|
7686
|
+
function isReference(node) {
|
|
7687
|
+
return Object.keys(node).every((key) => key === "@id");
|
|
7560
7688
|
}
|
|
7561
7689
|
//#endregion
|
|
7562
7690
|
//#region src/sig/http.ts
|
|
@@ -7748,6 +7876,81 @@ function parseRfc9421Signature(signature) {
|
|
|
7748
7876
|
for (const [key, value] of Object.entries(dict)) if (value.value instanceof Uint8Array) result[key] = value.value;
|
|
7749
7877
|
return result;
|
|
7750
7878
|
}
|
|
7879
|
+
/**
|
|
7880
|
+
* Parses the `Signature` header of draft-cavage HTTP Signatures into its
|
|
7881
|
+
* fields, e.g., `keyId` and `headers`.
|
|
7882
|
+
*/
|
|
7883
|
+
function parseDraftSignature(signature) {
|
|
7884
|
+
return Object.fromEntries(signature.split(",").map((pair) => pair.match(/^\s*([A-Za-z]+)=(?:"([^"]*)"|(\d+))\s*$/)).filter((m) => m != null).map((m) => [m[1], m[2] ?? m[3]]));
|
|
7885
|
+
}
|
|
7886
|
+
/**
|
|
7887
|
+
* Lists the HTTP Signatures that a request claims to carry, without verifying
|
|
7888
|
+
* any of them. Signatures whose key IDs are not URLs are left out.
|
|
7889
|
+
* @param request The request.
|
|
7890
|
+
* @returns The signatures; RFC 9421 ones if the request has
|
|
7891
|
+
* a `Signature-Input` header, or a draft-cavage one otherwise.
|
|
7892
|
+
* @internal
|
|
7893
|
+
*/
|
|
7894
|
+
function listRequestSignatures(request) {
|
|
7895
|
+
const signatureInput = request.headers.get("Signature-Input");
|
|
7896
|
+
if (signatureInput != null) {
|
|
7897
|
+
const signatures = [];
|
|
7898
|
+
const inputs = parseRfc9421SignatureInput(signatureInput);
|
|
7899
|
+
for (const [label, input] of Object.entries(inputs)) {
|
|
7900
|
+
const keyId = parseKeyId(input.keyId);
|
|
7901
|
+
if (keyId == null) continue;
|
|
7902
|
+
signatures.push({
|
|
7903
|
+
keyId,
|
|
7904
|
+
label,
|
|
7905
|
+
components: input.components
|
|
7906
|
+
});
|
|
7907
|
+
}
|
|
7908
|
+
return signatures;
|
|
7909
|
+
}
|
|
7910
|
+
const signature = request.headers.get("Signature");
|
|
7911
|
+
if (signature == null) return [];
|
|
7912
|
+
const values = parseDraftSignature(signature);
|
|
7913
|
+
const keyId = parseKeyId(values.keyId);
|
|
7914
|
+
if (keyId == null) return [];
|
|
7915
|
+
return [{
|
|
7916
|
+
keyId,
|
|
7917
|
+
label: null,
|
|
7918
|
+
components: (values.headers ?? "").split(/\s+/g).filter((name) => name !== "").map((name) => ({
|
|
7919
|
+
value: name,
|
|
7920
|
+
params: {}
|
|
7921
|
+
}))
|
|
7922
|
+
}];
|
|
7923
|
+
}
|
|
7924
|
+
/**
|
|
7925
|
+
* Makes a copy of a request that carries only one of its RFC 9421 signatures,
|
|
7926
|
+
* so that verifying the copy verifies that signature and no other.
|
|
7927
|
+
* A draft-cavage signature is the only one in its request, so the request is
|
|
7928
|
+
* just copied.
|
|
7929
|
+
* @param request The request, whose body must not be consumed.
|
|
7930
|
+
* @param signature One of the signatures that {@link listRequestSignatures}
|
|
7931
|
+
* lists for the request.
|
|
7932
|
+
* @returns The copy of the request.
|
|
7933
|
+
* @internal
|
|
7934
|
+
*/
|
|
7935
|
+
function selectRequestSignature(request, signature) {
|
|
7936
|
+
const copy = request.clone();
|
|
7937
|
+
if (signature.label == null) return copy;
|
|
7938
|
+
const { label } = signature;
|
|
7939
|
+
const pick = (header) => {
|
|
7940
|
+
if (header == null) return "";
|
|
7941
|
+
let dict;
|
|
7942
|
+
try {
|
|
7943
|
+
dict = (0, structured_field_values.decodeDict)(header);
|
|
7944
|
+
} catch {
|
|
7945
|
+
return "";
|
|
7946
|
+
}
|
|
7947
|
+
return Object.hasOwn(dict, label) ? (0, structured_field_values.encodeDict)({ [label]: dict[label] }) : "";
|
|
7948
|
+
};
|
|
7949
|
+
const headers = new Headers(copy.headers);
|
|
7950
|
+
headers.set("Signature-Input", pick(headers.get("Signature-Input")));
|
|
7951
|
+
headers.set("Signature", pick(headers.get("Signature")));
|
|
7952
|
+
return new Request(copy, { headers });
|
|
7953
|
+
}
|
|
7751
7954
|
async function signRequestRfc9421(request, privateKey, keyId, span, currentTime, bodyBuffer, rfc9421Options) {
|
|
7752
7955
|
if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
|
|
7753
7956
|
const url = new URL(request.url);
|
|
@@ -7979,7 +8182,7 @@ async function verifyRequestDraft(request, span, metricsContext, { documentLoade
|
|
|
7979
8182
|
logger.debug("Failed to verify; no Signature header found.", { headers: Object.fromEntries(request.headers.entries()) });
|
|
7980
8183
|
return noSignatureResult();
|
|
7981
8184
|
}
|
|
7982
|
-
const sigValues =
|
|
8185
|
+
const sigValues = parseDraftSignature(sigHeader);
|
|
7983
8186
|
const parsedKeyId = parseKeyId(sigValues.keyId);
|
|
7984
8187
|
const dateHeader = request.headers.get("Date");
|
|
7985
8188
|
if (dateHeader == null) {
|
|
@@ -8658,6 +8861,12 @@ Object.defineProperty(exports, "InvalidContextReferenceError", {
|
|
|
8658
8861
|
return InvalidContextReferenceError;
|
|
8659
8862
|
}
|
|
8660
8863
|
});
|
|
8864
|
+
Object.defineProperty(exports, "InvalidPortableObjectIdError", {
|
|
8865
|
+
enumerable: true,
|
|
8866
|
+
get: function() {
|
|
8867
|
+
return InvalidPortableObjectIdError;
|
|
8868
|
+
}
|
|
8869
|
+
});
|
|
8661
8870
|
Object.defineProperty(exports, "Temporal", {
|
|
8662
8871
|
enumerable: true,
|
|
8663
8872
|
get: function() {
|
|
@@ -8784,6 +8993,12 @@ Object.defineProperty(exports, "getNormalizationContextLoader", {
|
|
|
8784
8993
|
return getNormalizationContextLoader;
|
|
8785
8994
|
}
|
|
8786
8995
|
});
|
|
8996
|
+
Object.defineProperty(exports, "getPortableDid", {
|
|
8997
|
+
enumerable: true,
|
|
8998
|
+
get: function() {
|
|
8999
|
+
return getPortableDid;
|
|
9000
|
+
}
|
|
9001
|
+
});
|
|
8787
9002
|
Object.defineProperty(exports, "getRemoteHost", {
|
|
8788
9003
|
enumerable: true,
|
|
8789
9004
|
get: function() {
|
|
@@ -8850,6 +9065,12 @@ Object.defineProperty(exports, "isInvalidUrlTypeError", {
|
|
|
8850
9065
|
return isInvalidUrlTypeError;
|
|
8851
9066
|
}
|
|
8852
9067
|
});
|
|
9068
|
+
Object.defineProperty(exports, "isPortableId", {
|
|
9069
|
+
enumerable: true,
|
|
9070
|
+
get: function() {
|
|
9071
|
+
return isPortableId;
|
|
9072
|
+
}
|
|
9073
|
+
});
|
|
8853
9074
|
Object.defineProperty(exports, "isPortableUri", {
|
|
8854
9075
|
enumerable: true,
|
|
8855
9076
|
get: function() {
|
|
@@ -8862,6 +9083,12 @@ Object.defineProperty(exports, "isSamePublicKey", {
|
|
|
8862
9083
|
return isSamePublicKey;
|
|
8863
9084
|
}
|
|
8864
9085
|
});
|
|
9086
|
+
Object.defineProperty(exports, "listRequestSignatures", {
|
|
9087
|
+
enumerable: true,
|
|
9088
|
+
get: function() {
|
|
9089
|
+
return listRequestSignatures;
|
|
9090
|
+
}
|
|
9091
|
+
});
|
|
8865
9092
|
Object.defineProperty(exports, "name", {
|
|
8866
9093
|
enumerable: true,
|
|
8867
9094
|
get: function() {
|
|
@@ -8886,6 +9113,12 @@ Object.defineProperty(exports, "parseRfc9421SignatureInput", {
|
|
|
8886
9113
|
return parseRfc9421SignatureInput;
|
|
8887
9114
|
}
|
|
8888
9115
|
});
|
|
9116
|
+
Object.defineProperty(exports, "preloadedOnlyDocumentLoader", {
|
|
9117
|
+
enumerable: true,
|
|
9118
|
+
get: function() {
|
|
9119
|
+
return preloadedOnlyDocumentLoader;
|
|
9120
|
+
}
|
|
9121
|
+
});
|
|
8889
9122
|
Object.defineProperty(exports, "recordCircuitBreakerStateChange", {
|
|
8890
9123
|
enumerable: true,
|
|
8891
9124
|
get: function() {
|
|
@@ -8958,6 +9191,12 @@ Object.defineProperty(exports, "registerQueueDepthGauge", {
|
|
|
8958
9191
|
return registerQueueDepthGauge;
|
|
8959
9192
|
}
|
|
8960
9193
|
});
|
|
9194
|
+
Object.defineProperty(exports, "selectRequestSignature", {
|
|
9195
|
+
enumerable: true,
|
|
9196
|
+
get: function() {
|
|
9197
|
+
return selectRequestSignature;
|
|
9198
|
+
}
|
|
9199
|
+
});
|
|
8961
9200
|
Object.defineProperty(exports, "signJsonLd", {
|
|
8962
9201
|
enumerable: true,
|
|
8963
9202
|
get: function() {
|