@fedify/vocab-runtime 2.4.0-dev.2030 → 2.4.0-dev.2047

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/deno.json +2 -1
  2. package/dist/internal/jsonld-cache.cjs +1 -1
  3. package/dist/internal/jsonld-cache.js +1 -1
  4. package/dist/internal/portable-dereference.cjs +315 -0
  5. package/dist/internal/portable-dereference.d.cts +87 -0
  6. package/dist/internal/portable-dereference.d.ts +87 -0
  7. package/dist/internal/portable-dereference.js +311 -0
  8. package/dist/mod.cjs +11 -3
  9. package/dist/mod.d.cts +75 -1
  10. package/dist/mod.d.ts +75 -1
  11. package/dist/mod.js +10 -4
  12. package/dist/portable-CduElToy.d.ts +52 -0
  13. package/dist/portable-DxF2Ut-Z.d.cts +52 -0
  14. package/dist/tests/{body-B9qsaRLZ.cjs → body-BgDeabVX.cjs} +1 -1
  15. package/dist/tests/{body-BhoBj6zg.mjs → body-D7iDYZxi.mjs} +1 -1
  16. package/dist/tests/body.test.cjs +1 -1
  17. package/dist/tests/body.test.mjs +1 -1
  18. package/dist/tests/decimal.test.cjs +2 -2
  19. package/dist/tests/decimal.test.mjs +2 -2
  20. package/dist/tests/{docloader--w8_lTnA.cjs → docloader-0r0eohuK.cjs} +3 -3
  21. package/dist/tests/{docloader-DKk8OANv.mjs → docloader-DzfRRzKc.mjs} +3 -3
  22. package/dist/tests/docloader.test.cjs +3 -3
  23. package/dist/tests/docloader.test.mjs +3 -3
  24. package/dist/tests/internal/portable-dereference.test.cjs +123 -0
  25. package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
  26. package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
  27. package/dist/tests/internal/portable-dereference.test.mjs +124 -0
  28. package/dist/tests/jsonld-CmDhHNTx.cjs +12 -0
  29. package/dist/tests/jsonld-DweIJ07b.mjs +5 -0
  30. package/dist/tests/jsonld-cache.test.cjs +18 -23
  31. package/dist/tests/jsonld-cache.test.mjs +2 -5
  32. package/dist/tests/{request-Dm_aAoKt.mjs → request-DOyNI0N1.mjs} +8 -2
  33. package/dist/tests/{request-D3Z--Onp.cjs → request-DdT5TslC.cjs} +8 -2
  34. package/dist/tests/request.test.cjs +4 -1
  35. package/dist/tests/request.test.mjs +4 -1
  36. package/dist/{url-i33c0US6.cjs → tests/url-BqlldzT8.cjs} +164 -1
  37. package/dist/tests/{url-Basm1L_c.mjs → url-DgJM9WJn.mjs} +147 -1
  38. package/dist/tests/url.test.cjs +220 -1
  39. package/dist/tests/url.test.mjs +220 -1
  40. package/dist/{url-BHAShw9J.js → url-CEhi4hb4.js} +147 -1
  41. package/dist/{tests/url-CPfMatUL.cjs → url-Cke4irYD.cjs} +155 -2
  42. package/package.json +12 -2
  43. package/src/internal/portable-dereference.test.ts +74 -0
  44. package/src/internal/portable-dereference.ts +426 -0
  45. package/src/mod.ts +6 -0
  46. package/src/portable.ts +52 -0
  47. package/src/request.test.ts +10 -1
  48. package/src/request.ts +9 -1
  49. package/src/url.test.ts +361 -0
  50. package/src/url.ts +235 -0
  51. package/tsdown.config.ts +1 -0
