@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2448
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/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
- package/dist/{builder-Da8b9O6v.mjs → builder-DbC8bn6k.mjs} +5 -5
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-D1djO5fa.mjs → compound-proof-heeXW2_A.mjs} +2 -2
- package/dist/{context-CQ9H8vNW.d.cts → context-BazwQrHn.d.cts} +304 -4
- package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
- package/dist/{context-BdB54V-A.d.ts → context-GlHrzdSP.d.ts} +304 -4
- package/dist/{deno-eNpfj4oR.mjs → deno-Dpewt_jJ.mjs} +1 -1
- package/dist/{docloader-B-0nqOB1.mjs → docloader-CpsDyqVN.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 +5 -5
- package/dist/federation/handler.test.mjs +21 -6
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-actorless.test.d.mts +2 -0
- package/dist/federation/inbox-actorless.test.mjs +487 -0
- package/dist/federation/inbox-authentication.test.d.mts +2 -0
- package/dist/federation/inbox-authentication.test.mjs +116 -0
- package/dist/federation/inbox-report.test.mjs +5 -5
- package/dist/federation/inbox-view.test.d.mts +2 -0
- package/dist/federation/inbox-view.test.mjs +369 -0
- package/dist/federation/inbox.test.mjs +22 -12
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +9 -9
- package/dist/federation/mod.cjs +2 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +8 -2
- package/dist/federation/mq.test.mjs +36 -1
- package/dist/federation/outgoing-proof.test.mjs +4 -4
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-delivery.test.mjs +2 -2
- package/dist/federation/portable-gateway-keys.test.mjs +6 -6
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable-route-activity.test.mjs +2 -2
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +3 -3
- package/dist/federation/tasks/tasks.test.mjs +4 -4
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-BVH7drJA.cjs → federation-D8YexkP1.cjs} +1172 -131
- package/dist/{http-DlNxxW_5.js → http-BBr-VLil.js} +143 -16
- package/dist/{http-BUJ5SFh6.mjs → http-BZkyQIkb.mjs} +4 -4
- package/dist/{http-BfW3wyQ4.cjs → http-CAl7Num5.cjs} +161 -16
- package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
- package/dist/inbox-view-NE9wXdLo.mjs +385 -0
- package/dist/inbox-wDuMKaFW.mjs +312 -0
- package/dist/{kv-cache-D_m3A241.mjs → kv-cache-CTQSfkmp.mjs} +1 -1
- package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-CjgCOz3i.cjs} +1 -1
- package/dist/{kv-cache-7015lbrx.js → kv-cache-U7uz5nx4.js} +1 -1
- package/dist/{ld-CD2CsPxq.mjs → ld-CQFuiah9.mjs} +52 -9
- package/dist/ld-CcuZ4NZa.d.ts +250 -0
- package/dist/ld-Cel1RRZ5.d.cts +250 -0
- package/dist/{metrics-D3LHk6Z4.mjs → metrics-B-QxRSSR.mjs} +1 -1
- package/dist/{middleware-CCDXvXOG.mjs → middleware-BdlNa8BD.mjs} +1 -1
- package/dist/{middleware-CzoWvrdZ.mjs → middleware-CuVa0UuS.mjs} +459 -98
- package/dist/{middleware-N9eM16XE.js → middleware-P-AZEQMn.js} +1169 -141
- package/dist/mod-B2783_3v.d.cts +154 -0
- package/dist/mod-BHQE-7WQ.d.ts +154 -0
- package/dist/{mod-D0bxB8jS.d.ts → mod-DjZfJj9R.d.ts} +1 -1
- package/dist/{mod-B0dGgQsg.d.cts → mod-DvWOv2Ho.d.cts} +1 -1
- package/dist/mod.cjs +5 -4
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/{owner-C5-FVX6t.mjs → owner-ccKkQOAf.mjs} +3 -3
- package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CndnpXfo.mjs} +2 -2
- package/dist/{portable-collection-CC2zNAje.js → portable-collection-DUlQ3s2M.js} +1 -1
- package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-TC-kvkVI.cjs} +1 -1
- package/dist/{portable-key-DNc_flH-.mjs → portable-key-D5tFOoV-.mjs} +1 -1
- package/dist/{proof-Cryb1c70.mjs → proof-DPoF06nz.mjs} +98 -10
- package/dist/{send-AF25pWaJ.mjs → send-CF44O1Df.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 +2 -2
- 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 +1 -1
- package/dist/sig/mitra-receive-interop.test.mjs +4 -4
- package/dist/sig/mitra-send-interop.test.mjs +3 -3
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-interop.test.mjs +3 -3
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +44 -4
- package/dist/sig/tootik-request-interop.test.mjs +4 -4
- package/dist/sig/tootik-send-interop.test.mjs +3 -3
- package/dist/sig/verification.test.mjs +3 -3
- package/dist/{temporal-BsAeuUJj.mjs → temporal-XlZ4Wb94.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +262 -2
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.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
- package/dist/inbox-CHvMaIFX.mjs +0 -188
- package/dist/mod-BRpER-B0.d.ts +0 -320
- package/dist/mod-xAfaLS4T.d.cts +0 -320
- package/dist/owner-BF0L9cMt.d.cts +0 -78
- package/dist/owner-BSzr5-I8.d.ts +0 -78
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { i as getPortableDid, l as isPortableId, n as getCanonicalPortableId } from "./portable-key-id-Ca30-hOa.mjs";
|
|
5
|
+
//#region src/federation/inbox-authentication.ts
|
|
6
|
+
/**
|
|
7
|
+
* The evidence of an activity that has not been authenticated by Fedify.
|
|
8
|
+
*/
|
|
9
|
+
const EMPTY_EVIDENCE = Object.freeze({
|
|
10
|
+
verified: false,
|
|
11
|
+
requestSigners: Object.freeze([]),
|
|
12
|
+
documentSigners: Object.freeze([]),
|
|
13
|
+
originAuthenticated: Object.freeze([]),
|
|
14
|
+
expandedActivity: Object.freeze([]),
|
|
15
|
+
baseUrl: null
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Gets the evidence of an activity that has not been authenticated.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
function getEmptyAuthenticationEvidence() {
|
|
22
|
+
return EMPTY_EVIDENCE;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Gets the principals that an authorization decision should rely on.
|
|
26
|
+
* @param evidence The evidence.
|
|
27
|
+
* @param scope See {@link IsActorAuthenticatedOptions.scope}.
|
|
28
|
+
* @returns The principals.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
function getEffectivePrincipals(evidence, scope = "effective") {
|
|
32
|
+
if (scope === "document" || evidence.documentSigners.length > 0) return evidence.documentSigners;
|
|
33
|
+
if (evidence.requestSigners.length > 0) return evidence.requestSigners;
|
|
34
|
+
return evidence.originAuthenticated;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes an actor ID for comparison: portable IDs and compatible
|
|
38
|
+
* identifiers to their canonical portable ID, other IDs to their `href`.
|
|
39
|
+
* @returns The normalized ID, or `null` for a malformed portable ID.
|
|
40
|
+
*/
|
|
41
|
+
function normalizeActorId(id) {
|
|
42
|
+
return isPortableId(id) ? getCanonicalPortableId(id) : id.href;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Checks whether the given principal authenticates the given actor.
|
|
46
|
+
* @param principal The principal.
|
|
47
|
+
* @param actorId The actor ID.
|
|
48
|
+
* @returns `true` if the principal authenticates the actor.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
function principalAuthenticates(principal, actorId) {
|
|
52
|
+
if (principal.kind === "portableAuthority") {
|
|
53
|
+
if (!isPortableId(actorId)) return false;
|
|
54
|
+
const did = getPortableDid(actorId);
|
|
55
|
+
return did != null && did === getPortableDid(principal.id);
|
|
56
|
+
}
|
|
57
|
+
const expected = normalizeActorId(actorId);
|
|
58
|
+
return expected != null && normalizeActorId(principal.id) === expected;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Checks whether the given evidence authenticates the given actor.
|
|
62
|
+
*
|
|
63
|
+
* Actor IDs are compared exactly, except that [FEP-ef61] portable IDs and
|
|
64
|
+
* compatible identifiers are compared by their canonical portable IDs, and
|
|
65
|
+
* a portable authority (a DID) authenticates the portable actors of that DID.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ``` typescript
|
|
69
|
+
* federation
|
|
70
|
+
* .setInboxListeners("/users/{identifier}/inbox", "/inbox")
|
|
71
|
+
* .on(Follow, async (ctx, follow) => {
|
|
72
|
+
* if (follow.actorId == null) return;
|
|
73
|
+
* // The actor is always authenticated for activities that declare one,
|
|
74
|
+
* // but the same check works for any actor an activity names:
|
|
75
|
+
* isActorAuthenticated(ctx.authentication, follow.actorId);
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
80
|
+
* @param evidence The authentication evidence, usually
|
|
81
|
+
* {@link InboxContext.authentication}.
|
|
82
|
+
* @param actorId The actor ID to check.
|
|
83
|
+
* @param options Options.
|
|
84
|
+
* @returns `true` if the actor is authenticated.
|
|
85
|
+
* @since 2.5.0
|
|
86
|
+
*/
|
|
87
|
+
function isActorAuthenticated(evidence, actorId, options = {}) {
|
|
88
|
+
return getEffectivePrincipals(evidence, options.scope).some((principal) => principalAuthenticates(principal, actorId));
|
|
89
|
+
}
|
|
90
|
+
function serializePrincipal(principal) {
|
|
91
|
+
return {
|
|
92
|
+
kind: principal.kind,
|
|
93
|
+
id: principal.id.href,
|
|
94
|
+
mechanism: principal.mechanism,
|
|
95
|
+
keyId: principal.keyId?.href ?? null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Serializes the principals of the given evidence for a queued message.
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
function serializeAuthentication(evidence) {
|
|
103
|
+
return {
|
|
104
|
+
verified: evidence.verified,
|
|
105
|
+
requestSigners: evidence.requestSigners.map(serializePrincipal),
|
|
106
|
+
documentSigners: evidence.documentSigners.map(serializePrincipal),
|
|
107
|
+
originAuthenticated: evidence.originAuthenticated.map(serializePrincipal)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const KINDS = /* @__PURE__ */ new Set(["actor", "portableAuthority"]);
|
|
111
|
+
const MECHANISMS = /* @__PURE__ */ new Set([
|
|
112
|
+
"httpSignatures",
|
|
113
|
+
"linkedDataSignatures",
|
|
114
|
+
"objectIntegrityProofs",
|
|
115
|
+
"origin"
|
|
116
|
+
]);
|
|
117
|
+
function deserializePrincipals(value) {
|
|
118
|
+
if (!Array.isArray(value)) return null;
|
|
119
|
+
const principals = [];
|
|
120
|
+
for (const item of value) {
|
|
121
|
+
if (item == null || typeof item !== "object" || !KINDS.has(item.kind) || !MECHANISMS.has(item.mechanism) || typeof item.id !== "string" || !URL.canParse(item.id) || item.keyId != null && (typeof item.keyId !== "string" || !URL.canParse(item.keyId))) return null;
|
|
122
|
+
principals.push(Object.freeze({
|
|
123
|
+
kind: item.kind,
|
|
124
|
+
id: new URL(item.id),
|
|
125
|
+
mechanism: item.mechanism,
|
|
126
|
+
keyId: item.keyId == null ? null : new URL(item.keyId)
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
return principals;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Restores the evidence of a queued message.
|
|
133
|
+
* @param value The serialized principals.
|
|
134
|
+
* @param expandedActivity The expanded activity, restored from the queued
|
|
135
|
+
* verified view.
|
|
136
|
+
* @param baseUrl The base URL, restored from the queued verified view.
|
|
137
|
+
* @returns The evidence, or `null` if the serialized value is malformed.
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
function deserializeAuthentication(value, expandedActivity, baseUrl) {
|
|
141
|
+
if (value == null || typeof value !== "object" || typeof value.verified !== "boolean") return null;
|
|
142
|
+
const record = value;
|
|
143
|
+
const requestSigners = deserializePrincipals(record.requestSigners);
|
|
144
|
+
const documentSigners = deserializePrincipals(record.documentSigners);
|
|
145
|
+
const originAuthenticated = deserializePrincipals(record.originAuthenticated);
|
|
146
|
+
if (requestSigners == null || documentSigners == null || originAuthenticated == null) return null;
|
|
147
|
+
return createAuthenticationEvidence({
|
|
148
|
+
verified: record.verified,
|
|
149
|
+
requestSigners,
|
|
150
|
+
documentSigners,
|
|
151
|
+
originAuthenticated,
|
|
152
|
+
expandedActivity,
|
|
153
|
+
baseUrl
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Creates frozen evidence.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
function createAuthenticationEvidence(evidence) {
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
verified: evidence.verified,
|
|
163
|
+
requestSigners: Object.freeze([...evidence.requestSigners]),
|
|
164
|
+
documentSigners: Object.freeze([...evidence.documentSigners]),
|
|
165
|
+
originAuthenticated: Object.freeze([...evidence.originAuthenticated]),
|
|
166
|
+
expandedActivity: evidence.expandedActivity,
|
|
167
|
+
baseUrl: evidence.baseUrl
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { isActorAuthenticated as a, getEmptyAuthenticationEvidence as i, deserializeAuthentication as n, serializeAuthentication as o, getEffectivePrincipals as r, createAuthenticationEvidence as t };
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { c as getNormalizationContextLoader, p as markEffectiveContextLoader, s as detachSignature, y as wrapContextLoaderForJsonLd } from "./ld-CQFuiah9.mjs";
|
|
5
|
+
import { preloadedContexts } from "@fedify/vocab-runtime";
|
|
6
|
+
import { encodeHex } from "byte-encodings/hex";
|
|
7
|
+
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
8
|
+
import serialize from "json-canon";
|
|
9
|
+
//#region src/federation/inbox-view.ts
|
|
10
|
+
/**
|
|
11
|
+
* The interpretation of an incoming activity that Fedify authenticated, kept
|
|
12
|
+
* so that listeners, authorization helpers, and queue workers observe the
|
|
13
|
+
* same JSON-LD meaning that signature verification observed.
|
|
14
|
+
*
|
|
15
|
+
* Every JSON-LD processing step of one delivery goes through a request-scoped
|
|
16
|
+
* *context snapshot*, which memoizes the remote context responses so that
|
|
17
|
+
* a nondeterministic or mutable remote context cannot give verification and
|
|
18
|
+
* dispatch different meanings. The snapshot is serialized into queued
|
|
19
|
+
* messages, so that workers and retries replay the very same responses
|
|
20
|
+
* without loading contexts again.
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when replaying an interpretation needs a context response that its
|
|
25
|
+
* snapshot does not have, or has with a different content.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
var ContextSnapshotMissError = class extends Error {
|
|
29
|
+
url;
|
|
30
|
+
constructor(url, reason) {
|
|
31
|
+
super(`Context ${url} cannot be replayed: ${reason}.`);
|
|
32
|
+
this.name = "ContextSnapshotMissError";
|
|
33
|
+
this.url = url;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function normalizeUrl(url) {
|
|
37
|
+
return URL.canParse(url) ? new URL(url).href : url;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates the context snapshot of a delivery.
|
|
41
|
+
* @param base The caller's context loader.
|
|
42
|
+
* @returns The snapshot.
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
function createContextSnapshot(base) {
|
|
46
|
+
const underlying = {
|
|
47
|
+
standard: wrapContextLoaderForJsonLd(base),
|
|
48
|
+
normalization: getNormalizationContextLoader(base)
|
|
49
|
+
};
|
|
50
|
+
const cache = /* @__PURE__ */ new Map();
|
|
51
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
52
|
+
const loaders = /* @__PURE__ */ new Map();
|
|
53
|
+
let released = false;
|
|
54
|
+
return {
|
|
55
|
+
loader(policy) {
|
|
56
|
+
let loader = loaders.get(policy);
|
|
57
|
+
if (loader != null) return loader;
|
|
58
|
+
const effective = underlying[policy];
|
|
59
|
+
loader = markEffectiveContextLoader(async (url, options) => {
|
|
60
|
+
if (released) return await effective(url, options);
|
|
61
|
+
const normalizedUrl = normalizeUrl(url);
|
|
62
|
+
const key = `${policy}\n${normalizedUrl}`;
|
|
63
|
+
let promise = cache.get(key);
|
|
64
|
+
if (promise == null) {
|
|
65
|
+
promise = Promise.resolve(effective(url, options)).then((response) => {
|
|
66
|
+
const cloned = normalizeResponse(structuredClone(response));
|
|
67
|
+
recorded.set(key, {
|
|
68
|
+
policy,
|
|
69
|
+
url: normalizedUrl,
|
|
70
|
+
response: structuredClone(cloned)
|
|
71
|
+
});
|
|
72
|
+
return cloned;
|
|
73
|
+
});
|
|
74
|
+
promise.catch(() => {});
|
|
75
|
+
cache.set(key, promise);
|
|
76
|
+
}
|
|
77
|
+
return structuredClone(await promise);
|
|
78
|
+
}, policy);
|
|
79
|
+
loaders.set(policy, loader);
|
|
80
|
+
return loader;
|
|
81
|
+
},
|
|
82
|
+
entries() {
|
|
83
|
+
return [...recorded.values()];
|
|
84
|
+
},
|
|
85
|
+
release() {
|
|
86
|
+
released = true;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function getBundledResponse(url) {
|
|
91
|
+
if (!(url in preloadedContexts)) return null;
|
|
92
|
+
return {
|
|
93
|
+
contextUrl: null,
|
|
94
|
+
document: preloadedContexts[url],
|
|
95
|
+
documentUrl: url
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function sha256(value) {
|
|
99
|
+
return encodeHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialize(value))));
|
|
100
|
+
}
|
|
101
|
+
function normalizeResponse(response) {
|
|
102
|
+
return {
|
|
103
|
+
contextUrl: response.contextUrl ?? null,
|
|
104
|
+
document: response.document,
|
|
105
|
+
documentUrl: response.documentUrl
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Serializes the entries of a context snapshot for a queued message.
|
|
110
|
+
* @param entries The entries.
|
|
111
|
+
* @returns The serialized entries, which are plain JSON values.
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
async function serializeContextSnapshot(entries) {
|
|
115
|
+
const result = [];
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const response = normalizeResponse(entry.response);
|
|
118
|
+
const hash = await sha256(response);
|
|
119
|
+
const bundled = getBundledResponse(entry.url);
|
|
120
|
+
if (bundled != null && await sha256(bundled) === hash) result.push({
|
|
121
|
+
policy: entry.policy,
|
|
122
|
+
url: entry.url,
|
|
123
|
+
sha256: hash,
|
|
124
|
+
bundled: true
|
|
125
|
+
});
|
|
126
|
+
else result.push({
|
|
127
|
+
policy: entry.policy,
|
|
128
|
+
url: entry.url,
|
|
129
|
+
sha256: hash,
|
|
130
|
+
response: structuredClone(response)
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
function isRemoteDocument(value) {
|
|
136
|
+
if (value == null || typeof value !== "object") return false;
|
|
137
|
+
const record = value;
|
|
138
|
+
return typeof record.documentUrl === "string" && "document" in record && (record.contextUrl == null || typeof record.contextUrl === "string");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Restores the entries of a serialized context snapshot, verifying the
|
|
142
|
+
* hashes of stored responses. Bundled entries are reconstructed from the
|
|
143
|
+
* contexts this version of Fedify bundles.
|
|
144
|
+
* @param entries The serialized entries.
|
|
145
|
+
* @returns The entries.
|
|
146
|
+
* @throws {ContextSnapshotMissError} If an entry is malformed, or its
|
|
147
|
+
* content does not match its hash.
|
|
148
|
+
* @internal
|
|
149
|
+
*/
|
|
150
|
+
async function deserializeContextSnapshot(entries) {
|
|
151
|
+
if (!Array.isArray(entries)) throw new ContextSnapshotMissError("(snapshot)", "malformed snapshot");
|
|
152
|
+
const result = [];
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (entry == null || typeof entry !== "object" || entry.policy !== "standard" && entry.policy !== "normalization" || typeof entry.url !== "string" || typeof entry.sha256 !== "string") throw new ContextSnapshotMissError("(snapshot)", "malformed entry");
|
|
155
|
+
let response;
|
|
156
|
+
if (entry.bundled === true) {
|
|
157
|
+
response = getBundledResponse(entry.url);
|
|
158
|
+
if (response == null) throw new ContextSnapshotMissError(entry.url, "not bundled");
|
|
159
|
+
} else if (isRemoteDocument(entry.response)) response = normalizeResponse(entry.response);
|
|
160
|
+
else throw new ContextSnapshotMissError(entry.url, "malformed entry");
|
|
161
|
+
if (entry.bundled !== true && await sha256(response) !== entry.sha256) throw new ContextSnapshotMissError(entry.url, "content mismatch");
|
|
162
|
+
result.push({
|
|
163
|
+
policy: entry.policy,
|
|
164
|
+
url: entry.url,
|
|
165
|
+
response: structuredClone(response)
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Creates a context loader of a policy that serves only the responses of
|
|
172
|
+
* a snapshot. It is marked as the effective loader of the policy.
|
|
173
|
+
* @param entries The snapshot's entries.
|
|
174
|
+
* @param policy The policy.
|
|
175
|
+
* @returns The loader, which throws {@link ContextSnapshotMissError} for
|
|
176
|
+
* anything else.
|
|
177
|
+
* @internal
|
|
178
|
+
*/
|
|
179
|
+
function createSnapshotOnlyLoader(entries, policy) {
|
|
180
|
+
const responses = /* @__PURE__ */ new Map();
|
|
181
|
+
for (const entry of entries) if (entry.policy === policy) responses.set(entry.url, entry.response);
|
|
182
|
+
return markEffectiveContextLoader(async (url) => {
|
|
183
|
+
const response = responses.get(normalizeUrl(url));
|
|
184
|
+
if (response == null) throw new ContextSnapshotMissError(url, "not in the snapshot");
|
|
185
|
+
return structuredClone(response);
|
|
186
|
+
}, policy);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Creates a {@link ReleasableLoader}.
|
|
190
|
+
* @param strict The loader to use until release.
|
|
191
|
+
* @param fallback The loader to use after release.
|
|
192
|
+
* @param policy The policy both loaders apply.
|
|
193
|
+
* @returns The releasable loader.
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
function createReleasableLoader(strict, fallback, policy) {
|
|
197
|
+
let released = false;
|
|
198
|
+
return {
|
|
199
|
+
loader: markEffectiveContextLoader((url, options) => released ? fallback(url, options) : strict(url, options), policy),
|
|
200
|
+
release() {
|
|
201
|
+
released = true;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Expands a JSON-LD document with the given context loader.
|
|
207
|
+
* @param document The document.
|
|
208
|
+
* @param contextLoader The context loader.
|
|
209
|
+
* @returns The expanded document.
|
|
210
|
+
* @internal
|
|
211
|
+
*/
|
|
212
|
+
async function expandDocument(document, contextLoader) {
|
|
213
|
+
return await jsonld.expand(document, { documentLoader: contextLoader });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Computes the digest of an expanded document.
|
|
217
|
+
* @param expanded The expanded document.
|
|
218
|
+
* @returns The hex-encoded SHA-256 of its canonical JSON.
|
|
219
|
+
* @internal
|
|
220
|
+
*/
|
|
221
|
+
function digestExpandedDocument(expanded) {
|
|
222
|
+
return sha256(expanded);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Deeply freezes a JSON value.
|
|
226
|
+
* @param value The value.
|
|
227
|
+
* @returns The same value, frozen.
|
|
228
|
+
* @internal
|
|
229
|
+
*/
|
|
230
|
+
function deepFreeze(value) {
|
|
231
|
+
if (value != null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
232
|
+
Object.freeze(value);
|
|
233
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
const ACTOR_IRI = "https://www.w3.org/ns/activitystreams#actor";
|
|
238
|
+
/**
|
|
239
|
+
* Gets the single root node of an expanded document.
|
|
240
|
+
* @param expanded The expanded document.
|
|
241
|
+
* @returns The root node, or `null` if there is not exactly one.
|
|
242
|
+
* @internal
|
|
243
|
+
*/
|
|
244
|
+
function getRootNode(expanded) {
|
|
245
|
+
if (expanded.length !== 1) return null;
|
|
246
|
+
const node = expanded[0];
|
|
247
|
+
if (node == null || typeof node !== "object" || Array.isArray(node)) return null;
|
|
248
|
+
return node;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Gets the values of a property of an expanded node, unwrapping a single
|
|
252
|
+
* `@list` container the way generated vocabulary decoders do.
|
|
253
|
+
* @param node The expanded node.
|
|
254
|
+
* @param iri The property IRI.
|
|
255
|
+
* @returns The values, or an empty array if the property is absent.
|
|
256
|
+
* @internal
|
|
257
|
+
*/
|
|
258
|
+
function getPropertyValues(node, iri) {
|
|
259
|
+
const values = node[iri];
|
|
260
|
+
if (values == null) return [];
|
|
261
|
+
if (!Array.isArray(values)) return [values];
|
|
262
|
+
if (values.length === 1 && values[0] != null && typeof values[0] === "object" && "@list" in values[0]) {
|
|
263
|
+
const list = values[0]["@list"];
|
|
264
|
+
return Array.isArray(list) ? list : [list];
|
|
265
|
+
}
|
|
266
|
+
return values;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Tells whether the root node of an expanded activity declares any `actor`
|
|
270
|
+
* value at all, including blank nodes, anonymous or untyped nodes, and
|
|
271
|
+
* literals, which ID accessors and typed parsing may drop.
|
|
272
|
+
* @param expanded The expanded activity.
|
|
273
|
+
* @returns `"absent"` if the root node declares no actor; `"present"`
|
|
274
|
+
* otherwise, including when there is no single root node.
|
|
275
|
+
* @internal
|
|
276
|
+
*/
|
|
277
|
+
function getActorPresence(expanded) {
|
|
278
|
+
const root = getRootNode(expanded);
|
|
279
|
+
if (root == null) return "present";
|
|
280
|
+
return getPropertyValues(root, ACTOR_IRI).length < 1 ? "absent" : "present";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Builds the serialized view of an authenticated activity. The document is
|
|
284
|
+
* expanded through the snapshot, so that the snapshot holds every context
|
|
285
|
+
* response the interpretation needs.
|
|
286
|
+
* @param snapshot The delivery's context snapshot.
|
|
287
|
+
* @param source Which document the activity was parsed from.
|
|
288
|
+
* @returns The serialized view and the expanded activity.
|
|
289
|
+
* @internal
|
|
290
|
+
*/
|
|
291
|
+
async function buildVerifiedActivityView(snapshot, source) {
|
|
292
|
+
const loader = snapshot.loader(source.policy);
|
|
293
|
+
const used = /* @__PURE__ */ new Set();
|
|
294
|
+
const expanded = await expandDocument(source.document, markEffectiveContextLoader((url, options) => {
|
|
295
|
+
used.add(normalizeUrl(url));
|
|
296
|
+
return loader(url, options);
|
|
297
|
+
}, source.policy));
|
|
298
|
+
const entries = snapshot.entries().filter((entry) => entry.policy === source.policy && used.has(entry.url));
|
|
299
|
+
const contexts = await serializeContextSnapshot(entries);
|
|
300
|
+
const references = /* @__PURE__ */ new Set();
|
|
301
|
+
collectContextReferences(source.document, source.baseUrl?.href, references);
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
const { contextUrl, document } = entry.response;
|
|
304
|
+
collectContextReferences(document, entry.url, references);
|
|
305
|
+
if (contextUrl != null) collectContextReferences({ "@context": contextUrl }, entry.url, references);
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
replayable: [...references].every((url) => used.has(url)),
|
|
309
|
+
view: {
|
|
310
|
+
version: 1,
|
|
311
|
+
policy: source.policy,
|
|
312
|
+
source: source.source,
|
|
313
|
+
baseUrl: source.baseUrl?.href ?? null,
|
|
314
|
+
contexts,
|
|
315
|
+
expandedDigest: await digestExpandedDocument(expanded)
|
|
316
|
+
},
|
|
317
|
+
expanded
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Collects the remote context references of a JSON-LD document or context
|
|
322
|
+
* document: every IRI given as a value of `@context` at any depth, including
|
|
323
|
+
* scoped contexts of term definitions, and of `@import`.
|
|
324
|
+
* @param value The document.
|
|
325
|
+
* @param base The URL to resolve relative references against.
|
|
326
|
+
* @param references The set to add the normalized references to.
|
|
327
|
+
*/
|
|
328
|
+
function collectContextReferences(value, base, references) {
|
|
329
|
+
const add = (reference) => {
|
|
330
|
+
let url = reference;
|
|
331
|
+
if (base != null && URL.canParse(reference, base)) url = new URL(reference, base).href;
|
|
332
|
+
references.add(normalizeUrl(url));
|
|
333
|
+
};
|
|
334
|
+
const collectContext = (context) => {
|
|
335
|
+
if (typeof context === "string") add(context);
|
|
336
|
+
else if (Array.isArray(context)) for (const item of context) collectContext(item);
|
|
337
|
+
else if (context != null && typeof context === "object") {
|
|
338
|
+
const imported = context["@import"];
|
|
339
|
+
if (typeof imported === "string") add(imported);
|
|
340
|
+
walk(context);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const walk = (node) => {
|
|
344
|
+
if (Array.isArray(node)) for (const item of node) walk(item);
|
|
345
|
+
else if (node != null && typeof node === "object") for (const [key, item] of Object.entries(node)) if (key === "@context") collectContext(item);
|
|
346
|
+
else walk(item);
|
|
347
|
+
};
|
|
348
|
+
walk(value);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Validates the shape of a serialized view taken from a queued message.
|
|
352
|
+
* @param value The value of the message's `verifiedView` field.
|
|
353
|
+
* @returns The view, or `null` if it is malformed or of an unsupported
|
|
354
|
+
* version.
|
|
355
|
+
* @internal
|
|
356
|
+
*/
|
|
357
|
+
function parseSerializedVerifiedActivityView(value) {
|
|
358
|
+
if (value == null || typeof value !== "object") return null;
|
|
359
|
+
const view = value;
|
|
360
|
+
if (view.version !== 1 || view.policy !== "standard" && view.policy !== "normalization" || view.source !== "activity" && view.source !== "normalizedActivity" || view.baseUrl !== null && (typeof view.baseUrl !== "string" || !URL.canParse(view.baseUrl)) || !Array.isArray(view.contexts) || typeof view.expandedDigest !== "string") return null;
|
|
361
|
+
return view;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Restores a view from a queued message without loading any context.
|
|
365
|
+
* @param view The serialized view.
|
|
366
|
+
* @param message The fields of the message the view refers to.
|
|
367
|
+
* @returns The restored view.
|
|
368
|
+
* @throws {ContextSnapshotMissError} If the view cannot be replayed exactly.
|
|
369
|
+
* @internal
|
|
370
|
+
*/
|
|
371
|
+
async function restoreVerifiedActivityView(view, message) {
|
|
372
|
+
const loader = createSnapshotOnlyLoader(await deserializeContextSnapshot(view.contexts), view.policy);
|
|
373
|
+
if (view.source === "normalizedActivity" && message.normalizedActivity == null) throw new ContextSnapshotMissError("(normalizedActivity)", "the normalized activity is missing");
|
|
374
|
+
const document = detachSignature(view.source === "activity" ? message.activity : message.normalizedActivity);
|
|
375
|
+
const expanded = await expandDocument(document, loader);
|
|
376
|
+
if (await digestExpandedDocument(expanded) !== view.expandedDigest) throw new ContextSnapshotMissError("(activity)", "the interpretation does not match the authenticated one");
|
|
377
|
+
return {
|
|
378
|
+
document,
|
|
379
|
+
loader,
|
|
380
|
+
expanded,
|
|
381
|
+
baseUrl: view.baseUrl == null ? null : new URL(view.baseUrl)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
//#endregion
|
|
385
|
+
export { createSnapshotOnlyLoader as a, expandDocument as c, parseSerializedVerifiedActivityView as d, restoreVerifiedActivityView as f, createReleasableLoader as i, getActorPresence as l, buildVerifiedActivityView as n, deepFreeze as o, serializeContextSnapshot as p, createContextSnapshot as r, deserializeContextSnapshot as s, ContextSnapshotMissError as t, getPropertyValues as u };
|