@fedify/fedify 2.4.0-dev.2077 → 2.4.0-dev.2093
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-i3jxVe9m.mjs → builder-Ld4Y9ymR.mjs} +34 -3
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-Cayy-ECj.mjs → compound-proof-CTx_lewQ.mjs} +191 -13
- package/dist/{deno-Djkzd-G_.mjs → deno-8t8k_6Yv.mjs} +1 -1
- package/dist/{docloader-dA0RswUV.mjs → docloader-DJKcsAiF.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.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 +513 -6
- 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-CFd0Jhhv.cjs → federation-aSHO04ob.cjs} +472 -74
- package/dist/{http-CCJ97Jql.mjs → http-B2ftY0Ha.mjs} +4 -4
- package/dist/{http-Dvy_IwRa.cjs → http-DSaSAN7X.cjs} +1609 -1457
- package/dist/{http-aqh3yFdM.js → http-DyT7DP1Y.js} +1518 -1390
- package/dist/{key-IxTTC8Yf.mjs → key-Dwm4VswT.mjs} +39 -99
- package/dist/{kv-cache-BP69NV0P.cjs → kv-cache-BSJAlP4C.cjs} +1 -1
- package/dist/{kv-cache-Bwwbh-xR.mjs → kv-cache-D0yYukqk.mjs} +1 -1
- package/dist/{kv-cache-Y6SDRSX6.js → kv-cache-DmroAz3M.js} +1 -1
- package/dist/{ld-l0B9nREb.mjs → ld-BYZOoDzZ.mjs} +3 -3
- package/dist/{metrics-D1p6hv4z.mjs → metrics-B7YwKTFr.mjs} +1 -1
- package/dist/{middleware-BDjCUHMS.mjs → middleware-BNeqKSsL.mjs} +1 -1
- package/dist/{middleware-SSfryVzY.js → middleware-Blq0882d.js} +480 -82
- package/dist/{middleware-BMgzqBtr.mjs → middleware-E_0IH72m.mjs} +278 -80
- 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.cjs +5 -5
- package/dist/mod.d.cts +1 -1
- package/dist/mod.d.ts +1 -1
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-DGbYmXqU.mjs → owner-Cgx2Dc7I.mjs} +5 -4
- package/dist/{owner-jH1z_YfD.js → owner-DAGSC5gX.js} +2 -2
- package/dist/{owner-BpOxL5PG.cjs → owner-szIZl4qI.cjs} +2 -2
- package/dist/{portable-key-CCwJGSsU.mjs → portable-key-Cn1WJSN1.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-CVL7JnZd.mjs → proof-DVXC6nYz.mjs} +59 -18
- package/dist/{send-Ejy1jOz9.mjs → send-D-hOvYKy.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 +2 -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-Dpy1B1Ad.js → sig-B5Ell7pO.js} +19 -3
- package/dist/{sig-xoqy3ATq.cjs → sig-CXOKN4Br.cjs} +19 -3
- package/dist/{temporal-DNJ1cNpr.mjs → temporal-CqpKGEPk.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- 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.2093+e9706a54";
|
|
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.",
|
|
@@ -4945,99 +4679,6 @@ function getDurationMs(start) {
|
|
|
4945
4679
|
return Math.max(0, performance.now() - start);
|
|
4946
4680
|
}
|
|
4947
4681
|
//#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;
|
|
5039
|
-
}
|
|
5040
|
-
//#endregion
|
|
5041
4682
|
//#region src/sig/key.ts
|
|
5042
4683
|
/**
|
|
5043
4684
|
* Checks if the given key is valid and supported. No-op if the key is valid,
|
|
@@ -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,
|
|
@@ -5314,992 +4966,1409 @@ async function fetchKeyDetailed(keyId, cls, options = {}) {
|
|
|
5314
4966
|
logger.debug("Entry {keyId} found in cache with preserved fetch failure details.", { keyId });
|
|
5315
4967
|
return {
|
|
5316
4968
|
key: null,
|
|
5317
|
-
cached: true,
|
|
5318
|
-
fetchError
|
|
4969
|
+
cached: true,
|
|
4970
|
+
fetchError
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
logger.debug("Entry {keyId} found in cache, but no fetch failure details are available.", { keyId });
|
|
4974
|
+
return {
|
|
4975
|
+
key: null,
|
|
4976
|
+
cached: true
|
|
4977
|
+
};
|
|
4978
|
+
}, async (error, cacheKey, keyId, keyCache, logger) => {
|
|
4979
|
+
logger.debug("Failed to fetch key {keyId}.", {
|
|
4980
|
+
keyId,
|
|
4981
|
+
error
|
|
4982
|
+
});
|
|
4983
|
+
await keyCache?.set(cacheKey, null);
|
|
4984
|
+
if (error instanceof _fedify_vocab_runtime.FetchError && error.response != null) {
|
|
4985
|
+
const fetchError = {
|
|
4986
|
+
status: error.response.status,
|
|
4987
|
+
response: error.response.clone()
|
|
4988
|
+
};
|
|
4989
|
+
await keyCache?.setFetchError?.(cacheKey, fetchError);
|
|
4990
|
+
return {
|
|
4991
|
+
key: null,
|
|
4992
|
+
cached: false,
|
|
4993
|
+
fetchError
|
|
4994
|
+
};
|
|
4995
|
+
}
|
|
4996
|
+
const fetchError = { error: error instanceof Error ? error : new Error(String(error)) };
|
|
4997
|
+
await keyCache?.setFetchError?.(cacheKey, fetchError);
|
|
4998
|
+
return {
|
|
4999
|
+
key: null,
|
|
5000
|
+
cached: false,
|
|
5001
|
+
fetchError
|
|
5002
|
+
};
|
|
5003
|
+
});
|
|
5004
|
+
});
|
|
5005
|
+
}
|
|
5006
|
+
async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
|
|
5007
|
+
if (keyCache == null) return null;
|
|
5008
|
+
const cachedKey = await keyCache.get(cacheKey);
|
|
5009
|
+
const hit = checkCachedKeyHit(cachedKey, keyId, cls, logger);
|
|
5010
|
+
if (hit != null) return hit;
|
|
5011
|
+
if (cachedKey === null) {
|
|
5012
|
+
logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId });
|
|
5013
|
+
return {
|
|
5014
|
+
key: null,
|
|
5015
|
+
cached: true
|
|
5016
|
+
};
|
|
5017
|
+
}
|
|
5018
|
+
return null;
|
|
5019
|
+
}
|
|
5020
|
+
function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
|
|
5021
|
+
if (cachedKey instanceof cls && cachedKey.publicKey != null) {
|
|
5022
|
+
logger.debug("Key {keyId} found in cache.", { keyId });
|
|
5023
|
+
return {
|
|
5024
|
+
key: cachedKey,
|
|
5025
|
+
cached: true
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
return null;
|
|
5029
|
+
}
|
|
5030
|
+
async function clearFetchErrorMetadata(keyId, keyCache) {
|
|
5031
|
+
await keyCache?.setFetchError?.(keyId, null);
|
|
5032
|
+
}
|
|
5033
|
+
/**
|
|
5034
|
+
* Returns the identifier a fragment-bearing DID URL is a fragment of, or
|
|
5035
|
+
* `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
|
|
5036
|
+
* traced back to `did:web:example.com`, the DID that resolving the key went
|
|
5037
|
+
* through in the first place.
|
|
5038
|
+
*/
|
|
5039
|
+
function stripFragment(keyId) {
|
|
5040
|
+
if (keyId.protocol !== "did:") return null;
|
|
5041
|
+
const did = new URL(keyId.href);
|
|
5042
|
+
did.hash = "";
|
|
5043
|
+
return did;
|
|
5044
|
+
}
|
|
5045
|
+
function isDidKeyUrl(keyId) {
|
|
5046
|
+
return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
|
|
5047
|
+
}
|
|
5048
|
+
async function doRawKeysMatch(left, right) {
|
|
5049
|
+
const [leftRaw, rightRaw] = await Promise.all([crypto.subtle.exportKey("raw", left), crypto.subtle.exportKey("raw", right)]);
|
|
5050
|
+
if (leftRaw.byteLength !== rightRaw.byteLength) return false;
|
|
5051
|
+
const leftBytes = new Uint8Array(leftRaw);
|
|
5052
|
+
const rightBytes = new Uint8Array(rightRaw);
|
|
5053
|
+
return leftBytes.every((byte, index) => byte === rightBytes[index]);
|
|
5054
|
+
}
|
|
5055
|
+
async function isCachedDidKeyValid(key, verificationMethod) {
|
|
5056
|
+
try {
|
|
5057
|
+
return key.id?.href === verificationMethod.id.href && key.controllerId?.href === verificationMethod.controller.href && await doRawKeysMatch(key.publicKey, verificationMethod.publicKey);
|
|
5058
|
+
} catch {
|
|
5059
|
+
return false;
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
async function resolveDidKey(cacheKey, cls, keyCache, logger) {
|
|
5063
|
+
if (!isDidKeyUrl(cacheKey)) return null;
|
|
5064
|
+
const keyId = cacheKey.href;
|
|
5065
|
+
if (!(cls === _fedify_vocab.Multikey)) {
|
|
5066
|
+
logger.debug("Failed to resolve did:key {keyId}; did:key verification methods are only supported as Multikey values.", { keyId });
|
|
5067
|
+
return {
|
|
5068
|
+
key: null,
|
|
5069
|
+
cached: false
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
let verificationMethod;
|
|
5073
|
+
try {
|
|
5074
|
+
verificationMethod = await (0, _fedify_vocab_runtime.parseDidKeyVerificationMethod)(cacheKey);
|
|
5075
|
+
} catch (error) {
|
|
5076
|
+
logger.debug("Failed to resolve did:key verification method {keyId}: {error}", {
|
|
5077
|
+
keyId,
|
|
5078
|
+
error
|
|
5079
|
+
});
|
|
5080
|
+
return {
|
|
5081
|
+
key: null,
|
|
5082
|
+
cached: false
|
|
5083
|
+
};
|
|
5084
|
+
}
|
|
5085
|
+
const hit = checkCachedKeyHit(await keyCache?.get(cacheKey), keyId, cls, logger);
|
|
5086
|
+
if (hit?.key instanceof _fedify_vocab.Multikey && await isCachedDidKeyValid(hit.key, verificationMethod)) return hit;
|
|
5087
|
+
const key = new _fedify_vocab.Multikey({
|
|
5088
|
+
id: verificationMethod.id,
|
|
5089
|
+
controller: verificationMethod.controller,
|
|
5090
|
+
publicKey: verificationMethod.publicKey
|
|
5091
|
+
});
|
|
5092
|
+
await keyCache?.set(cacheKey, key);
|
|
5093
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5094
|
+
logger.debug("Resolved did:key verification method {keyId}.", { keyId });
|
|
5095
|
+
return {
|
|
5096
|
+
key,
|
|
5097
|
+
cached: false
|
|
5098
|
+
};
|
|
5099
|
+
}
|
|
5100
|
+
async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller, portableGatewayKeyResolver }, logger) {
|
|
5101
|
+
let object;
|
|
5102
|
+
try {
|
|
5103
|
+
object = await _fedify_vocab.Object.fromJsonLd(document, {
|
|
5104
|
+
documentLoader,
|
|
5105
|
+
contextLoader,
|
|
5106
|
+
tracerProvider
|
|
5107
|
+
});
|
|
5108
|
+
} catch (e) {
|
|
5109
|
+
if (!(e instanceof TypeError)) throw e;
|
|
5110
|
+
try {
|
|
5111
|
+
object = await cls.fromJsonLd(document, {
|
|
5112
|
+
documentLoader,
|
|
5113
|
+
contextLoader,
|
|
5114
|
+
tracerProvider
|
|
5115
|
+
});
|
|
5116
|
+
} catch (e) {
|
|
5117
|
+
if (e instanceof TypeError) {
|
|
5118
|
+
logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
|
|
5119
|
+
await keyCache?.set(cacheKey, null);
|
|
5120
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5121
|
+
return {
|
|
5122
|
+
key: null,
|
|
5123
|
+
cached: false
|
|
5319
5124
|
};
|
|
5320
5125
|
}
|
|
5321
|
-
|
|
5126
|
+
throw e;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
if (isCompatibleKeyId(cacheKey) && isPortableActorDocument(object)) {
|
|
5130
|
+
if (portableGatewayKeyResolver == null || cls !== _fedify_vocab.CryptographicKey) {
|
|
5131
|
+
logger.debug("Failed to verify; key {keyId} is a gateway key of the portable actor {actorId}, which is only accepted for HTTP Signatures.", {
|
|
5132
|
+
keyId,
|
|
5133
|
+
actorId: object.id?.href
|
|
5134
|
+
});
|
|
5322
5135
|
return {
|
|
5323
5136
|
key: null,
|
|
5324
|
-
cached:
|
|
5137
|
+
cached: false
|
|
5325
5138
|
};
|
|
5326
|
-
}
|
|
5327
|
-
|
|
5139
|
+
}
|
|
5140
|
+
const resolution = await portableGatewayKeyResolver(document, object, cacheKey);
|
|
5141
|
+
if (resolution.type !== "verified") {
|
|
5142
|
+
logger.debug("Failed to verify; the portable actor {actorId} does not vouch for its gateway key {keyId}.", {
|
|
5328
5143
|
keyId,
|
|
5329
|
-
|
|
5144
|
+
actorId: object.id?.href
|
|
5145
|
+
});
|
|
5146
|
+
return {
|
|
5147
|
+
key: null,
|
|
5148
|
+
cached: false
|
|
5149
|
+
};
|
|
5150
|
+
}
|
|
5151
|
+
return {
|
|
5152
|
+
key: resolution.key,
|
|
5153
|
+
cached: false
|
|
5154
|
+
};
|
|
5155
|
+
}
|
|
5156
|
+
let key = null;
|
|
5157
|
+
let ownerDocument = null;
|
|
5158
|
+
if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
|
|
5159
|
+
else if ((0, _fedify_vocab.isActor)(object)) {
|
|
5160
|
+
if (object.id == null || isPortableId(object.id) || object.id.origin !== documentUrl.origin) {
|
|
5161
|
+
logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
|
|
5162
|
+
keyId,
|
|
5163
|
+
documentUrl: documentUrl.href,
|
|
5164
|
+
actorId: object.id?.href
|
|
5330
5165
|
});
|
|
5331
5166
|
await keyCache?.set(cacheKey, null);
|
|
5332
|
-
if (error instanceof _fedify_vocab_runtime.FetchError && error.response != null) {
|
|
5333
|
-
const fetchError = {
|
|
5334
|
-
status: error.response.status,
|
|
5335
|
-
response: error.response.clone()
|
|
5336
|
-
};
|
|
5337
|
-
await keyCache?.setFetchError?.(cacheKey, fetchError);
|
|
5338
|
-
return {
|
|
5339
|
-
key: null,
|
|
5340
|
-
cached: false,
|
|
5341
|
-
fetchError
|
|
5342
|
-
};
|
|
5343
|
-
}
|
|
5344
|
-
const fetchError = { error: error instanceof Error ? error : new Error(String(error)) };
|
|
5345
|
-
await keyCache?.setFetchError?.(cacheKey, fetchError);
|
|
5346
5167
|
return {
|
|
5347
5168
|
key: null,
|
|
5348
|
-
cached: false
|
|
5349
|
-
fetchError
|
|
5169
|
+
cached: false
|
|
5350
5170
|
};
|
|
5171
|
+
}
|
|
5172
|
+
ownerDocument = object;
|
|
5173
|
+
const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
|
|
5174
|
+
documentLoader,
|
|
5175
|
+
contextLoader,
|
|
5176
|
+
suppressError: true,
|
|
5177
|
+
tracerProvider
|
|
5178
|
+
}) : object.getAssertionMethods({
|
|
5179
|
+
documentLoader,
|
|
5180
|
+
contextLoader,
|
|
5181
|
+
suppressError: true,
|
|
5182
|
+
tracerProvider
|
|
5351
5183
|
});
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5184
|
+
let length = 0;
|
|
5185
|
+
let lastKey = null;
|
|
5186
|
+
try {
|
|
5187
|
+
for await (const k of keys) {
|
|
5188
|
+
length++;
|
|
5189
|
+
lastKey = k;
|
|
5190
|
+
if (k.id?.href === keyId) {
|
|
5191
|
+
key = k;
|
|
5192
|
+
break;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
} catch (e) {
|
|
5196
|
+
if (!(e instanceof TypeError)) throw e;
|
|
5197
|
+
logger.debug("Failed to verify; a malformed key was encountered while iterating the keys of {keyId}; treating it as a missing key: {error}", {
|
|
5198
|
+
keyId,
|
|
5199
|
+
error: e
|
|
5200
|
+
});
|
|
5201
|
+
}
|
|
5202
|
+
const keyIdUrl = new URL(keyId);
|
|
5203
|
+
if (key == null && keyIdUrl.hash === "" && length === 1) key = lastKey;
|
|
5204
|
+
if (key == null) {
|
|
5205
|
+
logger.debug("Failed to verify; object {keyId} returned an {actorType}, but has no key matching {keyId}.", {
|
|
5206
|
+
keyId,
|
|
5207
|
+
actorType: object.constructor.name
|
|
5208
|
+
});
|
|
5209
|
+
await keyCache?.set(cacheKey, null);
|
|
5210
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5211
|
+
return {
|
|
5212
|
+
key: null,
|
|
5213
|
+
cached: false
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
} else {
|
|
5217
|
+
logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
|
|
5218
|
+
await keyCache?.set(cacheKey, null);
|
|
5219
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5361
5220
|
return {
|
|
5362
5221
|
key: null,
|
|
5363
|
-
cached:
|
|
5222
|
+
cached: false
|
|
5364
5223
|
};
|
|
5365
5224
|
}
|
|
5366
|
-
|
|
5367
|
-
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
logger.debug("Key {keyId} found in cache.", { keyId });
|
|
5225
|
+
if (key.publicKey == null) {
|
|
5226
|
+
logger.debug("Failed to verify; key {keyId} has no publicKeyPem field.", { keyId });
|
|
5227
|
+
await keyCache?.set(cacheKey, null);
|
|
5228
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5371
5229
|
return {
|
|
5372
|
-
key:
|
|
5373
|
-
cached:
|
|
5230
|
+
key: null,
|
|
5231
|
+
cached: false
|
|
5374
5232
|
};
|
|
5375
5233
|
}
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5234
|
+
const claimedOwnerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key.controllerId;
|
|
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
|
+
}
|
|
5248
|
+
if (ownerDocument != null && claimedOwnerId == null) key = key instanceof _fedify_vocab.CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
|
|
5249
|
+
else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
|
|
5250
|
+
if (await verifyKeyOwnership(key, {
|
|
5251
|
+
documentLoader,
|
|
5252
|
+
contextLoader,
|
|
5253
|
+
tracerProvider
|
|
5254
|
+
}) == null) {
|
|
5255
|
+
logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
|
|
5256
|
+
keyId,
|
|
5257
|
+
claimedOwnerId: claimedOwnerId.href
|
|
5258
|
+
});
|
|
5259
|
+
await keyCache?.set(cacheKey, null);
|
|
5260
|
+
return {
|
|
5261
|
+
key: null,
|
|
5262
|
+
cached: false
|
|
5263
|
+
};
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
if (keyCache != null) {
|
|
5268
|
+
await keyCache.set(cacheKey, key);
|
|
5269
|
+
logger.debug("Key {keyId} cached.", { keyId });
|
|
5270
|
+
}
|
|
5271
|
+
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5272
|
+
return {
|
|
5273
|
+
key,
|
|
5274
|
+
cached: false
|
|
5275
|
+
};
|
|
5380
5276
|
}
|
|
5381
5277
|
/**
|
|
5382
|
-
*
|
|
5383
|
-
*
|
|
5384
|
-
*
|
|
5385
|
-
* through in the first place.
|
|
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.
|
|
5386
5281
|
*/
|
|
5387
|
-
function
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
did.hash = "";
|
|
5391
|
-
return did;
|
|
5392
|
-
}
|
|
5393
|
-
function isDidKeyUrl(keyId) {
|
|
5394
|
-
return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
|
|
5395
|
-
}
|
|
5396
|
-
async function doRawKeysMatch(left, right) {
|
|
5397
|
-
const [leftRaw, rightRaw] = await Promise.all([crypto.subtle.exportKey("raw", left), crypto.subtle.exportKey("raw", right)]);
|
|
5398
|
-
if (leftRaw.byteLength !== rightRaw.byteLength) return false;
|
|
5399
|
-
const leftBytes = new Uint8Array(leftRaw);
|
|
5400
|
-
const rightBytes = new Uint8Array(rightRaw);
|
|
5401
|
-
return leftBytes.every((byte, index) => byte === rightBytes[index]);
|
|
5282
|
+
function claimsPortableOwner(key) {
|
|
5283
|
+
const ownerId = key instanceof _fedify_vocab.CryptographicKey ? key.ownerId : key?.controllerId;
|
|
5284
|
+
return ownerId != null && isPortableId(ownerId);
|
|
5402
5285
|
}
|
|
5403
|
-
async function
|
|
5286
|
+
async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
|
|
5287
|
+
const start = performance.now();
|
|
5288
|
+
let outcome = { result: "error" };
|
|
5404
5289
|
try {
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5290
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
5291
|
+
"fedify",
|
|
5292
|
+
"sig",
|
|
5293
|
+
"key"
|
|
5294
|
+
]);
|
|
5295
|
+
const keyId = cacheKey.href;
|
|
5296
|
+
if (isCompatibleKeyId(cacheKey)) options = {
|
|
5297
|
+
...options,
|
|
5298
|
+
keyCache: void 0
|
|
5299
|
+
};
|
|
5300
|
+
const keyCache = options.keyCache;
|
|
5301
|
+
const didKey = await resolveDidKey(cacheKey, cls, keyCache, logger);
|
|
5302
|
+
if (didKey != null) {
|
|
5303
|
+
outcome = { result: didKey.key == null ? "invalid" : didKey.cached ? "hit" : "fetched" };
|
|
5304
|
+
return didKey;
|
|
5305
|
+
}
|
|
5306
|
+
const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
|
|
5307
|
+
if (cached?.key === null && cached.cached) {
|
|
5308
|
+
const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
|
|
5309
|
+
outcome = { result: "hit" };
|
|
5310
|
+
return cachedUnavailable;
|
|
5311
|
+
}
|
|
5312
|
+
if (cached != null && !claimsPortableOwner(cached.key)) {
|
|
5313
|
+
outcome = { result: "hit" };
|
|
5314
|
+
return cached;
|
|
5315
|
+
}
|
|
5316
|
+
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
5317
|
+
let document;
|
|
5318
|
+
let documentUrl = cacheKey;
|
|
5319
|
+
try {
|
|
5320
|
+
const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
|
|
5321
|
+
document = remoteDocument.document;
|
|
5322
|
+
documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
|
|
5323
|
+
} catch (error) {
|
|
5324
|
+
const classified = classifyFetchError(error);
|
|
5325
|
+
const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
|
|
5326
|
+
outcome = classified;
|
|
5327
|
+
return errored;
|
|
5328
|
+
}
|
|
5329
|
+
const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
|
|
5330
|
+
outcome = { result: resolved.key != null ? "fetched" : "invalid" };
|
|
5331
|
+
return resolved;
|
|
5332
|
+
} finally {
|
|
5333
|
+
recordKeyLookup(options.meterProvider, {
|
|
5334
|
+
durationMs: getDurationMs(start),
|
|
5335
|
+
result: outcome.result,
|
|
5336
|
+
remoteUrl: cacheKey,
|
|
5337
|
+
cacheEnabled: options.keyCache != null,
|
|
5338
|
+
statusCode: outcome.statusCode
|
|
5339
|
+
});
|
|
5408
5340
|
}
|
|
5409
5341
|
}
|
|
5410
|
-
async function
|
|
5411
|
-
|
|
5412
|
-
const keyId = cacheKey.href;
|
|
5413
|
-
if (!(cls === _fedify_vocab.Multikey)) {
|
|
5414
|
-
logger.debug("Failed to resolve did:key {keyId}; did:key verification methods are only supported as Multikey values.", { keyId });
|
|
5342
|
+
async function fetchKeyInternal(keyId, cls, options = {}) {
|
|
5343
|
+
return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
|
|
5415
5344
|
return {
|
|
5416
5345
|
key: null,
|
|
5417
|
-
cached:
|
|
5346
|
+
cached: true
|
|
5418
5347
|
};
|
|
5419
|
-
}
|
|
5420
|
-
|
|
5421
|
-
try {
|
|
5422
|
-
verificationMethod = await (0, _fedify_vocab_runtime.parseDidKeyVerificationMethod)(cacheKey);
|
|
5423
|
-
} catch (error) {
|
|
5424
|
-
logger.debug("Failed to resolve did:key verification method {keyId}: {error}", {
|
|
5348
|
+
}, async (error, cacheKey, keyId, keyCache, logger) => {
|
|
5349
|
+
logger.debug("Failed to fetch key {keyId}.", {
|
|
5425
5350
|
keyId,
|
|
5426
5351
|
error
|
|
5427
5352
|
});
|
|
5353
|
+
await keyCache?.set(cacheKey, null);
|
|
5354
|
+
if (error instanceof _fedify_vocab_runtime.FetchError && error.response != null) await keyCache?.setFetchError?.(cacheKey, {
|
|
5355
|
+
status: error.response.status,
|
|
5356
|
+
response: error.response.clone()
|
|
5357
|
+
});
|
|
5358
|
+
else await keyCache?.setFetchError?.(cacheKey, { error: error instanceof Error ? error : new Error(String(error)) });
|
|
5428
5359
|
return {
|
|
5429
5360
|
key: null,
|
|
5430
5361
|
cached: false
|
|
5431
5362
|
};
|
|
5363
|
+
});
|
|
5364
|
+
}
|
|
5365
|
+
//#endregion
|
|
5366
|
+
//#region src/sig/ld.ts
|
|
5367
|
+
const logger$4 = (0, _logtape_logtape.getLogger)([
|
|
5368
|
+
"fedify",
|
|
5369
|
+
"sig",
|
|
5370
|
+
"ld"
|
|
5371
|
+
]);
|
|
5372
|
+
const localContext = [
|
|
5373
|
+
"https://w3id.org/identity/v1",
|
|
5374
|
+
"https://www.w3.org/ns/activitystreams",
|
|
5375
|
+
"https://w3id.org/security/v1",
|
|
5376
|
+
"https://w3id.org/security/data-integrity/v1"
|
|
5377
|
+
];
|
|
5378
|
+
const localContextUrls = new Set(localContext);
|
|
5379
|
+
const builtInContextLoader = (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
5380
|
+
const disallowedJsonLdKeywords = /* @__PURE__ */ new Set([
|
|
5381
|
+
"@graph",
|
|
5382
|
+
"@included",
|
|
5383
|
+
"@reverse"
|
|
5384
|
+
]);
|
|
5385
|
+
/** @internal */
|
|
5386
|
+
var UnsafeJsonLdError = class extends TypeError {
|
|
5387
|
+
keyword;
|
|
5388
|
+
constructor(keyword) {
|
|
5389
|
+
super(`Unsupported JSON-LD keyword: ${keyword}.`);
|
|
5390
|
+
this.keyword = keyword;
|
|
5391
|
+
this.name = "UnsafeJsonLdError";
|
|
5432
5392
|
}
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5393
|
+
};
|
|
5394
|
+
/** @internal */
|
|
5395
|
+
var InvalidContextReferenceError = class extends TypeError {
|
|
5396
|
+
reference;
|
|
5397
|
+
constructor(reference) {
|
|
5398
|
+
super(`Invalid JSON-LD context reference: ${reference}.`);
|
|
5399
|
+
this.reference = reference;
|
|
5400
|
+
this.name = "InvalidContextReferenceError";
|
|
5401
|
+
}
|
|
5402
|
+
};
|
|
5403
|
+
function createLoadingRemoteContextFailedError(reference, cause) {
|
|
5404
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
5405
|
+
const error = /* @__PURE__ */ new Error(`Dereferencing a URL did not result in a valid JSON-LD context: ${reference}. ${message}`);
|
|
5406
|
+
error.name = "jsonld.InvalidUrl";
|
|
5407
|
+
error.details = {
|
|
5408
|
+
code: "loading remote context failed",
|
|
5409
|
+
url: reference
|
|
5410
|
+
};
|
|
5411
|
+
error.cause = cause;
|
|
5412
|
+
return error;
|
|
5413
|
+
}
|
|
5414
|
+
/** @internal */
|
|
5415
|
+
function isClearlyMalformedContextReference(reference) {
|
|
5416
|
+
for (const char of reference) {
|
|
5417
|
+
const code = char.charCodeAt(0);
|
|
5418
|
+
if (code <= 32 || code === 127) return true;
|
|
5419
|
+
}
|
|
5420
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(reference) && !URL.canParse(reference)) return true;
|
|
5421
|
+
for (let i = 0; i < reference.length; i++) {
|
|
5422
|
+
if (reference[i] !== "%") continue;
|
|
5423
|
+
if (i + 2 >= reference.length || !/[0-9A-Fa-f]/.test(reference[i + 1]) || !/[0-9A-Fa-f]/.test(reference[i + 2])) return true;
|
|
5424
|
+
i += 2;
|
|
5425
|
+
}
|
|
5426
|
+
if (reference.startsWith("./") || reference.startsWith("../") || reference.startsWith("/") || reference.startsWith("//")) {
|
|
5427
|
+
for (const char of reference) if ("[]<>\"\\^`{|}".includes(char)) return true;
|
|
5428
|
+
}
|
|
5429
|
+
return false;
|
|
5430
|
+
}
|
|
5431
|
+
function cloneRemoteDocument(remoteDocument) {
|
|
5432
|
+
return structuredClone(remoteDocument);
|
|
5433
|
+
}
|
|
5434
|
+
function createMemoizedDocumentLoader(documentLoader) {
|
|
5435
|
+
const cache = /* @__PURE__ */ new Map();
|
|
5436
|
+
return async (url, options) => {
|
|
5437
|
+
const cacheKey = URL.canParse(url) ? new URL(url).href : url;
|
|
5438
|
+
let remoteDocument = cache.get(cacheKey);
|
|
5439
|
+
if (remoteDocument == null) {
|
|
5440
|
+
remoteDocument = Promise.resolve(documentLoader(url, options)).then(cloneRemoteDocument);
|
|
5441
|
+
remoteDocument.catch(() => {
|
|
5442
|
+
if (cache.get(cacheKey) === remoteDocument) cache.delete(cacheKey);
|
|
5443
|
+
});
|
|
5444
|
+
cache.set(cacheKey, remoteDocument);
|
|
5445
|
+
}
|
|
5446
|
+
return cloneRemoteDocument(await remoteDocument);
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
/** @internal */
|
|
5450
|
+
function wrapContextLoaderForJsonLd(contextLoader) {
|
|
5451
|
+
const loader = contextLoader ?? builtInContextLoader;
|
|
5452
|
+
return async (url, options) => {
|
|
5453
|
+
try {
|
|
5454
|
+
return await loader(url, options);
|
|
5455
|
+
} catch (error) {
|
|
5456
|
+
if (!isInvalidUrlTypeError(error)) throw error;
|
|
5457
|
+
if (isClearlyMalformedContextReference(url)) throw new InvalidContextReferenceError(url);
|
|
5458
|
+
throw createLoadingRemoteContextFailedError(url, error);
|
|
5459
|
+
}
|
|
5460
|
+
};
|
|
5461
|
+
}
|
|
5462
|
+
/** @internal */
|
|
5463
|
+
function getNormalizationContextLoader(contextLoader) {
|
|
5464
|
+
const loader = wrapContextLoaderForJsonLd(contextLoader);
|
|
5465
|
+
return createMemoizedDocumentLoader(async (url, options) => {
|
|
5466
|
+
if (URL.canParse(url)) {
|
|
5467
|
+
const normalizedUrl = new URL(url).href;
|
|
5468
|
+
if (localContextUrls.has(normalizedUrl)) return await builtInContextLoader(normalizedUrl, options);
|
|
5469
|
+
}
|
|
5470
|
+
return await loader(url, options);
|
|
5439
5471
|
});
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5472
|
+
}
|
|
5473
|
+
/** @internal */
|
|
5474
|
+
async function compactJsonLd(jsonLd, contextLoader) {
|
|
5475
|
+
const hasLds = typeof jsonLd === "object" && jsonLd != null && "signature" in jsonLd;
|
|
5476
|
+
const signature = hasLds ? jsonLd.signature : void 0;
|
|
5477
|
+
const normalizationContextLoader = getNormalizationContextLoader(contextLoader);
|
|
5478
|
+
const document = hasLds ? detachSignature(jsonLd) : jsonLd;
|
|
5479
|
+
await assertNoGraphBeforeCompaction(document, normalizationContextLoader);
|
|
5480
|
+
const compacted = await _fedify_vocab_runtime_jsonld.default.compact(document, localContext, { documentLoader: normalizationContextLoader });
|
|
5481
|
+
if (hasLds && typeof compacted === "object" && compacted != null) compacted.signature = signature;
|
|
5482
|
+
assertSafeJsonLd(compacted);
|
|
5483
|
+
return compacted;
|
|
5484
|
+
}
|
|
5485
|
+
function createInvalidRemoteContextError(reference) {
|
|
5486
|
+
const error = /* @__PURE__ */ new Error(`Dereferencing a URL did not result in a JSON object. The response was valid JSON, but it was not a JSON object. URL: "${reference}".`);
|
|
5487
|
+
error.name = "jsonld.InvalidUrl";
|
|
5488
|
+
error.details = {
|
|
5489
|
+
code: "invalid remote context",
|
|
5490
|
+
url: reference
|
|
5491
|
+
};
|
|
5492
|
+
return error;
|
|
5493
|
+
}
|
|
5494
|
+
function getRemoteContext(remoteDocument, reference) {
|
|
5495
|
+
const { contextUrl, documentUrl } = remoteDocument;
|
|
5496
|
+
let { document } = remoteDocument;
|
|
5497
|
+
if (typeof document === "string") document = JSON.parse(document);
|
|
5498
|
+
if (typeof document !== "object" || document == null || Array.isArray(document)) throw createInvalidRemoteContextError(reference);
|
|
5499
|
+
let context = "@context" in document ? document["@context"] : {};
|
|
5500
|
+
if (contextUrl != null) context = Array.isArray(context) ? [...context, contextUrl] : [context, contextUrl];
|
|
5443
5501
|
return {
|
|
5444
|
-
|
|
5445
|
-
|
|
5502
|
+
context,
|
|
5503
|
+
baseUrl: documentUrl ?? reference
|
|
5446
5504
|
};
|
|
5447
5505
|
}
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5506
|
+
function createGraphAliasContextState() {
|
|
5507
|
+
return {
|
|
5508
|
+
graphTerms: /* @__PURE__ */ new Set(),
|
|
5509
|
+
jsonTerms: /* @__PURE__ */ new Set(),
|
|
5510
|
+
propertyContexts: /* @__PURE__ */ new Map(),
|
|
5511
|
+
termTargets: /* @__PURE__ */ new Map()
|
|
5512
|
+
};
|
|
5513
|
+
}
|
|
5514
|
+
function cloneGraphAliasContextState(state) {
|
|
5515
|
+
return {
|
|
5516
|
+
graphTerms: new Set(state.graphTerms),
|
|
5517
|
+
jsonTerms: new Set(state.jsonTerms),
|
|
5518
|
+
propertyContexts: new Map(state.propertyContexts),
|
|
5519
|
+
termTargets: new Map(state.termTargets)
|
|
5520
|
+
};
|
|
5521
|
+
}
|
|
5522
|
+
function resolveContextTarget(target, state) {
|
|
5523
|
+
if (target === "@graph") return target;
|
|
5524
|
+
const mapped = state.termTargets.get(target);
|
|
5525
|
+
if (mapped == null) return target;
|
|
5526
|
+
return mapped;
|
|
5527
|
+
}
|
|
5528
|
+
function getDirectContextTarget(definition) {
|
|
5529
|
+
if (definition === null) return null;
|
|
5530
|
+
if (typeof definition === "string") return definition;
|
|
5531
|
+
if (typeof definition === "object" && definition != null && "@id" in definition) {
|
|
5532
|
+
const id = definition["@id"];
|
|
5533
|
+
if (id === null) return null;
|
|
5534
|
+
if (typeof id === "string") return id;
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
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);
|
|
5473
5587
|
}
|
|
5474
|
-
|
|
5588
|
+
const loadedRemoteContext = await remoteContext;
|
|
5589
|
+
return await applyGraphAliasContext(nextState, loadedRemoteContext.context, documentLoader, remoteContextCache, loadedRemoteContext.baseUrl, processingContexts);
|
|
5590
|
+
} finally {
|
|
5591
|
+
processingContexts.delete(cacheKey);
|
|
5475
5592
|
}
|
|
5476
5593
|
}
|
|
5477
|
-
if (
|
|
5478
|
-
if (
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
};
|
|
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);
|
|
5487
5603
|
}
|
|
5488
|
-
const
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
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
|
|
5493
5614
|
});
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
};
|
|
5615
|
+
else nextState.propertyContexts.delete(term);
|
|
5616
|
+
if (isJsonTypedDefinition(definition)) nextState.jsonTerms.add(term);
|
|
5617
|
+
else nextState.jsonTerms.delete(term);
|
|
5498
5618
|
}
|
|
5499
|
-
|
|
5500
|
-
key: resolution.key,
|
|
5501
|
-
cached: false
|
|
5502
|
-
};
|
|
5619
|
+
refreshGraphAliases(nextState);
|
|
5503
5620
|
}
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
keyId,
|
|
5511
|
-
documentUrl: documentUrl.href,
|
|
5512
|
-
actorId: object.id?.href
|
|
5513
|
-
});
|
|
5514
|
-
await keyCache?.set(cacheKey, null);
|
|
5515
|
-
return {
|
|
5516
|
-
key: null,
|
|
5517
|
-
cached: false
|
|
5518
|
-
};
|
|
5519
|
-
}
|
|
5520
|
-
ownerDocument = object;
|
|
5521
|
-
const keys = cls === _fedify_vocab.CryptographicKey ? object.getPublicKeys({
|
|
5522
|
-
documentLoader,
|
|
5523
|
-
contextLoader,
|
|
5524
|
-
suppressError: true,
|
|
5525
|
-
tracerProvider
|
|
5526
|
-
}) : object.getAssertionMethods({
|
|
5527
|
-
documentLoader,
|
|
5528
|
-
contextLoader,
|
|
5529
|
-
suppressError: true,
|
|
5530
|
-
tracerProvider
|
|
5531
|
-
});
|
|
5532
|
-
let length = 0;
|
|
5533
|
-
let lastKey = null;
|
|
5534
|
-
try {
|
|
5535
|
-
for await (const k of keys) {
|
|
5536
|
-
length++;
|
|
5537
|
-
lastKey = k;
|
|
5538
|
-
if (k.id?.href === keyId) {
|
|
5539
|
-
key = k;
|
|
5540
|
-
break;
|
|
5541
|
-
}
|
|
5542
|
-
}
|
|
5543
|
-
} catch (e) {
|
|
5544
|
-
if (!(e instanceof TypeError)) throw e;
|
|
5545
|
-
logger.debug("Failed to verify; a malformed key was encountered while iterating the keys of {keyId}; treating it as a missing key: {error}", {
|
|
5546
|
-
keyId,
|
|
5547
|
-
error: e
|
|
5548
|
-
});
|
|
5549
|
-
}
|
|
5550
|
-
const keyIdUrl = new URL(keyId);
|
|
5551
|
-
if (key == null && keyIdUrl.hash === "" && length === 1) key = lastKey;
|
|
5552
|
-
if (key == null) {
|
|
5553
|
-
logger.debug("Failed to verify; object {keyId} returned an {actorType}, but has no key matching {keyId}.", {
|
|
5554
|
-
keyId,
|
|
5555
|
-
actorType: object.constructor.name
|
|
5556
|
-
});
|
|
5557
|
-
await keyCache?.set(cacheKey, null);
|
|
5558
|
-
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5559
|
-
return {
|
|
5560
|
-
key: null,
|
|
5561
|
-
cached: false
|
|
5562
|
-
};
|
|
5563
|
-
}
|
|
5564
|
-
} else {
|
|
5565
|
-
logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
|
|
5566
|
-
await keyCache?.set(cacheKey, null);
|
|
5567
|
-
await clearFetchErrorMetadata(cacheKey, keyCache);
|
|
5568
|
-
return {
|
|
5569
|
-
key: null,
|
|
5570
|
-
cached: false
|
|
5571
|
-
};
|
|
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;
|
|
5572
5627
|
}
|
|
5573
|
-
if (
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
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);
|
|
5581
5639
|
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
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)));
|
|
5600
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();
|
|
5601
5730
|
}
|
|
5602
|
-
}
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
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");
|
|
5611
5753
|
};
|
|
5754
|
+
return Array.isArray(signature) ? signature.some(hasSignatureObject) : hasSignatureObject(signature);
|
|
5612
5755
|
}
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
const
|
|
5623
|
-
if (
|
|
5624
|
-
|
|
5625
|
-
keyCache: void 0
|
|
5626
|
-
};
|
|
5627
|
-
const keyCache = options.keyCache;
|
|
5628
|
-
const didKey = await resolveDidKey(cacheKey, cls, keyCache, logger);
|
|
5629
|
-
if (didKey != null) {
|
|
5630
|
-
outcome = { result: didKey.key == null ? "invalid" : didKey.cached ? "hit" : "fetched" };
|
|
5631
|
-
return didKey;
|
|
5632
|
-
}
|
|
5633
|
-
const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
|
|
5634
|
-
if (cached?.key === null && cached.cached) {
|
|
5635
|
-
const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
|
|
5636
|
-
outcome = { result: "hit" };
|
|
5637
|
-
return cachedUnavailable;
|
|
5638
|
-
}
|
|
5639
|
-
if (cached != null) {
|
|
5640
|
-
outcome = { result: "hit" };
|
|
5641
|
-
return cached;
|
|
5642
|
-
}
|
|
5643
|
-
logger.debug("Fetching key {keyId} to verify signature...", { keyId });
|
|
5644
|
-
let document;
|
|
5645
|
-
let documentUrl = cacheKey;
|
|
5646
|
-
try {
|
|
5647
|
-
const remoteDocument = await (options.documentLoader ?? (0, _fedify_vocab_runtime.getDocumentLoader)())(keyId);
|
|
5648
|
-
document = remoteDocument.document;
|
|
5649
|
-
documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
|
|
5650
|
-
} catch (error) {
|
|
5651
|
-
const classified = classifyFetchError(error);
|
|
5652
|
-
const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
|
|
5653
|
-
outcome = classified;
|
|
5654
|
-
return errored;
|
|
5655
|
-
}
|
|
5656
|
-
const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
|
|
5657
|
-
outcome = { result: resolved.key != null ? "fetched" : "invalid" };
|
|
5658
|
-
return resolved;
|
|
5659
|
-
} finally {
|
|
5660
|
-
recordKeyLookup(options.meterProvider, {
|
|
5661
|
-
durationMs: getDurationMs(start),
|
|
5662
|
-
result: outcome.result,
|
|
5663
|
-
remoteUrl: cacheKey,
|
|
5664
|
-
cacheEnabled: options.keyCache != null,
|
|
5665
|
-
statusCode: outcome.statusCode
|
|
5666
|
-
});
|
|
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";
|
|
5667
5768
|
}
|
|
5769
|
+
return false;
|
|
5668
5770
|
}
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
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;
|
|
5783
|
+
}
|
|
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,
|
|
5678
5812
|
error
|
|
5679
5813
|
});
|
|
5680
|
-
|
|
5681
|
-
if (error instanceof _fedify_vocab_runtime.FetchError && error.response != null) await keyCache?.setFetchError?.(cacheKey, {
|
|
5682
|
-
status: error.response.status,
|
|
5683
|
-
response: error.response.clone()
|
|
5684
|
-
});
|
|
5685
|
-
else await keyCache?.setFetchError?.(cacheKey, { error: error instanceof Error ? error : new Error(String(error)) });
|
|
5686
|
-
return {
|
|
5687
|
-
key: null,
|
|
5688
|
-
cached: false
|
|
5689
|
-
};
|
|
5690
|
-
});
|
|
5691
|
-
}
|
|
5692
|
-
//#endregion
|
|
5693
|
-
//#region src/sig/ld.ts
|
|
5694
|
-
const logger$2 = (0, _logtape_logtape.getLogger)([
|
|
5695
|
-
"fedify",
|
|
5696
|
-
"sig",
|
|
5697
|
-
"ld"
|
|
5698
|
-
]);
|
|
5699
|
-
const localContext = [
|
|
5700
|
-
"https://w3id.org/identity/v1",
|
|
5701
|
-
"https://www.w3.org/ns/activitystreams",
|
|
5702
|
-
"https://w3id.org/security/v1",
|
|
5703
|
-
"https://w3id.org/security/data-integrity/v1"
|
|
5704
|
-
];
|
|
5705
|
-
const localContextUrls = new Set(localContext);
|
|
5706
|
-
const builtInContextLoader = (0, _fedify_vocab_runtime.getDocumentLoader)();
|
|
5707
|
-
const disallowedJsonLdKeywords = /* @__PURE__ */ new Set([
|
|
5708
|
-
"@graph",
|
|
5709
|
-
"@included",
|
|
5710
|
-
"@reverse"
|
|
5711
|
-
]);
|
|
5712
|
-
/** @internal */
|
|
5713
|
-
var UnsafeJsonLdError = class extends TypeError {
|
|
5714
|
-
keyword;
|
|
5715
|
-
constructor(keyword) {
|
|
5716
|
-
super(`Unsupported JSON-LD keyword: ${keyword}.`);
|
|
5717
|
-
this.keyword = keyword;
|
|
5718
|
-
this.name = "UnsafeJsonLdError";
|
|
5719
|
-
}
|
|
5720
|
-
};
|
|
5721
|
-
/** @internal */
|
|
5722
|
-
var InvalidContextReferenceError = class extends TypeError {
|
|
5723
|
-
reference;
|
|
5724
|
-
constructor(reference) {
|
|
5725
|
-
super(`Invalid JSON-LD context reference: ${reference}.`);
|
|
5726
|
-
this.reference = reference;
|
|
5727
|
-
this.name = "InvalidContextReferenceError";
|
|
5814
|
+
return null;
|
|
5728
5815
|
}
|
|
5729
|
-
};
|
|
5730
|
-
|
|
5731
|
-
const
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
error.details = {
|
|
5735
|
-
code: "loading remote context failed",
|
|
5736
|
-
url: reference
|
|
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"
|
|
5737
5821
|
};
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
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;
|
|
5746
5834
|
}
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
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;
|
|
5752
5846
|
}
|
|
5753
|
-
|
|
5754
|
-
|
|
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;
|
|
5755
5865
|
}
|
|
5756
|
-
|
|
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;
|
|
5757
5872
|
}
|
|
5758
|
-
|
|
5759
|
-
|
|
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;
|
|
5760
5893
|
}
|
|
5761
|
-
function
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
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);
|
|
5775
5911
|
}
|
|
5776
5912
|
/** @internal */
|
|
5777
|
-
function
|
|
5778
|
-
|
|
5779
|
-
|
|
5913
|
+
async function verifyCompactJsonLd(jsonLd, options = {}) {
|
|
5914
|
+
return await verifyJsonLdInternal(jsonLd, options, false);
|
|
5915
|
+
}
|
|
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;
|
|
5780
5922
|
try {
|
|
5781
|
-
|
|
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
|
+
}
|
|
5939
|
+
}
|
|
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;
|
|
5782
5955
|
} catch (error) {
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
if (URL.canParse(url)) {
|
|
5794
|
-
const normalizedUrl = new URL(url).href;
|
|
5795
|
-
if (localContextUrls.has(normalizedUrl)) return await builtInContextLoader(normalizedUrl, options);
|
|
5956
|
+
threw = true;
|
|
5957
|
+
span.setStatus({
|
|
5958
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
5959
|
+
message: String(error)
|
|
5960
|
+
});
|
|
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();
|
|
5796
5966
|
}
|
|
5797
|
-
return await loader(url, options);
|
|
5798
5967
|
});
|
|
5799
5968
|
}
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
const
|
|
5806
|
-
await
|
|
5807
|
-
const compacted = await _fedify_vocab_runtime_jsonld.default.compact(document, localContext, { documentLoader: normalizationContextLoader });
|
|
5808
|
-
if (hasLds && typeof compacted === "object" && compacted != null) compacted.signature = signature;
|
|
5809
|
-
assertSafeJsonLd(compacted);
|
|
5810
|
-
return compacted;
|
|
5811
|
-
}
|
|
5812
|
-
function createInvalidRemoteContextError(reference) {
|
|
5813
|
-
const error = /* @__PURE__ */ new Error(`Dereferencing a URL did not result in a JSON object. The response was valid JSON, but it was not a JSON object. URL: "${reference}".`);
|
|
5814
|
-
error.name = "jsonld.InvalidUrl";
|
|
5815
|
-
error.details = {
|
|
5816
|
-
code: "invalid remote context",
|
|
5817
|
-
url: reference
|
|
5818
|
-
};
|
|
5819
|
-
return error;
|
|
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)));
|
|
5820
5976
|
}
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
if (typeof document !== "object" || document == null || Array.isArray(document)) throw createInvalidRemoteContextError(reference);
|
|
5826
|
-
let context = "@context" in document ? document["@context"] : {};
|
|
5827
|
-
if (contextUrl != null) context = Array.isArray(context) ? [...context, contextUrl] : [context, contextUrl];
|
|
5828
|
-
return {
|
|
5829
|
-
context,
|
|
5830
|
-
baseUrl: documentUrl ?? reference
|
|
5831
|
-
};
|
|
5977
|
+
//#endregion
|
|
5978
|
+
//#region src/compat/secured-document.ts
|
|
5979
|
+
function isJsonMap$1(value) {
|
|
5980
|
+
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
5832
5981
|
}
|
|
5833
|
-
function
|
|
5834
|
-
return
|
|
5835
|
-
graphTerms: /* @__PURE__ */ new Set(),
|
|
5836
|
-
jsonTerms: /* @__PURE__ */ new Set(),
|
|
5837
|
-
propertyContexts: /* @__PURE__ */ new Map(),
|
|
5838
|
-
termTargets: /* @__PURE__ */ new Map()
|
|
5839
|
-
};
|
|
5982
|
+
function isNonEmptyString(value) {
|
|
5983
|
+
return typeof value === "string" && value.length > 0;
|
|
5840
5984
|
}
|
|
5841
|
-
function
|
|
5842
|
-
return
|
|
5843
|
-
graphTerms: new Set(state.graphTerms),
|
|
5844
|
-
jsonTerms: new Set(state.jsonTerms),
|
|
5845
|
-
propertyContexts: new Map(state.propertyContexts),
|
|
5846
|
-
termTargets: new Map(state.termTargets)
|
|
5847
|
-
};
|
|
5985
|
+
function hasDataIntegrityProofType(value) {
|
|
5986
|
+
return (Array.isArray(value) ? value : [value]).some((type) => type === "DataIntegrityProof" || type === "https://w3id.org/security#DataIntegrityProof");
|
|
5848
5987
|
}
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
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);
|
|
5854
6007
|
}
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
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;
|
|
6034
|
+
if (key === "@context") continue;
|
|
6035
|
+
if (hasPublicCurieInAddressing(record[key], preserveSecured, key, depth + 1)) return true;
|
|
5862
6036
|
}
|
|
6037
|
+
return false;
|
|
5863
6038
|
}
|
|
5864
|
-
function
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
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;
|
|
5874
6050
|
}
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
}
|
|
5885
|
-
|
|
5886
|
-
function isInvalidUrlTypeError(error) {
|
|
5887
|
-
const code = error.code;
|
|
5888
|
-
return error instanceof TypeError && (code === "ERR_INVALID_URL" || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
|
|
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;
|
|
5889
6062
|
}
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
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;
|
|
5896
6078
|
}
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
6079
|
+
return false;
|
|
6080
|
+
}
|
|
6081
|
+
/**
|
|
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.
|
|
6092
|
+
*/
|
|
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;
|
|
5920
6105
|
}
|
|
5921
|
-
if (
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
if (term.startsWith("@")) continue;
|
|
5926
|
-
const target = getDirectContextTarget(definition);
|
|
5927
|
-
if (target == null) localTargets.set(term, null);
|
|
5928
|
-
else if (typeof target === "string") localTargets.set(term, target);
|
|
5929
|
-
else localTargets.delete(term);
|
|
5930
|
-
}
|
|
5931
|
-
for (const [term, definition] of globalThis.Object.entries(context)) {
|
|
5932
|
-
if (term.startsWith("@")) continue;
|
|
5933
|
-
if (localTargets.has(term)) {
|
|
5934
|
-
const directTarget = localTargets.get(term);
|
|
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
|
|
5941
|
-
});
|
|
5942
|
-
else nextState.propertyContexts.delete(term);
|
|
5943
|
-
if (isJsonTypedDefinition(definition)) nextState.jsonTerms.add(term);
|
|
5944
|
-
else nextState.jsonTerms.delete(term);
|
|
5945
|
-
}
|
|
5946
|
-
refreshGraphAliases(nextState);
|
|
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;
|
|
5947
6110
|
}
|
|
5948
|
-
return
|
|
6111
|
+
return true;
|
|
5949
6112
|
}
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
6113
|
+
/**
|
|
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.
|
|
6155
|
+
*/
|
|
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 });
|
|
5954
6174
|
}
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
6175
|
+
return jsonLd;
|
|
6176
|
+
}
|
|
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);
|
|
6211
|
+
}
|
|
5966
6212
|
}
|
|
5967
6213
|
}
|
|
5968
|
-
function
|
|
5969
|
-
return
|
|
6214
|
+
function isActivityStreamsAttachmentTerm(value) {
|
|
6215
|
+
return typeof value === "object" && value != null && value["@id"] === "as:attachment" && value["@type"] === "@id";
|
|
5970
6216
|
}
|
|
5971
6217
|
/** @internal */
|
|
5972
|
-
function
|
|
5973
|
-
if (
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
assertSafeJsonLd(value);
|
|
5980
|
-
}
|
|
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;
|
|
5981
6225
|
}
|
|
6226
|
+
return true;
|
|
5982
6227
|
}
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
* @returns The JSON-LD document with the attached signature.
|
|
5989
|
-
* @throws {TypeError} If the input document is not a valid JSON-LD document.
|
|
5990
|
-
* @since 1.0.0
|
|
5991
|
-
*/
|
|
5992
|
-
function attachSignature(jsonLd, signature) {
|
|
5993
|
-
if (typeof jsonLd !== "object" || jsonLd == null) throw new TypeError("Failed to attach signature; invalid JSON-LD document.");
|
|
5994
|
-
return {
|
|
5995
|
-
...jsonLd,
|
|
5996
|
-
signature
|
|
5997
|
-
};
|
|
5998
|
-
}
|
|
5999
|
-
/**
|
|
6000
|
-
* Creates a LD signature for the given JSON-LD document.
|
|
6001
|
-
* @param jsonLd The JSON-LD document to sign.
|
|
6002
|
-
* @param privateKey The private key to sign the document.
|
|
6003
|
-
* @param keyId The ID of the public key that corresponds to the private key.
|
|
6004
|
-
* @param options Additional options for creating the signature.
|
|
6005
|
-
* See also {@link CreateSignatureOptions}.
|
|
6006
|
-
* @return The created signature.
|
|
6007
|
-
* @throws {TypeError} If the private key is invalid or unsupported.
|
|
6008
|
-
* @since 1.0.0
|
|
6009
|
-
*/
|
|
6010
|
-
async function createSignature(jsonLd, privateKey, keyId, { contextLoader, created } = {}) {
|
|
6011
|
-
validateCryptoKey(privateKey, "private");
|
|
6012
|
-
if (privateKey.algorithm.name !== "RSASSA-PKCS1-v1_5") throw new TypeError("Unsupported algorithm: " + privateKey.algorithm.name);
|
|
6013
|
-
const options = {
|
|
6014
|
-
"@context": "https://w3id.org/identity/v1",
|
|
6015
|
-
creator: keyId.href,
|
|
6016
|
-
created: created?.toString() ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
6017
|
-
};
|
|
6018
|
-
const message = await hashJsonLd(options, contextLoader) + await hashJsonLd(jsonLd, contextLoader);
|
|
6019
|
-
const messageBytes = new TextEncoder().encode(message);
|
|
6020
|
-
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", privateKey, messageBytes);
|
|
6021
|
-
return {
|
|
6022
|
-
...options,
|
|
6023
|
-
type: "RsaSignature2017",
|
|
6024
|
-
signatureValue: (0, byte_encodings_base64.encodeBase64)(signature)
|
|
6025
|
-
};
|
|
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;
|
|
6026
6233
|
}
|
|
6027
6234
|
/**
|
|
6028
|
-
*
|
|
6029
|
-
* JSON-LD document.
|
|
6030
|
-
* @param jsonLd The JSON-LD document to sign.
|
|
6031
|
-
* @param privateKey The private key to sign the document.
|
|
6032
|
-
* @param keyId The key ID to use in the signature. It will be used by the
|
|
6033
|
-
* verifier to fetch the corresponding public key.
|
|
6034
|
-
* @param options Additional options for signing the document.
|
|
6035
|
-
* See also {@link SignJsonLdOptions}.
|
|
6036
|
-
* @returns The signed JSON-LD document.
|
|
6037
|
-
* @throws {TypeError} If the private key is invalid or unsupported.
|
|
6038
|
-
* @since 1.0.0
|
|
6235
|
+
* Wraps scalar ActivityStreams attachment properties in arrays.
|
|
6039
6236
|
*/
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
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;
|
|
6249
|
+
}
|
|
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];
|
|
6047
6264
|
}
|
|
6048
|
-
|
|
6049
|
-
} catch (error) {
|
|
6050
|
-
span.setStatus({
|
|
6051
|
-
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
6052
|
-
message: String(error)
|
|
6053
|
-
});
|
|
6054
|
-
throw error;
|
|
6055
|
-
} finally {
|
|
6056
|
-
span.end();
|
|
6265
|
+
normalized = cloned;
|
|
6057
6266
|
}
|
|
6058
|
-
|
|
6059
|
-
}
|
|
6060
|
-
|
|
6061
|
-
* Checks if the given JSON-LD document has a Linked Data Signature-like
|
|
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);
|
|
6267
|
+
if (normalized != null) normalized[key] = output;
|
|
6268
|
+
}
|
|
6269
|
+
return normalized ?? jsonLd;
|
|
6082
6270
|
}
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
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";
|
|
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;
|
|
6095
6280
|
}
|
|
6096
6281
|
return false;
|
|
6097
6282
|
}
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd;
|
|
6107
|
-
const doc = { ...jsonLd };
|
|
6108
|
-
delete doc.signature;
|
|
6109
|
-
return doc;
|
|
6110
|
-
}
|
|
6111
|
-
/**
|
|
6112
|
-
* Verifies Linked Data Signatures of the given JSON-LD document.
|
|
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
|
|
6129
|
-
*/
|
|
6130
|
-
async function verifySignature(jsonLd, options = {}) {
|
|
6131
|
-
if (!hasSignature(jsonLd)) return null;
|
|
6132
|
-
const sig = jsonLd.signature;
|
|
6133
|
-
let signature;
|
|
6134
|
-
try {
|
|
6135
|
-
signature = (0, byte_encodings_base64.decodeBase64)(sig.signatureValue);
|
|
6136
|
-
} catch (error) {
|
|
6137
|
-
logger$2.debug("Failed to verify; invalid base64 signatureValue: {signatureValue}", {
|
|
6138
|
-
...sig,
|
|
6139
|
-
error
|
|
6140
|
-
});
|
|
6141
|
-
return null;
|
|
6142
|
-
}
|
|
6143
|
-
const { key, cached } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, options));
|
|
6144
|
-
if (key == null) return null;
|
|
6145
|
-
const sigOpts = {
|
|
6146
|
-
...sig,
|
|
6147
|
-
"@context": "https://w3id.org/identity/v1"
|
|
6148
|
-
};
|
|
6149
|
-
delete sigOpts.type;
|
|
6150
|
-
delete sigOpts.id;
|
|
6151
|
-
delete sigOpts.signatureValue;
|
|
6152
|
-
let sigOptsHash;
|
|
6153
|
-
try {
|
|
6154
|
-
sigOptsHash = await hashJsonLd(sigOpts, options.contextLoader);
|
|
6155
|
-
} catch (error) {
|
|
6156
|
-
logger$2.warn("Failed to verify; failed to hash the signature options: {signatureOptions}\n{error}", {
|
|
6157
|
-
signatureOptions: sigOpts,
|
|
6158
|
-
error
|
|
6159
|
-
});
|
|
6160
|
-
return null;
|
|
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;
|
|
6161
6291
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
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;
|
|
6173
6306
|
}
|
|
6174
|
-
|
|
6175
|
-
const
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
if (cached) {
|
|
6179
|
-
logger$2.debug("Failed to verify with the cached key {keyId}; signature {signatureValue} is invalid. Retrying with the freshly fetched key...", {
|
|
6180
|
-
keyId: sig.creator,
|
|
6181
|
-
...sig
|
|
6182
|
-
});
|
|
6183
|
-
const { key } = await measureSignatureKeyFetch(options.meterProvider, "linked_data", () => fetchKey(new URL(sig.creator), _fedify_vocab.CryptographicKey, {
|
|
6184
|
-
...options,
|
|
6185
|
-
keyCache: {
|
|
6186
|
-
get: () => Promise.resolve(void 0),
|
|
6187
|
-
set: async (keyId, key) => await options.keyCache?.set(keyId, key)
|
|
6188
|
-
}
|
|
6189
|
-
}));
|
|
6190
|
-
if (key == null) return null;
|
|
6191
|
-
return await crypto.subtle.verify("RSASSA-PKCS1-v1_5", key.publicKey, signature.slice(), messageBytes) ? key : null;
|
|
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;
|
|
6192
6311
|
}
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
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
|
|
@@ -8658,6 +8786,12 @@ Object.defineProperty(exports, "InvalidContextReferenceError", {
|
|
|
8658
8786
|
return InvalidContextReferenceError;
|
|
8659
8787
|
}
|
|
8660
8788
|
});
|
|
8789
|
+
Object.defineProperty(exports, "InvalidPortableObjectIdError", {
|
|
8790
|
+
enumerable: true,
|
|
8791
|
+
get: function() {
|
|
8792
|
+
return InvalidPortableObjectIdError;
|
|
8793
|
+
}
|
|
8794
|
+
});
|
|
8661
8795
|
Object.defineProperty(exports, "Temporal", {
|
|
8662
8796
|
enumerable: true,
|
|
8663
8797
|
get: function() {
|
|
@@ -8784,6 +8918,12 @@ Object.defineProperty(exports, "getNormalizationContextLoader", {
|
|
|
8784
8918
|
return getNormalizationContextLoader;
|
|
8785
8919
|
}
|
|
8786
8920
|
});
|
|
8921
|
+
Object.defineProperty(exports, "getPortableDid", {
|
|
8922
|
+
enumerable: true,
|
|
8923
|
+
get: function() {
|
|
8924
|
+
return getPortableDid;
|
|
8925
|
+
}
|
|
8926
|
+
});
|
|
8787
8927
|
Object.defineProperty(exports, "getRemoteHost", {
|
|
8788
8928
|
enumerable: true,
|
|
8789
8929
|
get: function() {
|
|
@@ -8850,6 +8990,12 @@ Object.defineProperty(exports, "isInvalidUrlTypeError", {
|
|
|
8850
8990
|
return isInvalidUrlTypeError;
|
|
8851
8991
|
}
|
|
8852
8992
|
});
|
|
8993
|
+
Object.defineProperty(exports, "isPortableId", {
|
|
8994
|
+
enumerable: true,
|
|
8995
|
+
get: function() {
|
|
8996
|
+
return isPortableId;
|
|
8997
|
+
}
|
|
8998
|
+
});
|
|
8853
8999
|
Object.defineProperty(exports, "isPortableUri", {
|
|
8854
9000
|
enumerable: true,
|
|
8855
9001
|
get: function() {
|
|
@@ -8886,6 +9032,12 @@ Object.defineProperty(exports, "parseRfc9421SignatureInput", {
|
|
|
8886
9032
|
return parseRfc9421SignatureInput;
|
|
8887
9033
|
}
|
|
8888
9034
|
});
|
|
9035
|
+
Object.defineProperty(exports, "preloadedOnlyDocumentLoader", {
|
|
9036
|
+
enumerable: true,
|
|
9037
|
+
get: function() {
|
|
9038
|
+
return preloadedOnlyDocumentLoader;
|
|
9039
|
+
}
|
|
9040
|
+
});
|
|
8889
9041
|
Object.defineProperty(exports, "recordCircuitBreakerStateChange", {
|
|
8890
9042
|
enumerable: true,
|
|
8891
9043
|
get: function() {
|