@@ -0,0 +1,311 @@
1
+
2
+ import { t as preloadedContexts } from "../contexts-DoZUIW7y.js";
3
+ import { a as formatIri, g as toCompatibleEf61Id, m as parseIri, r as canonicalizePortableUri } from "../url-CEhi4hb4.js";
4
+ import jsonld_default from "../jsonld.js";
5
+ import { getLogger } from "@logtape/logtape";
6
+ import { SpanStatusCode } from "@opentelemetry/api";
7
+ //#region src/internal/portable-dereference.ts
8
+ const logger = getLogger([
9
+ "fedify",
10
+ "vocab",
11
+ "gateway"
12
+ ]);
13
+ /**
14
+ * The maximum number of `@gateway` location hints to try for one reference.
15
+ * Hints come from possibly untrusted documents, so they are bounded to keep
16
+ * a single accessor call from fanning out to many servers.
17
+ */
18
+ const MAX_GATEWAY_HINTS = 5;
19
+ const LOCATION_HINT_PARAMETER = "@gateway";
20
+ const BASELINE_CONTEXT_URLS = /* @__PURE__ */ new Set([
21
+ "https://w3id.org/identity/v1",
22
+ "https://www.w3.org/ns/activitystreams",
23
+ "https://w3id.org/security/v1",
24
+ "https://w3id.org/security/data-integrity/v1"
25
+ ]);
26
+ /**
27
+ * Checks whether a URL is an FEP-ef61 portable ActivityPub IRI.
28
+ *
29
+ * @internal Technically exported for generated vocabulary classes, but not
30
+ * part of the public API contract. This is not considered public API for
31
+ * Semantic Versioning decisions.
32
+ */
33
+ function isPortableIri(url) {
34
+ return url.protocol === "ap+ef61:" || url.protocol === "ap:";
35
+ }
36
+ /**
37
+ * Picks the ordered list of FEP-ef61 gateways to fetch a portable IRI from.
38
+ *
39
+ * If `gateways` is given, it is used as is (even when empty), and `@gateway`
40
+ * location hints in the IRI are ignored. Otherwise, up to
41
+ * {@link MAX_GATEWAY_HINTS} valid `@gateway` hints are used. Duplicate
42
+ * gateways are dropped in both cases.
43
+ *
44
+ * @throws {TypeError} If an explicit gateway is not an HTTP(S) origin.
45
+ * @internal
46
+ */
47
+ function getPortableGatewayCandidates(url, gateways) {
48
+ const candidates = [];
49
+ const seen = /* @__PURE__ */ new Set();
50
+ const add = (gateway) => {
51
+ if (seen.has(gateway.href)) return;
52
+ seen.add(gateway.href);
53
+ candidates.push(gateway);
54
+ };
55
+ if (gateways != null) {
56
+ for (const gateway of gateways) {
57
+ const parsed = parseGatewayOrigin(gateway);
58
+ if (parsed == null) throw new TypeError("FEP-ef61 gateways must be HTTP(S) origins with no credentials, path, query, or fragment: " + String(gateway));
59
+ add(parsed);
60
+ }
61
+ return candidates;
62
+ }
63
+ for (const hint of new URLSearchParams(url.search).getAll(LOCATION_HINT_PARAMETER)) {
64
+ if (candidates.length >= MAX_GATEWAY_HINTS) break;
65
+ const parsed = parseGatewayOrigin(hint);
66
+ if (parsed == null) {
67
+ logger.debug("Ignoring an invalid FEP-ef61 gateway hint {hint} in {url}.", {
68
+ hint,
69
+ url: formatIri(url)
70
+ });
71
+ continue;
72
+ }
73
+ add(parsed);
74
+ }
75
+ return candidates;
76
+ }
77
+ function parseGatewayOrigin(gateway) {
78
+ let url;
79
+ if (gateway instanceof URL) url = new URL(gateway.href);
80
+ else if (typeof gateway === "string" && URL.canParse(gateway)) url = new URL(gateway);
81
+ else return null;
82
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.href !== `${url.origin}/`) return null;
83
+ return url;
84
+ }
85
+ /**
86
+ * Creates a context loader that returns the same context documents for the
87
+ * whole dereference operation, so that the identity check, the proof
88
+ * verifier, and the parser interpret the fetched document identically even
89
+ * if the underlying loader is nondeterministic. Failed loads are not
90
+ * remembered, so a transient failure does not affect the next gateway.
91
+ *
92
+ * The parsed object keeps the loader for its own later dereferences, so
93
+ * `release()` turns it into a plain pass-through to the underlying loader
94
+ * once the operation is over.
95
+ */
96
+ function createSnapshotContextLoader(contextLoader) {
97
+ const cache = /* @__PURE__ */ new Map();
98
+ let released = false;
99
+ const release = () => {
100
+ released = true;
101
+ cache.clear();
102
+ };
103
+ const loader = async (url, options) => {
104
+ if (released) return await contextLoader(url, options);
105
+ const key = URL.canParse(url) ? new URL(url).href : url;
106
+ if (BASELINE_CONTEXT_URLS.has(key)) return {
107
+ contextUrl: null,
108
+ document: structuredClone(preloadedContexts[key]),
109
+ documentUrl: key
110
+ };
111
+ let promise = cache.get(key);
112
+ if (promise == null) {
113
+ const loading = contextLoader(url, options).then((document) => structuredClone(document));
114
+ promise = loading;
115
+ cache.set(key, loading);
116
+ loading.catch(() => {
117
+ if (cache.get(key) === loading) cache.delete(key);
118
+ });
119
+ }
120
+ return structuredClone(await promise);
121
+ };
122
+ return {
123
+ loader,
124
+ release
125
+ };
126
+ }
127
+ /**
128
+ * The error thrown by {@link dereferencePortableIri} under
129
+ * `crossOrigin: "throw"` when gateways returned objects but none of them
130
+ * satisfied the identity and proof checks.
131
+ *
132
+ * @internal Not part of the public API contract.
133
+ */
134
+ var PortableObjectRejectedError = class extends Error {
135
+ constructor(message) {
136
+ super(message);
137
+ this.name = "PortableObjectRejectedError";
138
+ }
139
+ };
140
+ var PortableObjectRejection = class {
141
+ message;
142
+ values;
143
+ constructor(message, values = {}) {
144
+ this.message = message;
145
+ this.values = values;
146
+ }
147
+ };
148
+ /**
149
+ * Dereferences an FEP-ef61 portable IRI through its gateways.
150
+ *
151
+ * Gateways are tried one by one until one of them returns a single-node
152
+ * document whose `@id`, if any, canonically matches the requested IRI and
153
+ * which passes `verifyPortableObject`. When there is no gateway to try, the document
154
+ * loader is asked for the portable IRI itself, and its result is validated
155
+ * the same way.
156
+ *
157
+ * @returns The parsed object, or `null` if no valid object was retrieved and
158
+ * `suppressError` is set or the failure was a rejected object
159
+ * (unless `crossOrigin` is `"throw"`).
160
+ * @throws {TypeError} If `gateways` has an invalid entry (always), if no
161
+ * verifier is given, or if the IRI cannot be turned into
162
+ * a gateway URL (unless `suppressError` is set).
163
+ * @internal Technically exported for generated vocabulary classes, but not
164
+ * part of the public API contract. This is not considered public API for
165
+ * Semantic Versioning decisions.
166
+ */
167
+ async function dereferencePortableIri(url, options) {
168
+ url = parseIri(url);
169
+ const lookupUrl = formatIri(url);
170
+ const { span } = options;
171
+ const gateways = getPortableGatewayCandidates(url, options.gateways);
172
+ const fail = (error) => {
173
+ span?.setStatus({
174
+ code: SpanStatusCode.ERROR,
175
+ message: String(error)
176
+ });
177
+ if (options.suppressError) {
178
+ logger.error("Failed to dereference {url}: {error}", {
179
+ url: lookupUrl,
180
+ error
181
+ });
182
+ return null;
183
+ }
184
+ throw error;
185
+ };
186
+ const verify = options.verifyPortableObject;
187
+ if (verify == null) return fail(/* @__PURE__ */ new TypeError("Dereferencing the portable object " + lookupUrl + " requires the verifyPortableObject option, e.g., verifyPortableObjectProof() from @fedify/fedify."));
188
+ const requestUrls = [];
189
+ let expectedId;
190
+ try {
191
+ expectedId = canonicalizePortableUri(lookupUrl);
192
+ for (const gateway of gateways) requestUrls.push({
193
+ url: toCompatibleEf61Id(url, gateway).href,
194
+ gateway
195
+ });
196
+ } catch (error) {
197
+ return fail(error);
198
+ }
199
+ if (requestUrls.length < 1) requestUrls.push({
200
+ url: lookupUrl,
201
+ gateway: null
202
+ });
203
+ const snapshot = createSnapshotContextLoader(options.contextLoader);
204
+ const contextLoader = snapshot.loader;
205
+ const attempts = [];
206
+ const { signal } = options;
207
+ try {
208
+ for (const { url: requestUrl, gateway } of requestUrls) {
209
+ signal?.throwIfAborted();
210
+ let remoteDocument;
211
+ try {
212
+ remoteDocument = await options.documentLoader(requestUrl, signal == null ? void 0 : { signal });
213
+ } catch (error) {
214
+ signal?.throwIfAborted();
215
+ logger.debug("Failed to fetch {url} from {requestUrl}: {error}", {
216
+ url: lookupUrl,
217
+ requestUrl,
218
+ error
219
+ });
220
+ attempts.push({
221
+ type: "error",
222
+ error
223
+ });
224
+ continue;
225
+ }
226
+ const { document } = remoteDocument;
227
+ try {
228
+ await checkPortableObjectId(document, expectedId, contextLoader);
229
+ let result;
230
+ try {
231
+ result = await verify(document, {
232
+ documentLoader: options.documentLoader,
233
+ contextLoader,
234
+ tracerProvider: options.tracerProvider
235
+ });
236
+ } catch (error) {
237
+ throw new PortableObjectRejection("the proof verifier failed: {error}", { error });
238
+ }
239
+ if (!result.verified) throw new PortableObjectRejection("it does not satisfy the FEP-ef61 proof policy: {result}", { result });
240
+ const object = await options.parse(document, {
241
+ contextLoader,
242
+ baseUrl: url
243
+ });
244
+ signal?.throwIfAborted();
245
+ if (gateway != null) span?.setAttribute("activitypub.gateway", gateway.href);
246
+ return object;
247
+ } catch (error) {
248
+ signal?.throwIfAborted();
249
+ if (error instanceof PortableObjectRejection) {
250
+ logger.warn("Rejected the portable object {url} served from {requestUrl}, because " + error.message, {
251
+ ...error.values,
252
+ url: lookupUrl,
253
+ requestUrl
254
+ });
255
+ attempts.push({
256
+ type: "rejected",
257
+ source: requestUrl
258
+ });
259
+ continue;
260
+ }
261
+ logger.debug("Failed to parse {url} from {requestUrl}: {error}", {
262
+ url: lookupUrl,
263
+ requestUrl,
264
+ error
265
+ });
266
+ attempts.push({
267
+ type: "error",
268
+ error
269
+ });
270
+ }
271
+ }
272
+ } finally {
273
+ snapshot.release();
274
+ }
275
+ const rejected = attempts.filter((a) => a.type === "rejected");
276
+ if (rejected.length > 0) {
277
+ const message = "No gateway returned a valid portable object for " + lookupUrl + "; refusing to return the object. Objects retrieved from: " + rejected.map((a) => a.source).join(", ") + ".";
278
+ if (options.suppressError || options.crossOrigin !== "throw") {
279
+ span?.setStatus({
280
+ code: SpanStatusCode.ERROR,
281
+ message
282
+ });
283
+ logger.warn(message);
284
+ return null;
285
+ }
286
+ throw new PortableObjectRejectedError(message);
287
+ }
288
+ const errors = attempts.flatMap((a) => a.type === "error" ? [a.error] : []);
289
+ return fail(errors.length === 1 ? errors[0] : new AggregateError(errors, "Failed to dereference " + lookupUrl + " through any of its gateways: " + requestUrls.map((r) => r.url).join(", ")));
290
+ }
291
+ async function checkPortableObjectId(document, expectedId, contextLoader) {
292
+ if (document == null || typeof document !== "object" || Array.isArray(document)) throw new PortableObjectRejection("it is not a single JSON object");
293
+ const expanded = await jsonld_default.expand(document, {
294
+ documentLoader: contextLoader,
295
+ keepFreeFloatingNodes: true
296
+ });
297
+ if (expanded.length !== 1) throw new PortableObjectRejection("it does not have exactly one top-level node");
298
+ const id = expanded[0]["@id"];
299
+ if (id == null) return;
300
+ if (typeof id !== "string") throw new PortableObjectRejection("it has an invalid @id");
301
+ let actualId;
302
+ try {
303
+ actualId = canonicalizePortableUri(id);
304
+ } catch (error) {
305
+ if (!(error instanceof TypeError)) throw error;
306
+ actualId = null;
307
+ }
308
+ if (actualId !== expectedId) throw new PortableObjectRejection("its @id ({objectId}) does not match the requested portable ID", { objectId: id });
309
+ }
310
+ //#endregion
311
+ export { PortableObjectRejectedError, dereferencePortableIri, getPortableGatewayCandidates, isPortableIri };
package/dist/mod.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_rolldown_runtime = require("./rolldown-runtime-B7lfambq.cjs");
4
4
  const require_contexts = require("./contexts-2hEbs3Qk.cjs");
