UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

204 lines (203 loc) • 9.05 kB
import "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { n as preloadedOnlyDocumentLoader, t as normalizePublicAudience } from "./public-audience-DYFHzm_c.mjs"; import { preloadedContexts } from "@fedify/vocab-runtime"; import { getLogger } from "@logtape/logtape"; import jsonld from "@fedify/vocab-runtime/jsonld"; //#region src/compat/outgoing-jsonld.ts const logger = getLogger([ "fedify", "compat", "outgoing-jsonld" ]); const ATTACHMENT_FIELDS = new Set(["attachment", "https://www.w3.org/ns/activitystreams#attachment"]); const AS_CONTEXT_URL = "https://www.w3.org/ns/activitystreams"; const KNOWN_SAFE_CONTEXT_URLS = getKnownSafeContextUrls(); const MAX_TRAVERSAL_DEPTH = 64; function isJsonLdListObject(value) { return typeof value === "object" && value != null && Object.hasOwn(value, "@list"); } function isJsonLdValueObject(value) { return typeof value === "object" && value != null && Object.hasOwn(value, "@value"); } function* getContextObjects(value, seen = /* @__PURE__ */ new WeakSet()) { if (Array.isArray(value)) { if (seen.has(value)) return; seen.add(value); for (const item of value) yield* getContextObjects(item, seen); return; } if (typeof value === "object" && value != null) { if (seen.has(value)) return; seen.add(value); const record = value; yield record; for (const definition of Object.values(record)) { if (typeof definition !== "object" || definition == null) continue; const nestedContext = definition["@context"]; if (nestedContext == null) continue; yield* getContextObjects(nestedContext, seen); } } } function isActivityStreamsAttachmentTerm(value) { return typeof value === "object" && value != null && value["@id"] === "as:attachment" && value["@type"] === "@id"; } /** @internal */ function isPreloadedContextAttachmentSafe(document) { if (typeof document !== "object" || document == null) return true; const context = document["@context"]; for (const contextObject of getContextObjects(context)) { if (!Object.hasOwn(contextObject, "attachment")) continue; if (isActivityStreamsAttachmentTerm(contextObject.attachment)) continue; return false; } return true; } function getKnownSafeContextUrls() { const urls = /* @__PURE__ */ new Set(); for (const [url, document] of Object.entries(preloadedContexts)) if (isPreloadedContextAttachmentSafe(document)) urls.add(url); else logger.warn("Preloaded JSON-LD context {contextUrl} redefines the `attachment` term incompatibly; attachment array normalization will require canonicalization for documents using it.", { contextUrl: url }); return urls; } /** * Wraps scalar ActivityStreams attachment properties in arrays. */ function wrapScalarAttachments(jsonLd, depth = 0) { if (depth >= MAX_TRAVERSAL_DEPTH) return jsonLd; if (Array.isArray(jsonLd)) { let normalized = null; for (let i = 0; i < jsonLd.length; i++) { const item = jsonLd[i]; const next = wrapScalarAttachments(item, depth + 1); if (normalized == null && next !== item) normalized = jsonLd.slice(0, i); if (normalized != null) normalized[i] = next; } return normalized ?? jsonLd; } if (typeof jsonLd !== "object" || jsonLd == null) return jsonLd; const record = jsonLd; const keys = Object.keys(record); let normalized = null; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const value = record[key]; const next = key === "@context" || key === "@value" && isJsonLdValueObject(jsonLd) ? value : wrapScalarAttachments(value, depth + 1); const output = ATTACHMENT_FIELDS.has(key) && next != null && !Array.isArray(next) && !isJsonLdListObject(next) ? [next] : next; if (normalized == null && output !== value) { const cloned = Object.create(null); for (let j = 0; j < i; j++) { const previousKey = keys[j]; cloned[previousKey] = record[previousKey]; } normalized = cloned; } if (normalized != null) normalized[key] = output; } return normalized ?? jsonLd; } function hasNestedContext(value, depth = 0) { if (depth >= MAX_TRAVERSAL_DEPTH) return true; if (Array.isArray(value)) return value.some((item) => hasNestedContext(item, depth + 1)); if (typeof value !== "object" || value == null) return false; const record = value; for (const key of Object.keys(record)) { if (key === "@context") return true; if (key === "@value" && isJsonLdValueObject(value)) continue; if (hasNestedContext(record[key], depth + 1)) return true; } return false; } function exceedsTraversalDepth(value, depth = 0) { if (depth >= MAX_TRAVERSAL_DEPTH) return true; if (Array.isArray(value)) return value.some((item) => exceedsTraversalDepth(item, depth + 1)); if (typeof value !== "object" || value == null) return false; const record = value; for (const key of Object.keys(record)) { if (key === "@context" || key === "@value" && isJsonLdValueObject(value)) continue; if (exceedsTraversalDepth(record[key], depth + 1)) return true; } return false; } function hasKnownSafeContext(jsonLd) { if (typeof jsonLd !== "object" || jsonLd == null) return false; const record = jsonLd; if (!Object.hasOwn(record, "@context")) return false; const context = record["@context"]; const entries = typeof context === "string" ? [context] : Array.isArray(context) ? context : null; if (entries == null || entries.length < 1) return false; let hasActivityStreamsContext = false; for (const entry of entries) { if (typeof entry !== "string") return false; if (!KNOWN_SAFE_CONTEXT_URLS.has(entry)) return false; if (entry === AS_CONTEXT_URL) hasActivityStreamsContext = true; } if (!hasActivityStreamsContext) return false; for (const key of Object.keys(record)) { if (key === "@context") continue; if (hasNestedContext(record[key])) return false; } return true; } function getLogSafeJsonLdMetadata(jsonLd) { if (typeof jsonLd !== "object" || jsonLd == null) return {}; const record = jsonLd; const context = record["@context"]; return { id: typeof record.id === "string" ? record.id : typeof record["@id"] === "string" ? record["@id"] : void 0, type: typeof record.type === "string" ? record.type : typeof record["@type"] === "string" ? record["@type"] : void 0, context: typeof context === "string" ? context : Array.isArray(context) ? context.filter((entry) => typeof entry === "string").slice(0, 4) : context == null ? void 0 : "[inline context]" }; } /** * Ensures ActivityStreams attachment properties are represented as arrays * when doing so preserves the JSON-LD semantics. * * JSON-LD compaction collapses single-item arrays into scalar values by * default. Some ActivityPub implementations, Pixelfed among them, parse * `attachment` as a plain JSON array rather than a JSON-LD property and reject * otherwise valid objects whose single attachment is emitted as a scalar. * * When no `contextLoader` is supplied, the helper falls back to a restricted * loader that resolves only Fedify's preloaded JSON-LD contexts and rejects * every other URL without network access. Documents with custom, inline, or * otherwise uncached contexts should pass a real `contextLoader` if they need * the semantic-preservation check to succeed; otherwise canonicalization * failures leave the original document unchanged. */ async function normalizeAttachmentArrays(jsonLd, contextLoader) { const normalized = wrapScalarAttachments(jsonLd); if (normalized === jsonLd) return jsonLd; if (exceedsTraversalDepth(jsonLd)) { logger.debug("Skipping attachment array normalization because the JSON-LD document exceeds the safe traversal depth; leaving it unchanged."); return jsonLd; } if (hasKnownSafeContext(jsonLd)) return normalized; const loader = contextLoader ?? preloadedOnlyDocumentLoader; try { const [before, after] = await Promise.all([jsonld.canonize(jsonLd, { format: "application/n-quads", documentLoader: loader }), jsonld.canonize(normalized, { format: "application/n-quads", documentLoader: loader })]); if (before === after) return normalized; logger.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)); } catch (error) { logger.debug("Failed to verify attachment array normalization equivalence via JSON-LD canonicalization; leaving the JSON-LD document unchanged.\n{error}", { error }); } return jsonLd; } /** * Applies Fedify's internal JSON-LD wire-format interoperability workarounds * to locally generated outgoing activities before they are signed, enqueued, * or sent. */ async function normalizeOutgoingActivityJsonLd(jsonLd, contextLoader) { jsonLd = await normalizePublicAudience(jsonLd, contextLoader); return await normalizeAttachmentArrays(jsonLd, contextLoader); } //#endregion export { normalizeAttachmentArrays as n, normalizeOutgoingActivityJsonLd as r, isPreloadedContextAttachmentSafe as t };