-
-
Notifications
You must be signed in to change notification settings - Fork 98
Normalize outgoing JSON-LD for Pixelfed interop #721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
f2ebf3d
Normalize outgoing JSON-LD for Pixelfed interop
dahlia 895f30e
Avoid needless attachment canonization
dahlia 9774a7b
Tighten attachment normalization fast path
dahlia e7aced1
Skip JSON-LD value payloads in attachment walker
dahlia 076b7bb
Clarify activity transformer configuration
dahlia 94292cc
Avoid array clones in attachment walker
dahlia dae75bc
Defer normalized proof verification fallback
dahlia a4d8901
Preserve pre-signed activity proofs
dahlia 8ddd745
Avoid canonicalizing overly deep JSON-LD
dahlia 297413c
Harden JSON-LD normalization
dahlia 5cc2fbc
Expose proof normalization opt-in
dahlia ae6f220
Tighten JSON-LD proof normalization
dahlia 359d113
Harden JSON-LD context safety checks
dahlia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,287 @@ | ||
| import { mockDocumentLoader, test } from "@fedify/fixture"; | ||
| import { Create, Document, Note, PUBLIC_COLLECTION } from "@fedify/vocab"; | ||
| import { assertEquals } from "@std/assert/assert-equals"; | ||
| import { assertStrictEquals } from "@std/assert/assert-strict-equals"; | ||
| import { | ||
| isPreloadedContextAttachmentSafe, | ||
| normalizeAttachmentArrays, | ||
| normalizeOutgoingActivityJsonLd, | ||
| } from "./outgoing-jsonld.ts"; | ||
|
|
||
| test("normalizeAttachmentArrays() wraps scalar attachments", async () => { | ||
| const input = { | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| type: "Create", | ||
| object: { | ||
| type: "Note", | ||
| attachment: { | ||
| type: "Document", | ||
| mediaType: "image/png", | ||
| url: "https://example.com/image.png", | ||
| }, | ||
| }, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input) as Record< | ||
| string, | ||
| unknown | ||
| >; | ||
| const object = output.object as Record<string, unknown>; | ||
| assertEquals(object.attachment, [ | ||
| { | ||
| type: "Document", | ||
| mediaType: "image/png", | ||
| url: "https://example.com/image.png", | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() skips canonicalization for known-safe contexts", async () => { | ||
| const input = { | ||
| "@context": [ | ||
| "https://www.w3.org/ns/activitystreams", | ||
| "https://w3id.org/security/data-integrity/v1", | ||
| ], | ||
| type: "Note", | ||
| attachment: { | ||
| type: "Document", | ||
| mediaType: "image/png", | ||
| url: "https://example.com/image.png", | ||
| }, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input, () => { | ||
| throw new Error("context loader should not be called"); | ||
| }) as Record<string, unknown>; | ||
| assertEquals(output.attachment, [ | ||
| { | ||
| type: "Document", | ||
| mediaType: "image/png", | ||
| url: "https://example.com/image.png", | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| test("isPreloadedContextAttachmentSafe() checks scoped contexts", () => { | ||
| assertEquals( | ||
| isPreloadedContextAttachmentSafe({ | ||
| "@context": { | ||
| attachment: { | ||
| "@id": "as:attachment", | ||
| "@type": "@id", | ||
| }, | ||
| Example: { | ||
| "@context": { | ||
| attachment: { | ||
| "@id": "as:attachment", | ||
| "@type": "@id", | ||
| }, | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| true, | ||
| ); | ||
| assertEquals( | ||
| isPreloadedContextAttachmentSafe({ | ||
| "@context": { | ||
| Example: { | ||
| "@context": { | ||
| attachment: "https://example.com/custom-attachment", | ||
| }, | ||
| }, | ||
| }, | ||
| }), | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() does not wrap JSON-LD list objects", async () => { | ||
| const attachment = { | ||
| "@list": [ | ||
| { type: "Document", url: "https://example.com/image.png" }, | ||
| ], | ||
| }; | ||
| const input = { | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| type: "Note", | ||
| attachment, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input, () => { | ||
| throw new Error("context loader should not be called"); | ||
| }) as Record<string, unknown>; | ||
| assertEquals(output.attachment, attachment); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() does not traverse JSON-LD value payloads", async () => { | ||
| const input = { | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| type: "Note", | ||
| attachment: { type: "Document" }, | ||
| content: { | ||
| "@type": "@json", | ||
| "@value": { | ||
| "@context": { | ||
| attachment: "https://example.com/custom-attachment", | ||
| }, | ||
| attachment: "https://example.com/metadata", | ||
| }, | ||
| }, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input, () => { | ||
| throw new Error("context loader should not be called"); | ||
| }) as Record<string, unknown>; | ||
| assertEquals(output.attachment, [{ type: "Document" }]); | ||
| assertEquals(output.content, { | ||
| "@type": "@json", | ||
| "@value": { | ||
| "@context": { | ||
| attachment: "https://example.com/custom-attachment", | ||
| }, | ||
| attachment: "https://example.com/metadata", | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() leaves attachment arrays unchanged", async () => { | ||
| const attachment = [ | ||
| { | ||
| type: "Document", | ||
| mediaType: "image/png", | ||
| url: "https://example.com/image.png", | ||
| }, | ||
| ]; | ||
| const input = { | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| type: "Note", | ||
| attachment, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input) as Record< | ||
| string, | ||
| unknown | ||
| >; | ||
| assertEquals(output.attachment, attachment); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() leaves documents without attachments unchanged", async () => { | ||
| const input = { | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| type: "Note", | ||
| content: "Hello", | ||
| }; | ||
| assertEquals(await normalizeAttachmentArrays(input), input); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() leaves @context subtrees untouched", async () => { | ||
| const input = { | ||
| "@context": [ | ||
| "https://www.w3.org/ns/activitystreams", | ||
| { attachment: "https://example.com/custom-attachment" }, | ||
| ], | ||
| type: "Note", | ||
| attachment: "https://example.com/attachment", | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input) as Record< | ||
| string, | ||
| unknown | ||
| >; | ||
| const context = output["@context"] as unknown[]; | ||
| assertEquals(context[1], { | ||
| attachment: "https://example.com/custom-attachment", | ||
| }); | ||
| assertEquals(output.attachment, ["https://example.com/attachment"]); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() bails out when wrapping changes semantics", async () => { | ||
| const input = { | ||
| "@context": { | ||
| attachment: { | ||
| "@id": "https://example.com/custom-attachment", | ||
| "@type": "@json", | ||
| }, | ||
| }, | ||
| attachment: { | ||
| custom: true, | ||
| }, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input) as Record< | ||
| string, | ||
| unknown | ||
| >; | ||
| assertEquals(output.attachment, { custom: true }); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() does not poison the global prototype via a __proto__ key", async () => { | ||
| const input = JSON.parse(`{ | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| "type": "Note", | ||
| "attachment": { "type": "Document" }, | ||
| "__proto__": { "polluted": true } | ||
| }`); | ||
| await normalizeAttachmentArrays(input); | ||
| assertEquals( | ||
| (Object.prototype as Record<string, unknown>).polluted, | ||
| undefined, | ||
| ); | ||
| }); | ||
|
|
||
| test("normalizeAttachmentArrays() stops before blowing the stack on pathological nesting", async () => { | ||
| let deep: Record<string, unknown> = { attachment: { type: "Document" } }; | ||
| for (let i = 0; i < 256; i++) deep = { object: deep }; | ||
| const input = { | ||
| "@context": "https://www.w3.org/ns/activitystreams", | ||
| type: "Create", | ||
| object: deep, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input); | ||
| assertStrictEquals(output, input); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| test("normalizeAttachmentArrays() skips canonicalization for pathological nesting", async () => { | ||
| let deep: Record<string, unknown> = { type: "Note" }; | ||
| for (let i = 0; i < 256; i++) deep = { object: deep }; | ||
| const input = { | ||
| "@context": [ | ||
| "https://www.w3.org/ns/activitystreams", | ||
| "https://example.com/context", | ||
| ], | ||
| type: "Note", | ||
| attachment: { type: "Document" }, | ||
| object: deep, | ||
| }; | ||
| const output = await normalizeAttachmentArrays(input, () => { | ||
| throw new Error("context loader should not be called"); | ||
| }) as Record<string, unknown>; | ||
| assertEquals(output.attachment, { type: "Document" }); | ||
| }); | ||
|
|
||
| test("normalizeOutgoingActivityJsonLd() applies outgoing JSON-LD workarounds", async () => { | ||
| const activity = new Create({ | ||
| id: new URL("https://example.com/activities/1"), | ||
| actor: new URL("https://example.com/alice"), | ||
| object: new Note({ | ||
| id: new URL("https://example.com/notes/1"), | ||
| tos: [PUBLIC_COLLECTION], | ||
| attachments: [ | ||
| new Document({ | ||
| mediaType: "image/png", | ||
| url: new URL("https://example.com/image.png"), | ||
| }), | ||
| ], | ||
| }), | ||
| tos: [PUBLIC_COLLECTION], | ||
| }); | ||
| const compact = await activity.toJsonLd({ format: "compact" }) as Record< | ||
| string, | ||
| unknown | ||
| >; | ||
| assertEquals(compact.to, "as:Public"); | ||
| const compactObject = compact.object as Record<string, unknown>; | ||
| assertEquals(Array.isArray(compactObject.attachment), false); | ||
|
|
||
| const normalized = await normalizeOutgoingActivityJsonLd( | ||
| compact, | ||
| mockDocumentLoader, | ||
| ) as Record<string, unknown>; | ||
| assertEquals(normalized.to, PUBLIC_COLLECTION.href); | ||
| const normalizedObject = normalized.object as Record<string, unknown>; | ||
| assertEquals(Array.isArray(normalizedObject.attachment), true); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.