5
- const require_url = require("./url-i33c0US6.cjs");
5
+ const require_url = require("./url-Cke4irYD.cjs");
6
6
  let _logtape_logtape = require("@logtape/logtape");
7
7
  let _opentelemetry_api = require("@opentelemetry/api");
8
8
  let node_process = require("node:process");
@@ -16,7 +16,7 @@ let _multiformats_base_x = require("@multiformats/base-x");
16
16
  _multiformats_base_x = require_rolldown_runtime.__toESM(_multiformats_base_x, 1);
17
17
  //#region deno.json
18
18
  var name = "@fedify/vocab-runtime";
19
- var version = "2.4.0-dev.2030+e6b3de3d";
19
+ var version = "2.4.0-dev.2047+140b94db";
20
20
  //#endregion
21
21
  //#region src/request.ts
22
22
  /**
@@ -46,6 +46,12 @@ var FetchError = class extends Error {
46
46
  }
47
47
  };
48
48
  /**
49
+ * The `Accept` header value for fetching ActivityPub objects. ActivityPub
50
+ * and FEP-ef61 gateways require the ActivityStreams profile on the JSON-LD
51
+ * media type.
52
+ */
53
+ const ACTIVITYPUB_ACCEPT = "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
54
+ /**
49
55
  * Creates a request for the given URL.
50
56
  * @param url The URL to create the request for.
51
57
  * @param options The options for the request.
@@ -55,7 +61,7 @@ var FetchError = class extends Error {
55
61
  function createActivityPubRequest(url, options = {}) {
56
62
  return new Request(url, {
57
63
  headers: {
58
- Accept: "application/activity+json, application/ld+json",
64
+ Accept: ACTIVITYPUB_ACCEPT,
59
65
  "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
60
66
  },
61
67
  redirect: "manual"
@@ -1379,6 +1385,7 @@ exports.exportDidKey = exportDidKey;
1379
1385
  exports.exportMultibaseKey = exportMultibaseKey;
1380
1386
  exports.exportSpki = exportSpki;
1381
1387
  exports.formatIri = require_url.formatIri;
1388
+ exports.fromCompatibleEf61Id = require_url.fromCompatibleEf61Id;
1382
1389
  exports.getDocumentLoader = getDocumentLoader;
1383
1390
  exports.getFe34Origin = require_url.getFe34Origin;
1384
1391
  exports.getRemoteDocument = getRemoteDocument;
@@ -1403,6 +1410,7 @@ exports.parseHashlink = parseHashlink;
1403
1410
  exports.parseIri = require_url.parseIri;
1404
1411
  exports.parseJsonLdId = require_url.parseJsonLdId;
1405
1412
  exports.preloadedContexts = require_contexts.preloadedContexts;
1413
+ exports.toCompatibleEf61Id = require_url.toCompatibleEf61Id;
1406
1414
  exports.validatePublicUrl = require_url.validatePublicUrl;
1407
1415
  exports.verifyDigestMultibase = verifyDigestMultibase;
1408
1416
  exports.verifyHashlink = verifyHashlink;
package/dist/mod.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { a as DocumentLoaderOptions, c as getDocumentLoader, d as FetchError, f as GetUserAgentOptions, h as logRequest, i as DocumentLoaderFactoryOptions, l as getRemoteDocument, m as getUserAgent, n as DocumentLoader, o as GetDocumentLoaderOptions, p as createActivityPubRequest, r as DocumentLoaderFactory, s as RemoteDocument, t as AuthenticatedDocumentLoaderFactory, u as CreateRequestOptions } from "./docloader-Dr8wEkif.cjs";
3
+ import { n as PortableObjectVerifierOptions, t as PortableObjectVerifier } from "./portable-DxF2Ut-Z.cjs";
3
4
  import { TracerProvider } from "@opentelemetry/api";
4
5
 
5
6
  //#region src/contexts.d.ts
@@ -466,6 +467,79 @@ declare function isGatewayUrl(url: URL): boolean;
466
467
  */
