@fedify/vocab 2.5.0-dev.2504 → 2.5.0-dev.2516
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/deno.json +1 -1
- package/dist/mod.cjs +13426 -2215
- package/dist/mod.d.cts +9001 -782
- package/dist/mod.d.ts +9001 -782
- package/dist/mod.js +13427 -2216
- package/dist-tests/{actor-BRLxkkip.mjs → actor-HAORUnb8.mjs} +2 -2
- package/dist-tests/actor.test.mjs +2 -2
- package/dist-tests/deep-clone.test.mjs +292 -0
- package/dist-tests/gateway.test.mjs +71 -4
- package/dist-tests/license.test.mjs +2 -2
- package/dist-tests/{lookup-C20BmzHy.mjs → lookup-B9VbSKaj.mjs} +2 -2
- package/dist-tests/lookup.test.mjs +2 -2
- package/dist-tests/portable-lookup.test.mjs +3 -3
- package/dist-tests/signed-representation.test.mjs +15 -1
- package/dist-tests/sparse-arrays.test.mjs +459 -0
- package/dist-tests/{src-CB6Qo4bK.mjs → src-sHf6TARh.mjs} +3 -3
- package/dist-tests/translation.test.mjs +2 -2
- package/dist-tests/type.test.mjs +1 -1
- package/dist-tests/{vocab-BmIsd6gR.mjs → vocab-CCAqTuHd.mjs} +13427 -2216
- package/dist-tests/vocab.test.mjs +2 -2
- package/package.json +4 -4
- package/src/deep-clone.test.ts +347 -0
- package/src/gateway.test.ts +81 -5
- package/src/signed-representation.test.ts +21 -0
- package/src/sparse-arrays.test.ts +507 -0
- package/src/vocab.bench.ts +63 -1
|
@@ -2,7 +2,7 @@ import { Temporal } from "temporal-polyfill";
|
|
|
2
2
|
globalThis.addEventListener = () => {};
|
|
3
3
|
import { i as __toESM } from "./rolldown-runtime-BWJg4WE8.mjs";
|
|
4
4
|
import { t as esm_default } from "./esm-BIqRhjGA.mjs";
|
|
5
|
-
import { A as
|
|
5
|
+
import { A as Note, B as Question, C as Image, D as Link, E as InteractionRule, F as Organization, G as Source, H as QuoteRequest, I as Person, K as Tombstone, L as Place, M as Offer, O as Measure, P as OrderedCollectionPage, Q as vocab_exports, R as PropertyValue, S as Hashtag, T as InteractionPolicy, U as Reject, V as QuoteAuthorization, W as Service, _ as FeatureRequest, a as Application, b as Follow, d as CryptographicKey, g as FeatureAuthorization, h as Endpoints, i as Announce, j as Object$1, k as Multikey, l as Commitment, m as Document, n as Activity, p as Delete, r as Agreement, s as Collection, t as Accept, u as Create, v as FeaturedCollection, w as Intent, x as Group, y as FeaturedItem, z as Proposal } from "./vocab-CCAqTuHd.mjs";
|
|
6
6
|
import { t as assertInstanceOf } from "./utils-Df2odC27.mjs";
|
|
7
7
|
import { mockDocumentLoader, test } from "@fedify/fixture";
|
|
8
8
|
import { deepStrictEqual, notDeepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
|
|
@@ -17,7 +17,7 @@ import { areAllScalarTypes, loadSchemaFiles } from "@fedify/vocab-tools";
|
|
|
17
17
|
import { inspect } from "node:util";
|
|
18
18
|
//#region ../vocab-runtime/deno.json
|
|
19
19
|
var name = "@fedify/vocab-runtime";
|
|
20
|
-
var version = "2.5.0-dev.
|
|
20
|
+
var version = "2.5.0-dev.2516+42d3ed0b";
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region ../vocab-runtime/src/url.ts
|
|
23
23
|
var UrlError = class extends Error {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fedify/vocab",
|
|
3
|
-
"version": "2.5.0-dev.
|
|
3
|
+
"version": "2.5.0-dev.2516+42d3ed0b",
|
|
4
4
|
"homepage": "https://fedify.dev/",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"jsonld": "^9.0.0",
|
|
44
44
|
"pkijs": "^3.3.3",
|
|
45
45
|
"temporal-polyfill": "^1.0.1",
|
|
46
|
-
"@fedify/
|
|
47
|
-
"@fedify/vocab-
|
|
48
|
-
"@fedify/
|
|
46
|
+
"@fedify/webfinger": "2.5.0-dev.2516+42d3ed0b",
|
|
47
|
+
"@fedify/vocab-tools": "2.5.0-dev.2516+42d3ed0b",
|
|
48
|
+
"@fedify/vocab-runtime": "2.5.0-dev.2516+42d3ed0b"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@dprint/formatter": "0.6.0",
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTestTracerProvider,
|
|
3
|
+
mockDocumentLoader,
|
|
4
|
+
test,
|
|
5
|
+
} from "@fedify/fixture";
|
|
6
|
+
import { type DocumentLoader, LanguageString } from "@fedify/vocab-runtime";
|
|
7
|
+
import {
|
|
8
|
+
deepStrictEqual,
|
|
9
|
+
notStrictEqual,
|
|
10
|
+
ok,
|
|
11
|
+
strictEqual,
|
|
12
|
+
throws,
|
|
13
|
+
} from "node:assert/strict";
|
|
14
|
+
import { Buffer } from "node:buffer";
|
|
15
|
+
import {
|
|
16
|
+
Accept,
|
|
17
|
+
Create,
|
|
18
|
+
DataIntegrityProof,
|
|
19
|
+
Endpoints,
|
|
20
|
+
Link,
|
|
21
|
+
Note,
|
|
22
|
+
Object as VocabularyObject,
|
|
23
|
+
Person,
|
|
24
|
+
QuoteRequest,
|
|
25
|
+
Reject,
|
|
26
|
+
Source,
|
|
27
|
+
} from "./vocab.ts";
|
|
28
|
+
|
|
29
|
+
const options = { contextLoader: mockDocumentLoader };
|
|
30
|
+
|
|
31
|
+
function personLoader(name: string, fetched: string[] = []): DocumentLoader {
|
|
32
|
+
// deno-lint-ignore require-await
|
|
33
|
+
return async (url) => {
|
|
34
|
+
fetched.push(url);
|
|
35
|
+
return {
|
|
36
|
+
contextUrl: null,
|
|
37
|
+
documentUrl: url,
|
|
38
|
+
document: {
|
|
39
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
40
|
+
id: url,
|
|
41
|
+
type: "Person",
|
|
42
|
+
name,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const hydrateOriginal of [false, true]) {
|
|
49
|
+
test(`deepClone() isolates embedded quote hydration (${hydrateOriginal})`, async () => {
|
|
50
|
+
const original = await QuoteRequest.fromJsonLd({
|
|
51
|
+
"@context": ["https://www.w3.org/ns/activitystreams", {
|
|
52
|
+
QuoteRequest: "https://w3id.org/fep/044f#QuoteRequest",
|
|
53
|
+
}],
|
|
54
|
+
id: "https://a.example/requests/1",
|
|
55
|
+
type: "QuoteRequest",
|
|
56
|
+
instrument: {
|
|
57
|
+
id: "https://a.example/notes/1",
|
|
58
|
+
type: "Note",
|
|
59
|
+
attributedTo: "https://a.example/alice",
|
|
60
|
+
content: "Quoted",
|
|
61
|
+
},
|
|
62
|
+
}, { ...options, documentLoader: personLoader("Alice") });
|
|
63
|
+
const copy = original.deepClone();
|
|
64
|
+
const sourceNote = await original.getInstrument();
|
|
65
|
+
const copyNote = await copy.getInstrument();
|
|
66
|
+
ok(sourceNote instanceof Note && copyNote instanceof Note);
|
|
67
|
+
notStrictEqual(copyNote, sourceNote);
|
|
68
|
+
const hydrated = hydrateOriginal ? sourceNote : copyNote;
|
|
69
|
+
strictEqual((await hydrated.getAttribution())?.name, "Alice");
|
|
70
|
+
for (const Wrapper of [Accept, Reject]) {
|
|
71
|
+
const json = await new Wrapper({
|
|
72
|
+
object: hydrateOriginal ? copy : original,
|
|
73
|
+
}).toJsonLd(options) as {
|
|
74
|
+
object: { instrument: { attributedTo: unknown } };
|
|
75
|
+
};
|
|
76
|
+
strictEqual(
|
|
77
|
+
json.object.instrument.attributedTo,
|
|
78
|
+
"https://a.example/alice",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
strictEqual(await original.getInstrument(), sourceNote);
|
|
82
|
+
strictEqual(await copy.getInstrument(), copyNote);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("deepClone() copies replacements and preserves graph aliases", async () => {
|
|
87
|
+
const id = new URL("https://example.com/notes/1");
|
|
88
|
+
const child = new Note({ id, url: id, content: "Child" });
|
|
89
|
+
const source = new Create({ object: child, attachments: [child] });
|
|
90
|
+
const copy = source.deepClone({ objects: [child, child] });
|
|
91
|
+
const objects = await Array.fromAsync(copy.getObjects());
|
|
92
|
+
const attachment = (await Array.fromAsync(copy.getAttachments()))[0];
|
|
93
|
+
strictEqual(objects[0], objects[1]);
|
|
94
|
+
strictEqual(objects[0], attachment);
|
|
95
|
+
notStrictEqual(objects[0], child);
|
|
96
|
+
strictEqual(objects[0].id, objects[0].url);
|
|
97
|
+
notStrictEqual(objects[0].id, id);
|
|
98
|
+
objects[0].id!.pathname = "/changed";
|
|
99
|
+
strictEqual(id.pathname, "/notes/1");
|
|
100
|
+
strictEqual(await source.getObject(), child);
|
|
101
|
+
|
|
102
|
+
const backReference = source.deepClone({ object: source });
|
|
103
|
+
const snapshot = await backReference.getObject();
|
|
104
|
+
ok(snapshot instanceof Create);
|
|
105
|
+
notStrictEqual(snapshot, source);
|
|
106
|
+
notStrictEqual(snapshot, backReference);
|
|
107
|
+
ok(await snapshot.getObject() instanceof Note);
|
|
108
|
+
await backReference.toJsonLd(options);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("deepClone() copies non-entity vocabulary objects and standalone roots", () => {
|
|
112
|
+
const endpoint = new URL("https://example.com/inbox");
|
|
113
|
+
const endpoints = new Endpoints({ sharedInbox: endpoint });
|
|
114
|
+
const source = new Source({ content: "Markdown" });
|
|
115
|
+
const person = new Person({ endpoints, source });
|
|
116
|
+
const copy = person.deepClone();
|
|
117
|
+
notStrictEqual(copy.endpoints, endpoints);
|
|
118
|
+
notStrictEqual(copy.source, source);
|
|
119
|
+
notStrictEqual(copy.endpoints!.sharedInbox, endpoint);
|
|
120
|
+
copy.endpoints!.sharedInbox!.pathname = "/copied";
|
|
121
|
+
strictEqual(endpoint.pathname, "/inbox");
|
|
122
|
+
const link = new Link({ href: endpoint }).deepClone();
|
|
123
|
+
notStrictEqual(link.href, endpoint);
|
|
124
|
+
strictEqual(link.href!.href, endpoint.href);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("deepClone() copies byte arrays including Buffer storage", async () => {
|
|
128
|
+
for (const bytes of [new Uint8Array([1, 2, 3]), Buffer.from([1, 2, 3])]) {
|
|
129
|
+
const proof = new DataIntegrityProof({ proofValue: bytes });
|
|
130
|
+
const original = new Note({ proof });
|
|
131
|
+
const copy = original.deepClone();
|
|
132
|
+
const copiedProof = await copy.getProof();
|
|
133
|
+
ok(copiedProof instanceof DataIntegrityProof);
|
|
134
|
+
notStrictEqual(copiedProof, proof);
|
|
135
|
+
notStrictEqual(copiedProof.proofValue, bytes);
|
|
136
|
+
copiedProof.proofValue![0] = 9;
|
|
137
|
+
strictEqual(bytes[0], 1);
|
|
138
|
+
bytes[1] = 8;
|
|
139
|
+
strictEqual(copiedProof.proofValue![1], 2);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("deepClone() shares immutable scalars and preserves sparse arrays", () => {
|
|
144
|
+
const content = new LanguageString("Hello", "en");
|
|
145
|
+
const published = Temporal.Instant.from("2026-10-27T00:00:00Z");
|
|
146
|
+
const names = new Array<string>(3);
|
|
147
|
+
names[2] = "Kept";
|
|
148
|
+
const source = new Note({ names, content, published });
|
|
149
|
+
const copy = source.deepClone();
|
|
150
|
+
strictEqual(copy.content, content);
|
|
151
|
+
strictEqual(copy.published, published);
|
|
152
|
+
deepStrictEqual(copy.names, source.names);
|
|
153
|
+
strictEqual(0 in copy.names, false);
|
|
154
|
+
copy.names.push("Copy only");
|
|
155
|
+
strictEqual(source.names.length, 3);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("deepClone() retains node defaults and overrides only the root", async () => {
|
|
159
|
+
const fetched: string[] = [];
|
|
160
|
+
const [rootTracer, rootSpans] = createTestTracerProvider();
|
|
161
|
+
const [childTracer, childSpans] = createTestTracerProvider();
|
|
162
|
+
const child = new Note({
|
|
163
|
+
attribution: new URL("https://example.com/child-actor"),
|
|
164
|
+
}, {
|
|
165
|
+
...options,
|
|
166
|
+
documentLoader: personLoader("Child", fetched),
|
|
167
|
+
tracerProvider: childTracer,
|
|
168
|
+
});
|
|
169
|
+
const source = new Create({
|
|
170
|
+
actor: new URL("https://example.com/root-actor"),
|
|
171
|
+
object: child,
|
|
172
|
+
}, {
|
|
173
|
+
...options,
|
|
174
|
+
documentLoader: personLoader("Root", fetched),
|
|
175
|
+
tracerProvider: rootTracer,
|
|
176
|
+
});
|
|
177
|
+
const copy = source.deepClone({}, {
|
|
178
|
+
documentLoader: personLoader("Override"),
|
|
179
|
+
});
|
|
180
|
+
strictEqual((await copy.getActor())?.name, "Override");
|
|
181
|
+
const copiedChild = await copy.getObject();
|
|
182
|
+
ok(copiedChild instanceof Note);
|
|
183
|
+
strictEqual((await copiedChild.getAttribution())?.name, "Child");
|
|
184
|
+
strictEqual((await source.getActor())?.name, "Root");
|
|
185
|
+
deepStrictEqual(fetched, [
|
|
186
|
+
"https://example.com/child-actor",
|
|
187
|
+
"https://example.com/root-actor",
|
|
188
|
+
]);
|
|
189
|
+
strictEqual(rootSpans.getSpans("activitypub.lookup_object").length, 2);
|
|
190
|
+
strictEqual(childSpans.getSpans("activitypub.lookup_object").length, 1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("deepClone() preserves origin checks and isolates hydrated trust", async () => {
|
|
194
|
+
let fetches = 0;
|
|
195
|
+
const original = await Create.fromJsonLd({
|
|
196
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
197
|
+
id: "https://a.example/create",
|
|
198
|
+
type: "Create",
|
|
199
|
+
object: {
|
|
200
|
+
id: "https://b.example/note",
|
|
201
|
+
type: "Note",
|
|
202
|
+
content: "Embedded",
|
|
203
|
+
},
|
|
204
|
+
}, {
|
|
205
|
+
...options,
|
|
206
|
+
// deno-lint-ignore require-await
|
|
207
|
+
documentLoader: async (url) => {
|
|
208
|
+
fetches++;
|
|
209
|
+
return {
|
|
210
|
+
contextUrl: null,
|
|
211
|
+
documentUrl: url,
|
|
212
|
+
document: {
|
|
213
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
214
|
+
id: url,
|
|
215
|
+
type: "Note",
|
|
216
|
+
content: "Fetched",
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const copy = original.deepClone();
|
|
222
|
+
strictEqual((await copy.getObject())?.content, "Fetched");
|
|
223
|
+
strictEqual(fetches, 1);
|
|
224
|
+
strictEqual(
|
|
225
|
+
(await original.getObject({ crossOrigin: "trust" }))?.content,
|
|
226
|
+
"Embedded",
|
|
227
|
+
);
|
|
228
|
+
strictEqual((await original.getObject())?.content, "Fetched");
|
|
229
|
+
strictEqual(fetches, 2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("deepClone() keeps base URLs and discards cached JSON-LD", async () => {
|
|
233
|
+
const source = await Note.fromJsonLd({
|
|
234
|
+
"@context": ["https://www.w3.org/ns/activitystreams", {
|
|
235
|
+
extension: "https://example.com/extension",
|
|
236
|
+
}],
|
|
237
|
+
type: "Note",
|
|
238
|
+
id: "./note",
|
|
239
|
+
attributedTo: "./alice",
|
|
240
|
+
extension: "received only",
|
|
241
|
+
}, {
|
|
242
|
+
...options,
|
|
243
|
+
baseUrl: new URL("https://example.com/posts/"),
|
|
244
|
+
documentLoader: personLoader("Alice"),
|
|
245
|
+
});
|
|
246
|
+
const copy = source.deepClone({ content: "Copy" });
|
|
247
|
+
strictEqual(copy.id!.href, "https://example.com/posts/note");
|
|
248
|
+
strictEqual(
|
|
249
|
+
(await copy.getAttribution())?.id?.href,
|
|
250
|
+
"https://example.com/posts/alice",
|
|
251
|
+
);
|
|
252
|
+
const json = await copy.toJsonLd(options) as Record<string, unknown>;
|
|
253
|
+
strictEqual(json.content, "Copy");
|
|
254
|
+
strictEqual(json.extension, undefined);
|
|
255
|
+
strictEqual(
|
|
256
|
+
(await source.toJsonLd() as Record<string, unknown>).extension,
|
|
257
|
+
"received only",
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("deepClone() validates replacements through clone() only", () => {
|
|
262
|
+
const source = new Note({ content: "Source" });
|
|
263
|
+
for (
|
|
264
|
+
const values of [
|
|
265
|
+
{ id: 42 },
|
|
266
|
+
{ name: "one", names: ["two"] },
|
|
267
|
+
{ names: [42] },
|
|
268
|
+
]
|
|
269
|
+
) {
|
|
270
|
+
throws(() => source.deepClone(values as never), TypeError);
|
|
271
|
+
}
|
|
272
|
+
function values(counter: { reads: number }) {
|
|
273
|
+
return Object.create({
|
|
274
|
+
get content() {
|
|
275
|
+
counter.reads++;
|
|
276
|
+
return "Inherited";
|
|
277
|
+
},
|
|
278
|
+
}) as { content: string };
|
|
279
|
+
}
|
|
280
|
+
const shallowReads = { reads: 0 };
|
|
281
|
+
const deepReads = { reads: 0 };
|
|
282
|
+
strictEqual(source.clone(values(shallowReads)).content, "Inherited");
|
|
283
|
+
strictEqual(source.deepClone(values(deepReads)).content, "Inherited");
|
|
284
|
+
strictEqual(deepReads.reads, shallowReads.reads);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("deepClone() preserves user subclasses and handles deep graphs", async () => {
|
|
288
|
+
class CustomNote extends Note {}
|
|
289
|
+
const child = new CustomNote({ content: "Child" });
|
|
290
|
+
const copiedChild = await new Create({ object: child }).deepClone()
|
|
291
|
+
.getObject();
|
|
292
|
+
ok(copiedChild instanceof CustomNote);
|
|
293
|
+
notStrictEqual(copiedChild, child);
|
|
294
|
+
let source = new Note({ content: "End" });
|
|
295
|
+
for (let i = 0; i < 3000; i++) source = new Note({ replyTarget: source });
|
|
296
|
+
let copy = source.deepClone();
|
|
297
|
+
for (let i = 0; i < 3000; i++) {
|
|
298
|
+
const next = await copy.getReplyTarget();
|
|
299
|
+
ok(next instanceof Note);
|
|
300
|
+
copy = next;
|
|
301
|
+
}
|
|
302
|
+
strictEqual(copy.content, "End");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("deepClone() rejects broken subclass clone results", () => {
|
|
306
|
+
class ReturnsSelf extends Note {
|
|
307
|
+
override clone(): this {
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const source = new ReturnsSelf({ id: new URL("https://example.com/note") });
|
|
312
|
+
const id = source.id;
|
|
313
|
+
throws(() => source.deepClone(), /fresh, compatible/);
|
|
314
|
+
strictEqual(source.id, id);
|
|
315
|
+
|
|
316
|
+
class Incompatible extends Note {
|
|
317
|
+
override clone(): Note {
|
|
318
|
+
return new VocabularyObject({}) as Note;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
throws(() => new Incompatible({}).deepClone(), /fresh, compatible/);
|
|
322
|
+
|
|
323
|
+
class ReusesCopy extends Note {
|
|
324
|
+
static copy: Note | undefined;
|
|
325
|
+
override clone(): Note {
|
|
326
|
+
return ReusesCopy.copy ??= super.clone();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const first = new ReusesCopy({ content: "First" });
|
|
330
|
+
const second = new ReusesCopy({ content: "Second" });
|
|
331
|
+
throws(
|
|
332
|
+
() => new Create({ objects: [first, second] }).deepClone(),
|
|
333
|
+
/fresh, compatible/,
|
|
334
|
+
);
|
|
335
|
+
strictEqual(first.content, "First");
|
|
336
|
+
strictEqual(second.content, "Second");
|
|
337
|
+
|
|
338
|
+
class Throws extends Note {
|
|
339
|
+
override clone(): never {
|
|
340
|
+
throw new Error("Copy failed");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const child = new Throws({ content: "Unchanged" });
|
|
344
|
+
const parent = new Create({ object: child });
|
|
345
|
+
throws(() => parent.deepClone(), /Copy failed/);
|
|
346
|
+
strictEqual(child.content, "Unchanged");
|
|
347
|
+
});
|
package/src/gateway.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
type DocumentLoader,
|
|
8
8
|
FetchError,
|
|
9
|
+
formatIri,
|
|
9
10
|
getDocumentLoader,
|
|
10
11
|
parseIri,
|
|
11
12
|
type PortableObjectVerification,
|
|
@@ -1062,6 +1063,81 @@ test("getFirst() passes the referrer chain and collection context", async () =>
|
|
|
1062
1063
|
ok(referrer?.referrer?.referrer?.referrer?.object === create);
|
|
1063
1064
|
});
|
|
1064
1065
|
|
|
1066
|
+
test("deepClone() preserves historical ancestry independently of replacements", async () => {
|
|
1067
|
+
const context = Object.freeze({});
|
|
1068
|
+
const documentLoader = createLoader({
|
|
1069
|
+
[gatewayUrl("https://gw.example", "/actor/outbox")]: portableCollection(
|
|
1070
|
+
"/actor/outbox",
|
|
1071
|
+
{
|
|
1072
|
+
attributedTo: `ap://${did}/actor`,
|
|
1073
|
+
first: {
|
|
1074
|
+
id: `ap://${did}/actor/outbox?page=1`,
|
|
1075
|
+
type: "OrderedCollectionPage",
|
|
1076
|
+
// An existing mismatch must not be rewritten to match root overrides.
|
|
1077
|
+
partOf: `ap://${did}/other/outbox`,
|
|
1078
|
+
next: `ap://${did}/actor/outbox?page=2`,
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
),
|
|
1082
|
+
[gatewayUrl("https://gw.example", "/actor/outbox?page=2")]: {
|
|
1083
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1084
|
+
id: `ap://${did}/actor/outbox?page=2`,
|
|
1085
|
+
type: "OrderedCollectionPage",
|
|
1086
|
+
},
|
|
1087
|
+
[gatewayUrl("https://gw.example", "/actor")]: {
|
|
1088
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1089
|
+
id: `ap://${did}/actor`,
|
|
1090
|
+
type: "Person",
|
|
1091
|
+
name: "Fetched actor",
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
const verifier = createRecordingVerifier(() => ({
|
|
1095
|
+
verified: true,
|
|
1096
|
+
collectionContext: context,
|
|
1097
|
+
}));
|
|
1098
|
+
const options = {
|
|
1099
|
+
documentLoader,
|
|
1100
|
+
contextLoader: mockDocumentLoader,
|
|
1101
|
+
gateways: ["https://gw.example"],
|
|
1102
|
+
verifyPortableObject: verifier,
|
|
1103
|
+
};
|
|
1104
|
+
const actor = await Person.fromJsonLd({
|
|
1105
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1106
|
+
id: `ap://${did}/actor`,
|
|
1107
|
+
type: "Person",
|
|
1108
|
+
outbox: `ap://${did}/actor/outbox`,
|
|
1109
|
+
}, { contextLoader: mockDocumentLoader });
|
|
1110
|
+
const outbox = await actor.getOutbox(options);
|
|
1111
|
+
assertInstanceOf(outbox, OrderedCollection);
|
|
1112
|
+
// Record first -> outbox provenance before copying.
|
|
1113
|
+
const first = await outbox.getFirst(options);
|
|
1114
|
+
assertInstanceOf(first, CollectionPage);
|
|
1115
|
+
const copy = outbox.deepClone({
|
|
1116
|
+
id: parseIri(`ap://${did}/other/outbox`),
|
|
1117
|
+
attribution: parseIri(`ap://${did}/different-actor`),
|
|
1118
|
+
});
|
|
1119
|
+
const copiedFirst = await copy.getFirst(options);
|
|
1120
|
+
assertInstanceOf(copiedFirst, CollectionPage);
|
|
1121
|
+
ok(copiedFirst !== first);
|
|
1122
|
+
await copiedFirst.getNext(options);
|
|
1123
|
+
const referrer = verifier.calls.at(-1)!.options.referrer;
|
|
1124
|
+
ok(referrer?.object === copiedFirst);
|
|
1125
|
+
const history = referrer.referrer;
|
|
1126
|
+
ok(history?.object instanceof OrderedCollection);
|
|
1127
|
+
ok(history.object !== copy && history.object !== outbox);
|
|
1128
|
+
deepStrictEqual(history.id, outbox.id);
|
|
1129
|
+
deepStrictEqual(history.acceptance, undefined);
|
|
1130
|
+
deepStrictEqual(history.collectionContext, undefined);
|
|
1131
|
+
deepStrictEqual(history.object.attributionId, outbox.attributionId);
|
|
1132
|
+
deepStrictEqual(copiedFirst.partOfId, first.partOfId);
|
|
1133
|
+
ok(copiedFirst.partOfId!.href !== history.id!.href);
|
|
1134
|
+
// External ancestry is retained, while in-graph history can hydrate safely.
|
|
1135
|
+
ok(history.referrer?.object === actor);
|
|
1136
|
+
await history.object.getAttribution(options);
|
|
1137
|
+
const originalJson = await outbox.toJsonLd() as Record<string, unknown>;
|
|
1138
|
+
deepStrictEqual(originalJson.attributedTo, formatIri(outbox.attributionId!));
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1065
1141
|
test("accessors do not trust objects embedded in unsecured collections", async () => {
|
|
1066
1142
|
const itemPath = "/objects/1";
|
|
1067
1143
|
const documentLoader = createLoader({
|
|
@@ -1116,14 +1192,14 @@ test("accessors do not trust objects embedded in unsecured collections", async (
|
|
|
1116
1192
|
// again (and fail here, since nothing can be fetched):
|
|
1117
1193
|
const outbox3 = await (await createPerson()).getOutbox(options);
|
|
1118
1194
|
assertInstanceOf(outbox3, Collection);
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1195
|
+
for (const copy of [outbox3.clone(), outbox3.deepClone()]) {
|
|
1196
|
+
const items3 = await Array.fromAsync(copy.getItems({
|
|
1121
1197
|
...options,
|
|
1122
1198
|
documentLoader: createLoader({}),
|
|
1123
1199
|
suppressError: true,
|
|
1124
|
-
})
|
|
1125
|
-
|
|
1126
|
-
|
|
1200
|
+
}));
|
|
1201
|
+
deepStrictEqual(items3, []);
|
|
1202
|
+
}
|
|
1127
1203
|
});
|
|
1128
1204
|
|
|
1129
1205
|
function createRedirectingLoader(
|
|
@@ -193,6 +193,27 @@ test("a clone does not inherit a retained signed representation", async () => {
|
|
|
193
193
|
deepStrictEqual(embedded.content, "Changed");
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
test("deepClone() drops retained signed representations at every level", async () => {
|
|
197
|
+
const note = retainedNote();
|
|
198
|
+
const original = new Create({ object: note });
|
|
199
|
+
retainSignedRepresentation(original, {
|
|
200
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
201
|
+
type: "Create",
|
|
202
|
+
content: "Retained root only",
|
|
203
|
+
});
|
|
204
|
+
const copied = original.deepClone();
|
|
205
|
+
const json = await new Create({ object: copied }).toJsonLd(options) as {
|
|
206
|
+
object: { content?: unknown; object: Record<string, unknown> };
|
|
207
|
+
};
|
|
208
|
+
deepStrictEqual(json.object.content, undefined);
|
|
209
|
+
deepStrictEqual(json.object.object.content, "Hello");
|
|
210
|
+
deepStrictEqual(json.object.object.proof, undefined);
|
|
211
|
+
const originalJson = await new Create({ object: note }).toJsonLd(options) as {
|
|
212
|
+
object: unknown;
|
|
213
|
+
};
|
|
214
|
+
deepStrictEqual(originalJson.object, securedNote);
|
|
215
|
+
});
|
|
216
|
+
|
|
196
217
|
test("every generated default context keeps placeholders recoverable", async () => {
|
|
197
218
|
interface VocabClass {
|
|
198
219
|
new (values: { id?: URL | null }): { toJsonLd(): Promise<unknown> };
|