@fedify/fedify
Version:
An ActivityPub server framework
810 lines (809 loc) • 28 kB
JavaScript
import "@js-temporal/polyfill";
import "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
import { t as assert } from "../assert-DikXweDx.mjs";
import { i as generateCryptoKeyPair } from "../key-BAQuZEU1.mjs";
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
import { a as compactJsonLd, f as isInvalidUrlTypeError, g as verifySignature, h as verifyJsonLd, i as attachSignature, n as UnsafeJsonLdError, o as createSignature, p as signJsonLd, s as detachSignature, u as hasSignatureLike } from "../ld-tusP_XxG.mjs";
import { mockDocumentLoader, test } from "@fedify/fixture";
import { CryptographicKey } from "@fedify/vocab";
import { encodeBase64 } from "byte-encodings/base64";
//#region src/sig/ld.test.ts
test("isInvalidUrlTypeError()", () => {
assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Invalid URL: http://[")));
assert(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("\"http://[\" cannot be parsed as a URL.")));
const error = /* @__PURE__ */ new TypeError("Failed to parse URL");
error.code = "ERR_INVALID_URL";
assert(isInvalidUrlTypeError(error));
assertFalse(isInvalidUrlTypeError(/* @__PURE__ */ new TypeError("Failed to fetch")));
});
test("attachSignature()", () => {
const sig = {
"@context": "https://w3id.org/identity/v1",
type: "RsaSignature2017",
creator: "https://activitypub.academy/users/brauca_darradiul#main-key",
created: "2024-09-12T16:50:46Z",
signatureValue: "asdf"
};
const doc = {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/1"
};
assertEquals(attachSignature(doc, sig), {
...doc,
signature: sig
});
assertThrows(() => attachSignature(null, sig), TypeError);
assertThrows(() => attachSignature(1234, sig), TypeError);
});
test("createSignature()", async () => {
const doc = {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/1",
type: "Create"
};
assertEquals(await verifySignature(attachSignature(doc, await createSignature(doc, rsaPrivateKey2, rsaPublicKey2.id, { contextLoader: mockDocumentLoader })), {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
}), rsaPublicKey2);
assertRejects(() => createSignature(doc, rsaPublicKey2.publicKey, rsaPublicKey2.id, { contextLoader: mockDocumentLoader }), TypeError);
assertRejects(() => createSignature(doc, ed25519PrivateKey, ed25519Multikey.id, { contextLoader: mockDocumentLoader }), TypeError);
});
test("signJsonLd()", async () => {
assert(await verifyJsonLd(await signJsonLd({
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/1",
type: "Create",
actor: "https://example.com/person2"
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }), {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
}));
});
test("hasSignatureLike()", () => {
assert(hasSignatureLike({ signature: {
type: "RsaSignature2017",
creator: "https://example.com/users/alice#main-key",
signatureValue: "signature"
} }));
assert(hasSignatureLike({ signature: {
type: "Ed25519Signature2020",
verificationMethod: "https://example.com/users/alice#main-key",
jws: "signature"
} }));
assert(hasSignatureLike({ signature: {
type: "Ed25519Signature2020",
verificationMethod: { id: "https://example.com/users/alice#main-key" },
jws: "signature"
} }));
assert(hasSignatureLike({ signature: {
type: "Ed25519Signature2020",
verificationMethod: [{ id: "https://example.com/users/alice#main-key" }],
jws: "signature"
} }));
assert(hasSignatureLike({ signature: {
type: "Ed25519Signature2020",
verificationMethod: { "@id": "https://example.com/users/alice#main-key" },
jws: "signature"
} }));
assert(hasSignatureLike({ signature: [{
type: "Ed25519Signature2020",
verificationMethod: { "@id": "https://example.com/users/alice#main-key" },
jws: "signature"
}] }));
assert(hasSignatureLike({ signature: {
type: ["Ed25519Signature2020"],
verificationMethod: "https://example.com/users/alice#main-key",
jws: "signature"
} }));
assertFalse(hasSignatureLike({ signature: {
type: "Ed25519Signature2020",
verificationMethod: "https://example.com/users/alice#main-key"
} }));
assertFalse(hasSignatureLike(null));
});
const document = {
"@context": ["https://www.w3.org/ns/activitystreams", {
"ostatus": "http://ostatus.org#",
"atomUri": "ostatus:atomUri",
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount"
}],
"id": "https://activitypub.academy/users/brauca_darradiul/statuses/113125611605598678/activity",
"type": "Create",
"actor": "https://activitypub.academy/users/brauca_darradiul",
"published": "2024-09-12T16:50:45Z",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://activitypub.academy/users/brauca_darradiul/followers"],
"object": {
"id": "https://activitypub.academy/users/brauca_darradiul/statuses/113125611605598678",
"type": "Note",
"summary": null,
"inReplyTo": null,
"published": "2024-09-12T16:50:45Z",
"url": "https://activitypub.academy/@brauca_darradiul/113125611605598678",
"attributedTo": "https://activitypub.academy/users/brauca_darradiul",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://activitypub.academy/users/brauca_darradiul/followers"],
"sensitive": false,
"atomUri": "https://activitypub.academy/users/brauca_darradiul/statuses/113125611605598678",
"inReplyToAtomUri": null,
"conversation": "tag:activitypub.academy,2024-09-12:objectId=187606:objectType=Conversation",
"content": "<p>Test</p>",
"contentMap": { "en": "<p>Test</p>" },
"attachment": [],
"tag": [],
"replies": {
"id": "https://activitypub.academy/users/brauca_darradiul/statuses/113125611605598678/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://activitypub.academy/users/brauca_darradiul/statuses/113125611605598678/replies?only_other_accounts=true&page=true",
"partOf": "https://activitypub.academy/users/brauca_darradiul/statuses/113125611605598678/replies",
"items": []
}
}
}
};
const signature = {
"type": "RsaSignature2017",
"creator": "https://activitypub.academy/users/brauca_darradiul#main-key",
"created": "2024-09-12T16:50:46Z",
"signatureValue": "osp9n4Pubp8XFvBi0iwrpCjDkIpuuUr2klp+r8Jp289ISqRNlUPeHVvNrQSE2vqNm4j/cJGuQruIqZPTAmTjjB3HtqgawoAG11DA7OPpY6mJLruKnbqadV1cy5V0DJI9CRJXEBuEmMTJRO9gi1cyzlM4QxK30YrjmtQNLoU9th97da4lumsl+a5cAue38MDuJZvLWDOTZ1EGixwhLP8FevdnZ+jqwctGu9KrgDImBIpBkQaqHFTTGrbE7FlXsj1pneOUQTuRDa9zlk2DmgXeEBWN2OJZDjgJ4iBsF2JHtCn6PccKbuI9s2VLhnobPtLB8YdHYKqIPLmv0UOjAM8XrQ=="
};
const testVector = {
...document,
signature
};
test("detachSignature()", () => {
assertEquals(detachSignature(testVector), document);
assertEquals(detachSignature(document), document);
});
test("verifySignature()", async () => {
const doc = { ...testVector };
const key = await verifySignature(doc, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
});
assertEquals(doc, testVector);
assertEquals(key, await CryptographicKey.fromJsonLd({
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
id: "https://activitypub.academy/users/brauca_darradiul#main-key",
owner: "https://activitypub.academy/users/brauca_darradiul",
publicKeyPem: "-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5W/9rYXddIjKo9Ury/LK XqQYbj0cOx/c+T1uRHJzced8JbvXdiBCNZXVVrIaygy3G/MOvxMW4kbA1bqeiSYY V9TXBMI6gVVDnl5VG64uGxswcvUWqQU5Q1mwuwyGCPhexAq3BKe/7uH64AZgx11e KLl3W3WcIMKmunYn8+z6hm0003hMensXMNpMVfqLoXaeuro7pYnwOSWoHFS3AxWK llMwAoa5waulgai8gD7/uA5Y9Hvguk/OBYBh9YnIX5N5jScsmY/EYuesNIH2Ct9s E3aVkTjZUt55JtXnk8Q9eTnrcB/98RtLWH4pJTKJhzxv19i3aZT3yDApPk0Q/biI JQIDAQAB -----END PUBLIC KEY----- "
}));
const doc2 = {
...testVector,
signature: {
...testVector.signature,
signatureValue: "!"
}
};
const key2 = await verifySignature(doc2, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
});
assertEquals(doc2, {
...testVector,
signature: {
...testVector.signature,
signatureValue: "!"
}
});
assertEquals(key2, null);
const incorrectSig = encodeBase64(new Uint8Array([
1,
2,
3,
4
]));
const doc3 = {
...testVector,
signature: {
...testVector.signature,
signatureValue: incorrectSig
}
};
const key3 = await verifySignature(doc3, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
});
assertEquals(doc3, {
...testVector,
signature: {
...testVector.signature,
signatureValue: incorrectSig
}
});
assertEquals(key3, null);
const doc4 = { ...testVector };
const key4 = await verifySignature(doc4, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader,
keyCache: {
async get(keyId) {
return new CryptographicKey({
id: keyId,
owner: new URL("https://activitypub.academy/users/brauca_darradiul"),
publicKey: (await generateCryptoKeyPair("RSASSA-PKCS1-v1_5")).publicKey
});
},
set(_keyId, _key) {
return Promise.resolve();
}
}
});
assertEquals(doc4, testVector);
assertEquals(key4, key);
});
test("verifyJsonLd()", async () => {
assert(await verifyJsonLd(testVector, {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
}));
assertFalse(await verifyJsonLd(await signJsonLd({
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/1",
type: "Create",
actor: "https://example.com/person2"
}, rsaPrivateKey2, rsaPublicKey2.id, { contextLoader: mockDocumentLoader }), {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
}));
});
test("compactJsonLd() with restrictive context loader", async () => {
const restrictiveContextLoader = async (resource) => {
const url = new URL(resource).href;
if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
throw new Error(`Unexpected context: ${url}`);
};
const signed = await signJsonLd({
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/identity/v1",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/1",
type: "Create",
actor: "https://example.com/person2"
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
assertEquals(await compactJsonLd(signed, restrictiveContextLoader), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/1",
type: "Create",
actor: "https://example.com/person2",
signature: signed.signature
});
});
test("compactJsonLd() caches repeated remote contexts across graph scan and compaction", async () => {
const remoteUrl = "https://example.com/context";
let calls = 0;
const countingLoader = async (resource) => {
const url = new URL(resource).href;
if (url === remoteUrl) {
calls++;
return {
contextUrl: null,
documentUrl: url,
document: { "@context": { extra: "https://example.com/extra" } }
};
}
return await mockDocumentLoader(url);
};
await compactJsonLd({
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/activities/remote-contexts",
type: "Create",
actor: "https://example.com/person2",
object: [
{
"@context": ["https://www.w3.org/ns/activitystreams", remoteUrl],
type: "Note",
content: "one"
},
{
"@context": ["https://www.w3.org/ns/activitystreams", remoteUrl],
type: "Note",
content: "two"
},
{
"@context": ["https://www.w3.org/ns/activitystreams", remoteUrl],
type: "Note",
content: "three"
}
]
}, countingLoader);
assertEquals(calls, 1);
});
test("compactJsonLd() reuses the same remote context response for graph scan and compaction", async () => {
const remoteUrl = "https://example.com/context";
let calls = 0;
const compacted = await compactJsonLd({
"@context": [remoteUrl, "https://www.w3.org/ns/activitystreams"],
id: "https://example.com/activities/memoized-remote-context",
type: "Create",
actor: "https://example.com/person2",
graph: "https://example.com/custom-graph",
object: "https://example.com/notes/1"
}, async (resource) => {
const url = new URL(resource).href;
if (url === remoteUrl) {
calls++;
if (calls > 1) throw new Error(`Remote context should not be fetched twice: ${url}`);
return {
contextUrl: null,
documentUrl: url,
document: { "@context": { graph: "https://example.com/graph" } }
};
}
return await mockDocumentLoader(url);
});
assertEquals(calls, 1);
assertEquals(compacted, {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/memoized-remote-context",
type: "Create",
actor: "https://example.com/person2",
"https://example.com/graph": "https://example.com/custom-graph",
object: "https://example.com/notes/1"
});
});
test("compactJsonLd() preserves opaque top-level ids and resolves relative remote contexts against documentUrl during graph scan", async () => {
const rootContextId = "opaque-root";
const rootContextUrl = "https://example.com/contexts/root";
const childContextUrl = "https://example.com/contexts/child";
const calls = [];
const customLoader = async (resource) => {
calls.push(resource);
if (resource === rootContextId) return {
contextUrl: null,
documentUrl: rootContextUrl,
document: { "@context": {
"@import": "./child",
ext: "https://example.com/ext"
} }
};
if (resource === childContextUrl || resource === "child") return {
contextUrl: null,
documentUrl: childContextUrl,
document: { "@context": { child: "https://example.com/child" } }
};
return await mockDocumentLoader(resource);
};
assertEquals(await compactJsonLd({
"@context": [rootContextId, "https://www.w3.org/ns/activitystreams"],
id: "https://example.com/activities/custom-loader-contexts",
type: "Create",
actor: "https://example.com/person2",
ext: "preserve-me",
object: {
type: "Note",
content: "Hello"
}
}, customLoader), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/custom-loader-contexts",
type: "Create",
actor: "https://example.com/person2",
"https://example.com/ext": "preserve-me",
object: {
type: "Note",
content: "Hello"
}
});
assert(calls.includes(rootContextId));
assert(calls.includes(childContextUrl));
assertFalse(calls.includes("./child"));
});
test("compactJsonLd() preserves base URLs for property-scoped remote contexts", async () => {
const rootContextId = "opaque-root";
const rootContextUrl = "https://example.com/contexts/root";
const childContextUrl = "https://example.com/contexts/child";
const calls = [];
const customLoader = async (resource) => {
calls.push(resource);
if (resource === rootContextId) return {
contextUrl: null,
documentUrl: rootContextUrl,
document: { "@context": { p: {
"@id": "https://example.com/p",
"@context": "./child"
} } }
};
if (resource === childContextUrl) return {
contextUrl: null,
documentUrl: childContextUrl,
document: { "@context": { nested: "https://example.com/nested" } }
};
return await mockDocumentLoader(resource);
};
assertEquals(await compactJsonLd({
"@context": [rootContextId, "https://www.w3.org/ns/activitystreams"],
id: "https://example.com/activities/property-scoped-contexts",
type: "Create",
actor: "https://example.com/person2",
p: { nested: "value" },
object: "https://example.com/notes/1"
}, customLoader), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/property-scoped-contexts",
type: "Create",
actor: "https://example.com/person2",
"https://example.com/p": { "https://example.com/nested": "value" },
object: "https://example.com/notes/1"
});
assert(calls.includes(rootContextId));
assert(calls.includes(childContextUrl));
assertFalse(calls.includes("./child"));
});
test("compactJsonLd() ignores unsafe-looking keys inside @json values", async () => {
const remoteContextUrl = "https://example.com/contexts/json";
assertEquals(await compactJsonLd({
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
id: "https://example.com/activities/json-blob",
type: "Create",
actor: "https://example.com/person2",
blob: {
graph: { nested: true },
"@reverse": { nope: true },
"@included": [{ still: "raw-json" }]
},
object: "https://example.com/notes/1"
}, async (resource) => {
const url = new URL(resource).href;
if (url === remoteContextUrl) return {
contextUrl: null,
documentUrl: url,
document: { "@context": {
blob: {
"@id": "https://example.com/blob",
"@type": "@json"
},
graph: "@graph"
} }
};
return await mockDocumentLoader(url);
}), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/json-blob",
type: "Create",
"https://example.com/blob": {
type: "@json",
"@value": {
graph: { nested: true },
"@reverse": { nope: true },
"@included": [{ still: "raw-json" }]
}
},
actor: "https://example.com/person2",
object: "https://example.com/notes/1"
});
});
test("compactJsonLd() ignores unsafe-looking keys inside inline @json value wrappers", async () => {
const remoteContextUrl = "https://example.com/contexts/inline-json";
assertEquals(await compactJsonLd({
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
id: "https://example.com/activities/inline-json-blob",
type: "Create",
actor: "https://example.com/person2",
blob: {
"@value": {
graph: { nested: true },
"@reverse": { nope: true },
"@included": [{ still: "raw-json" }]
},
"@type": "@json"
},
object: "https://example.com/notes/1"
}, async (resource) => {
const url = new URL(resource).href;
if (url === remoteContextUrl) return {
contextUrl: null,
documentUrl: url,
document: { "@context": {
blob: "https://example.com/blob",
graph: "@graph"
} }
};
return await mockDocumentLoader(url);
}), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/inline-json-blob",
type: "Create",
"https://example.com/blob": {
type: "@json",
"@value": {
graph: { nested: true },
"@reverse": { nope: true },
"@included": [{ still: "raw-json" }]
}
},
actor: "https://example.com/person2",
object: "https://example.com/notes/1"
});
});
test("verifyJsonLd() respects @graph alias overrides", async () => {
assert(await verifyJsonLd(await signJsonLd({
"@context": [
"https://www.w3.org/ns/activitystreams",
{ graph: "@graph" },
{ graph: "https://example.com/graph" }
],
id: "https://example.com/activities/1",
type: "Create",
actor: "https://example.com/person2",
object: "https://example.com/notes/1",
graph: "https://example.com/custom-graph"
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }), {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
}));
});
test("compactJsonLd() respects nested @context scope for @graph aliases", async () => {
assertEquals(await compactJsonLd({
"@context": ["https://www.w3.org/ns/activitystreams", {
graph: "https://example.com/graph",
meta: {
"@id": "https://example.com/meta",
"@context": { graph: "@graph" }
}
}],
id: "https://example.com/activities/2",
type: "Create",
actor: "https://example.com/person2",
object: "https://example.com/notes/2",
graph: "https://example.com/custom-graph",
meta: { value: "ok" }
}, mockDocumentLoader), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/2",
type: "Create",
"https://example.com/graph": "https://example.com/custom-graph",
actor: "https://example.com/person2",
object: "https://example.com/notes/2",
"https://example.com/meta": { value: "ok" }
});
});
test("compactJsonLd() resets inherited @graph aliases on @context: null", async () => {
assertEquals(await compactJsonLd({
"@context": ["https://www.w3.org/ns/activitystreams", { g: "@graph" }],
id: "https://example.com/activities/3",
type: "Create",
actor: "https://example.com/person2",
object: {
"@context": null,
g: "literal"
}
}, mockDocumentLoader), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/3",
type: "Create",
actor: "https://example.com/person2",
object: {}
});
});
test("compactJsonLd() rejects same-object forward @graph alias chains", async () => {
await assertRejects(() => compactJsonLd({
"@context": ["https://www.w3.org/ns/activitystreams", {
a: "b",
b: "@graph"
}],
id: "https://example.com/activities/forward-graph-alias",
type: "Create",
actor: "https://example.com/person2",
a: [{
id: "https://example.com/notes/forward-graph-alias",
type: "Note",
content: "Hello"
}]
}, mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
});
test("compactJsonLd() preserves captured @graph aliases across later overrides", async () => {
await assertRejects(() => compactJsonLd({
"@context": [
"https://www.w3.org/ns/activitystreams",
{ b: "@graph" },
{ a: "b" },
{ b: "https://example.com/b" }
],
id: "https://example.com/activities/captured-graph-alias",
type: "Create",
actor: "https://example.com/person2",
a: [{
id: "https://example.com/notes/captured-graph-alias",
type: "Note",
content: "Hello"
}]
}, mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
});
test("compactJsonLd() does not retroactively apply later @graph aliases", async () => {
assertEquals(await compactJsonLd({
"@context": [
"https://www.w3.org/ns/activitystreams",
{ a: "b" },
{ b: "@graph" }
],
id: "https://example.com/activities/non-retroactive-graph-alias",
type: "Create",
actor: "https://example.com/person2",
a: [{
id: "https://example.com/notes/non-retroactive-graph-alias",
type: "Note",
content: "Hello"
}]
}, mockDocumentLoader), {
"@context": [
"https://w3id.org/identity/v1",
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/data-integrity/v1"
],
id: "https://example.com/activities/non-retroactive-graph-alias",
type: "Create",
b: {
id: "https://example.com/notes/non-retroactive-graph-alias",
type: "Note",
content: "Hello"
},
actor: "https://example.com/person2"
});
});
test("verifyJsonLd() rejects unsafe JSON-LD keywords", async () => {
const original = {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/activities/undo",
type: "Undo",
actor: "https://example.com/person2",
object: {
id: "https://example.com/activities/announce",
type: "Announce",
actor: "https://example.com/person2",
object: "https://example.com/status/1"
}
};
const signed = await signJsonLd(original, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
const options = {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
};
const cases = [
["@reverse", {
"@context": ["https://www.w3.org/ns/activitystreams", { rev: "@reverse" }],
id: "https://example.com/activities/announce",
type: "Announce",
actor: "https://example.com/person2",
object: "https://example.com/status/1",
rev: { object: {
id: "https://example.com/activities/undo",
type: "Undo",
actor: "https://example.com/person2"
} },
signature: signed.signature
}],
["@included", {
"@context": ["https://www.w3.org/ns/activitystreams", { inc: "@included" }],
id: "https://example.com/activities/announce",
type: "Announce",
actor: "https://example.com/person2",
object: "https://example.com/status/1",
inc: [{
id: "https://example.com/activities/undo",
type: "Undo",
actor: "https://example.com/person2",
object: "https://example.com/activities/announce"
}],
signature: signed.signature
}],
["@graph", {
"@context": ["https://www.w3.org/ns/activitystreams", { graph: "@graph" }],
graph: [original],
signature: signed.signature
}],
["@graph", {
"@context": ["https://www.w3.org/ns/activitystreams", { graph: "@graph" }],
id: "https://example.com/activities/announce",
type: "Announce",
actor: "https://example.com/person2",
object: "https://example.com/status/1",
graph: [{
id: "https://example.com/activities/undo",
type: "Undo",
actor: "https://example.com/person2",
object: "https://example.com/activities/announce"
}],
signature: signed.signature
}]
];
for (const [keyword, jsonLd] of cases) await assertRejects(() => verifyJsonLd(jsonLd, options), UnsafeJsonLdError, `Unsupported JSON-LD keyword: ${keyword}.`);
});
test("compactJsonLd() rejects unsafe JSON-LD keywords inside signature objects", async () => {
const signed = await signJsonLd({
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/activities/signed-signature-keywords",
type: "Create",
actor: "https://example.com/person2",
object: "https://example.com/notes/1"
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
for (const [keyword, value] of [
["@reverse", { object: {
id: "https://example.com/activities/reverse-inside-signature",
type: "Undo"
} }],
["@included", [{
id: "https://example.com/activities/included-inside-signature",
type: "Undo"
}]],
["@graph", [{
id: "https://example.com/activities/graph-inside-signature",
type: "Undo"
}]]
]) await assertRejects(() => compactJsonLd({
...signed,
signature: {
...signed.signature,
[keyword]: value
}
}, mockDocumentLoader), UnsafeJsonLdError, `Unsupported JSON-LD keyword: ${keyword}.`);
});
test("compactJsonLd() rejects inputs that compact into @graph wrappers", async () => {
await assertRejects(() => compactJsonLd([{
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/notes/graph-wrapper-1",
type: "Note",
content: "one"
}, {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://example.com/notes/graph-wrapper-2",
type: "Note",
content: "two"
}], mockDocumentLoader), UnsafeJsonLdError, "Unsupported JSON-LD keyword: @graph.");
});
//#endregion
export {};