467
468
  declare function parseGatewayUrl(url: string): URL;
468
469
  /**
470
+ * Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
471
+ * URI.
472
+ *
473
+ * A compatible identifier is an HTTP(S) URL under a gateway's fixed
474
+ * `/.well-known/apgateway/` path, such as
475
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
476
+ * This function removes the gateway part and returns the corresponding
477
+ * portable URI, e.g., `ap+ef61://did:key:z6Mk.../objects/1`, in the same
478
+ * internal `URL` form that {@link parseIri} produces. The path, query, and
479
+ * fragment are preserved, so the result is a portable URI, not a comparison
480
+ * form; pass its `href` to {@link canonicalizePortableUri} or
481
+ * {@link arePortableUrisEqual} to compare it with other portable URIs.
482
+ *
483
+ * The conversion only reveals the *claimed* portable identifier. Anyone can
484
+ * publish a compatible identifier for any DID on their own server, so the
485
+ * gateway that served it is neither the object's origin nor authorized to act
486
+ * for the DID. Callers must still verify the retrieved document's Object
487
+ * Integrity Proof against the DID, as FEP-ef61 requires, and should keep the
488
+ * original URL if they need the gateway as a retrieval hint.
489
+ *
490
+ * Arbitrary gateway paths are not supported.
491
+ *
492
+ * [FEP-ef61]: https://w3id.org/fep/ef61
493
+ *
494
+ * @param input The URL to convert.
495
+ * @returns The portable ActivityPub URI, or `null` if the input is not an
496
+ * HTTP(S) URL whose path starts with `/.well-known/apgateway/did:`.
497
+ * Other gateway routes, such as the gateway discovery endpoint and
498
+ * hashlink media URLs, also yield `null`.
499
+ * @throws {TypeError} If the input looks like a compatible identifier but is
500
+ * malformed, e.g., it has an invalid DID, no object path,
501
+ * invalid percent-encoding, credentials, or location
502
+ * hints (`@gateway` query parameters, or the legacy
503
+ * `gateways` parameter), which FEP-ef61 forbids in
504
+ * compatible identifiers.
505
+ * @since 2.4.0
506
+ */
507
+ declare function fromCompatibleEf61Id(input: string | URL): URL | null;
508
+ /**
509
+ * Converts a portable ActivityPub URI into an [FEP-ef61] compatible
510
+ * identifier, which is an HTTP(S) URL under the gateway's fixed
511
+ * `/.well-known/apgateway/` path.
512
+ *
513
+ * For example, `ap+ef61://did:key:z6Mk.../objects/1` and
514
+ * `https://server.example` yield
515
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
516
+ * Both `ap:` and `ap+ef61:` URIs with decoded or percent-encoded DID
517
+ * authorities are accepted. Publishers should use the first gateway in the
518
+ * actor's `gateways` list, as FEP-ef61 requires.
519
+ *
520
+ * The path and fragment are preserved, with characters that are not allowed
521
+ * in HTTP(S) URLs percent-encoded the same way as
522
+ * {@link canonicalizePortableUri} does. FEP-ef61 location hints (`@gateway`
523
+ * query parameters, and the legacy `gateways` parameter) are removed, since
524
+ * compatible identifiers must not have them; other query parameters are kept
525
+ * in order.
526
+ *
527
+ * Arbitrary gateway paths are not supported.
528
+ *
529
+ * [FEP-ef61]: https://w3id.org/fep/ef61
530
+ *
531
+ * @param portableId The `ap:` or `ap+ef61:` URI to convert.
532
+ * @param gateway The gateway's HTTP(S) origin, e.g., `https://server.example`.
533
+ * @returns The compatible identifier.
534
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
535
+ * URI, if its path has `.` or `..` segments (which
536
+ * HTTP(S) URLs cannot represent), or if the gateway is
537
+ * not an HTTP(S) origin with no credentials, path, query,
538
+ * or fragment.
539
+ * @since 2.4.0
540
+ */
541
+ declare function toCompatibleEf61Id(portableId: string | URL, gateway: string | URL): URL;
542
+ /**
469
543
  * Validates a URL to prevent SSRF attacks.
470
544
  */
471
545
  declare function validatePublicUrl(url: string): Promise<void>;
@@ -473,4 +547,4 @@ declare function isValidPublicIPv4Address(address: string): boolean;
473
547
  declare function isValidPublicIPv6Address(address: string): boolean;
474
548
  declare function expandIPv6Address(address: string): string;
475
549
  //#endregion
476
- export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
550
+ export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PortableObjectVerifier, type PortableObjectVerifierOptions, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
package/dist/mod.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { a as DocumentLoaderOptions, c as getDocumentLoader, d as FetchError, f as GetUserAgentOptions, h as logRequest, i as DocumentLoaderFactoryOptions, l as getRemoteDocument, m as getUserAgent, n as DocumentLoader, o as GetDocumentLoaderOptions, p as createActivityPubRequest, r as DocumentLoaderFactory, s as RemoteDocument, t as AuthenticatedDocumentLoaderFactory, u as CreateRequestOptions } from "./docloader-Dr8wEkif.js";
3
+ import { n as PortableObjectVerifierOptions, t as PortableObjectVerifier } from "./portable-CduElToy.js";
3
4
  import { TracerProvider } from "@opentelemetry/api";
4
5
  //#region src/contexts.d.ts
5
6
  declare const preloadedContexts: Record<string, unknown>;
@@ -465,6 +466,79 @@ declare function isGatewayUrl(url: URL): boolean;
465
466
  */
466
467
  declare function parseGatewayUrl(url: string): URL;
467
468
  /**
469
+ * Converts an [FEP-ef61] compatible identifier into a portable ActivityPub
470
+ * URI.
471
+ *
472
+ * A compatible identifier is an HTTP(S) URL under a gateway's fixed
473
+ * `/.well-known/apgateway/` path, such as
474
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
475
+ * This function removes the gateway part and returns the corresponding
476
+ * portable URI, e.g., `ap+ef61://did:key:z6Mk.../objects/1`, in the same
477
+ * internal `URL` form that {@link parseIri} produces. The path, query, and
478
+ * fragment are preserved, so the result is a portable URI, not a comparison
479
+ * form; pass its `href` to {@link canonicalizePortableUri} or
480
+ * {@link arePortableUrisEqual} to compare it with other portable URIs.
481
+ *
482
+ * The conversion only reveals the *claimed* portable identifier. Anyone can
483
+ * publish a compatible identifier for any DID on their own server, so the
484
+ * gateway that served it is neither the object's origin nor authorized to act
485
+ * for the DID. Callers must still verify the retrieved document's Object
486
+ * Integrity Proof against the DID, as FEP-ef61 requires, and should keep the
487
+ * original URL if they need the gateway as a retrieval hint.
488
+ *
489
+ * Arbitrary gateway paths are not supported.
490
+ *
491
+ * [FEP-ef61]: https://w3id.org/fep/ef61
492
+ *
493
+ * @param input The URL to convert.
494
+ * @returns The portable ActivityPub URI, or `null` if the input is not an
495
+ * HTTP(S) URL whose path starts with `/.well-known/apgateway/did:`.
496
+ * Other gateway routes, such as the gateway discovery endpoint and
497
+ * hashlink media URLs, also yield `null`.
498
+ * @throws {TypeError} If the input looks like a compatible identifier but is
499
+ * malformed, e.g., it has an invalid DID, no object path,
500
+ * invalid percent-encoding, credentials, or location
501
+ * hints (`@gateway` query parameters, or the legacy
502
+ * `gateways` parameter), which FEP-ef61 forbids in
503
+ * compatible identifiers.
504
+ * @since 2.4.0
505
+ */
506
+ declare function fromCompatibleEf61Id(input: string | URL): URL | null;
507
+ /**
508
+ * Converts a portable ActivityPub URI into an [FEP-ef61] compatible
509
+ * identifier, which is an HTTP(S) URL under the gateway's fixed
510
+ * `/.well-known/apgateway/` path.
511
+ *
512
+ * For example, `ap+ef61://did:key:z6Mk.../objects/1` and
513
+ * `https://server.example` yield
514
+ * `https://server.example/.well-known/apgateway/did:key:z6Mk.../objects/1`.
515
+ * Both `ap:` and `ap+ef61:` URIs with decoded or percent-encoded DID
516
+ * authorities are accepted. Publishers should use the first gateway in the
517
+ * actor's `gateways` list, as FEP-ef61 requires.
518
+ *
519
+ * The path and fragment are preserved, with characters that are not allowed
520
+ * in HTTP(S) URLs percent-encoded the same way as
521
+ * {@link canonicalizePortableUri} does. FEP-ef61 location hints (`@gateway`
522
+ * query parameters, and the legacy `gateways` parameter) are removed, since
523
+ * compatible identifiers must not have them; other query parameters are kept
524
+ * in order.
525
+ *
526
+ * Arbitrary gateway paths are not supported.
527
+ *
528
+ * [FEP-ef61]: https://w3id.org/fep/ef61
529
+ *
530
+ * @param portableId The `ap:` or `ap+ef61:` URI to convert.
531
+ * @param gateway The gateway's HTTP(S) origin, e.g., `https://server.example`.
532
+ * @returns The compatible identifier.
533
+ * @throws {TypeError} If the portable ID is not a valid `ap:` or `ap+ef61:`
534
+ * URI, if its path has `.` or `..` segments (which
535
+ * HTTP(S) URLs cannot represent), or if the gateway is
536
+ * not an HTTP(S) origin with no credentials, path, query,
537
+ * or fragment.
538
+ * @since 2.4.0
539
+ */
540
+ declare function toCompatibleEf61Id(portableId: string | URL, gateway: string | URL): URL;
541
+ /**
468
542
  * Validates a URL to prevent SSRF attacks.
469
543
  */
470
544
  declare function validatePublicUrl(url: string): Promise<void>;
@@ -472,4 +546,4 @@ declare function isValidPublicIPv4Address(address: string): boolean;
472
546
  declare function isValidPublicIPv6Address(address: string): boolean;
473
547
  declare function expandIPv6Address(address: string): string;
474
548
  //#endregion
475
- export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
549
+ export { type AuthenticatedDocumentLoaderFactory, type CreateRequestOptions, type Decimal, type DidKeyVerificationMethod, type DocumentLoader, type DocumentLoaderFactory, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, FetchError, type GetDocumentLoaderOptions, type GetUserAgentOptions, type Json, LanguageString, type ParsedDigestMultibase, type ParsedHashlink, type PortableObjectVerifier, type PortableObjectVerifierOptions, type PropertyPreprocessor, type PropertyPreprocessorContext, type RemoteDocument, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
package/dist/mod.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { t as preloadedContexts } from "./contexts-DoZUIW7y.js";
3
- import { a as formatIri, c as haveSameIriOrigin, d as isValidPublicIPv6Address, f as parseGatewayUrl, h as validatePublicUrl, i as expandIPv6Address, l as isGatewayUrl, m as parseJsonLdId, n as arePortableUrisEqual, o as getFe34Origin, p as parseIri, r as canonicalizePortableUri, s as haveSameFe34Origin, t as UrlError, u as isValidPublicIPv4Address } from "./url-BHAShw9J.js";
3
+ import { _ as validatePublicUrl, a as formatIri, c as haveSameFe34Origin, d as isValidPublicIPv4Address, f as isValidPublicIPv6Address, g as toCompatibleEf61Id, h as parseJsonLdId, i as expandIPv6Address, l as haveSameIriOrigin, m as parseIri, n as arePortableUrisEqual, o as fromCompatibleEf61Id, p as parseGatewayUrl, r as canonicalizePortableUri, s as getFe34Origin, t as UrlError, u as isGatewayUrl } from "./url-CEhi4hb4.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
6
6
  import process from "node:process";
@@ -12,7 +12,7 @@ import { PublicKeyInfo } from "pkijs";
12
12
  import baseX from "@multiformats/base-x";
13
13
  //#region deno.json
14
14
  var name = "@fedify/vocab-runtime";
15
- var version = "2.4.0-dev.2030+e6b3de3d";
15
+ var version = "2.4.0-dev.2047+140b94db";
16
16
  //#endregion
17
17
  //#region src/request.ts
18
18
  /**
@@ -42,6 +42,12 @@ var FetchError = class extends Error {
42
42
  }
43
43
  };
44
44
  /**
45
+ * The `Accept` header value for fetching ActivityPub objects. ActivityPub
46
+ * and FEP-ef61 gateways require the ActivityStreams profile on the JSON-LD
47
+ * media type.
48
+ */
49
+ const ACTIVITYPUB_ACCEPT = "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
50
+ /**
45
51
  * Creates a request for the given URL.
46
52
  * @param url The URL to create the request for.
47
53
  * @param options The options for the request.
@@ -51,7 +57,7 @@ var FetchError = class extends Error {
51
57
  function createActivityPubRequest(url, options = {}) {
52
58
  return new Request(url, {
53
59
  headers: {
54
- Accept: "application/activity+json, application/ld+json",
60
+ Accept: ACTIVITYPUB_ACCEPT,
55
61
  "User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
56
62
  },
57
63
  redirect: "manual"
@@ -1358,4 +1364,4 @@ LanguageString.prototype[Symbol.for("nodejs.util.inspect.custom")] = function(_d
1358
1364
  return `<${this.locale.baseName}> ${inspect(this.toString(), options)}`;
1359
1365
  };
1360
1366
  //#endregion
1361
- export { FetchError, LanguageString, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
1367
+ export { FetchError, LanguageString, UrlError, arePortableUrisEqual, canParseDecimal, canonicalizePortableUri, computeDigestMultibase, createActivityPubRequest, createHashlink, decodeMultibase, encodeMultibase, encodingFromBaseData, expandIPv6Address, exportDidKey, exportMultibaseKey, exportSpki, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, getRemoteDocument, getUserAgent, haveSameFe34Origin, haveSameIriOrigin, importDidKey, importMultibaseKey, importPem, importPkcs1, importSpki, isDecimal, isGatewayUrl, isValidPublicIPv4Address, isValidPublicIPv6Address, logRequest, parseDecimal, parseDidKeyVerificationMethod, parseDigestMultibase, parseGatewayUrl, parseHashlink, parseIri, parseJsonLdId, preloadedContexts, toCompatibleEf61Id, validatePublicUrl, verifyDigestMultibase, verifyHashlink };
@@ -0,0 +1,52 @@
1
+ /// <reference lib="esnext.temporal" />
2
+ import { n as DocumentLoader } from "./docloader-Dr8wEkif.js";
3
+ import { TracerProvider } from "@opentelemetry/api";
4
+
5
+ //#region src/portable.d.ts
6
+ /**
7
+ * Options passed to a {@link PortableObjectVerifier}.
8
+ * @since 2.4.0
9
+ */
10
+ interface PortableObjectVerifierOptions {
11
+ /**
12
+ * The document loader for fetching remote documents, such as verification
13
+ * methods that are not `did:key` URLs.
14
+ */
15
+ readonly documentLoader?: DocumentLoader;
16
+ /**
17
+ * The document loader for fetching remote JSON-LD contexts. During
18
+ * gateway dereferencing, this loader returns the same context documents
19
+ * that the vocabulary parser sees.
20
+ */
21
+ readonly contextLoader?: DocumentLoader;
22
+ /**
23
+ * The OpenTelemetry tracer provider.
24
+ */
25
+ readonly tracerProvider?: TracerProvider;
26
+ }
27
+ /**
28
+ * A function that applies the [FEP-ef61] proof policy to a portable object
29
+ * fetched through a gateway.
30
+ *
31
+ * It receives the fetched JSON-LD document as is, and resolves to an object
32
+ * whose `verified` property tells whether the document carries a valid
33
+ * [FEP-8b32] Object Integrity Proof made by the DID in its portable ID.
34
+ * `verifyPortableObjectProof()` from `@fedify/fedify` satisfies this type, so
35
+ * it can be passed as the `verifyPortableObject` option of generated
36
+ * vocabulary accessors such as `Activity.getObject()`.
37
+ *
38
+ * [FEP-ef61]: https://w3id.org/fep/ef61
39
+ * [FEP-8b32]: https://w3id.org/fep/8b32
40
+ *
41
+ * @param document The fetched JSON-LD document.
42
+ * @param options Loaders and tracing options for verification.
43
+ * @returns An object whose `verified` property is `true` if and only if the
44
+ * document satisfies the proof policy. Other properties, such as
45
+ * a failure reason, are logged when verification fails.
46
+ * @since 2.4.0
47
+ */
48
+ type PortableObjectVerifier = (document: unknown, options: PortableObjectVerifierOptions) => Promise<{
49
+ readonly verified: boolean;
50
+ }>;
51
+ //#endregion
52
+ export { PortableObjectVerifierOptions as n, PortableObjectVerifier as t };