@fedify/fedify 2.5.0-dev.2597 → 2.5.0-dev.2614

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 (123) hide show
  1. package/dist/{builder-Da_V_LDj.mjs → builder-B2H8GsyI.mjs} +82 -3
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-BqqP38Sf.mjs → compound-proof-BavXW-ow.mjs} +2 -2
  6. package/dist/{context-ByflxsY9.d.cts → context-C1NqcIct.d.cts} +1061 -443
  7. package/dist/{context-uQUY5fQT.d.ts → context-xrqq4Hrq.d.ts} +1064 -446
  8. package/dist/{deno-4cGKg59R.mjs → deno-CRXki9-m.mjs} +1 -1
  9. package/dist/{docloader-BiHD6NSO.mjs → docloader-Ca6i6ddp.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +4 -4
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/inbox-actorless.test.mjs +5 -5
  17. package/dist/federation/inbox-report.test.mjs +220 -5
  18. package/dist/federation/inbox-view.test.mjs +1 -1
  19. package/dist/federation/inbox.test.mjs +1 -1
  20. package/dist/federation/metrics.test.mjs +1 -1
  21. package/dist/federation/middleware.test.mjs +7 -7
  22. package/dist/federation/mod.cjs +1 -1
  23. package/dist/federation/mod.d.cts +3 -3
  24. package/dist/federation/mod.d.ts +4 -4
  25. package/dist/federation/mod.js +1 -1
  26. package/dist/federation/mq.test.mjs +1 -1
  27. package/dist/federation/outbound-body.test.d.mts +2 -0
  28. package/dist/federation/outbound-body.test.mjs +149 -0
  29. package/dist/federation/outbound-capture.test.d.mts +2 -0
  30. package/dist/federation/outbound-capture.test.mjs +394 -0
  31. package/dist/federation/outbound-dispatch.test.d.mts +2 -0
  32. package/dist/federation/outbound-dispatch.test.mjs +368 -0
  33. package/dist/federation/outbound-document.test.d.mts +2 -0
  34. package/dist/federation/outbound-document.test.mjs +171 -0
  35. package/dist/federation/outbound-provenance.test.d.mts +2 -0
  36. package/dist/federation/outbound-provenance.test.mjs +543 -0
  37. package/dist/federation/outbound-report.test.d.mts +2 -0
  38. package/dist/federation/outbound-report.test.mjs +5475 -0
  39. package/dist/federation/outbound-workers.test.d.mts +2 -0
  40. package/dist/federation/outbound-workers.test.mjs +64 -0
  41. package/dist/federation/outbound-workflow.test.d.mts +2 -0
  42. package/dist/federation/outbound-workflow.test.mjs +154 -0
  43. package/dist/federation/outgoing-proof.test.mjs +4 -4
  44. package/dist/federation/portable-actor.test.mjs +3 -3
  45. package/dist/federation/portable-chain.test.mjs +3 -3
  46. package/dist/federation/portable-collection.test.mjs +4 -4
  47. package/dist/federation/portable-delivery.test.mjs +106 -7
  48. package/dist/federation/portable-gateway-keys.test.mjs +5 -5
  49. package/dist/federation/portable-inbox.test.mjs +430 -5
  50. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  51. package/dist/federation/portable-route-activity.test.mjs +2 -2
  52. package/dist/federation/portable.test.mjs +3 -3
  53. package/dist/federation/send.test.mjs +44 -3
  54. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  55. package/dist/federation/tasks/tasks.test.mjs +2 -2
  56. package/dist/federation/temporal.test.mjs +1 -1
  57. package/dist/federation/webfinger.test.mjs +1 -1
  58. package/dist/{federation-BKJZEC0O.cjs → federation-DRl81KbJ.cjs} +2358 -405
  59. package/dist/{http-DQP4rw46.js → http-C2H4DT6t.js} +62 -32
  60. package/dist/{http-KU7_TLD9.cjs → http-DHSIMipE.cjs} +62 -32
  61. package/dist/{http-D5n4S1oL.mjs → http-DqOXkYbX.mjs} +64 -34
  62. package/dist/{inbox-CElAglrC.mjs → inbox-C4iW1g0F.mjs} +90 -3
  63. package/dist/{inbox-view-BTUuIIf6.mjs → inbox-view-Bu_aSwFJ.mjs} +1 -1
  64. package/dist/{key-DbLNT-vA.d.ts → key-dwwAg5bA.d.ts} +1 -1
  65. package/dist/{kv-cache-CFBwLUZr.js → kv-cache-B-x_OrBo.js} +1 -1
  66. package/dist/{kv-cache-DrQn-ugb.cjs → kv-cache-CHR_BVyM.cjs} +1 -1
  67. package/dist/{kv-cache-BV55jAE6.mjs → kv-cache-DJsxu9nI.mjs} +1 -1
  68. package/dist/{ld-CmAQYDWE.mjs → ld-C5JOBBFj.mjs} +2 -2
  69. package/dist/{ld-Inagww4R.d.ts → ld-o4LnhCYY.d.ts} +2 -2
  70. package/dist/{metrics-BvV60TwE.mjs → metrics-CTteRnEr.mjs} +1 -1
  71. package/dist/{metrics-DoRtI0qU.d.ts → metrics-DVKYY6K1.d.ts} +1 -1
  72. package/dist/{middleware-Di4y_M3y.js → middleware-B-tMVSUA.js} +2303 -351
  73. package/dist/{middleware-D02eC0j7.mjs → middleware-BkRqMGPU.mjs} +1573 -325
  74. package/dist/{middleware-iAJcDIM2.mjs → middleware-CPigKLnt.mjs} +1 -1
  75. package/dist/{mod-B7j30IQf.d.ts → mod-CQC9xlLZ.d.ts} +3 -3
  76. package/dist/{mod-B4kBw1-8.d.ts → mod-D22ZsiKi.d.ts} +2 -2
  77. package/dist/{mod-BN0TTfyQ.d.ts → mod-DO0epJGH.d.ts} +1 -1
  78. package/dist/{mod-DF2gbN_d.d.cts → mod-TwiQgEf3.d.cts} +1 -1
  79. package/dist/mod.cjs +4 -4
  80. package/dist/mod.d.cts +4 -4
  81. package/dist/mod.d.ts +8 -8
  82. package/dist/mod.js +4 -4
  83. package/dist/nodeinfo/handler.test.mjs +1 -1
  84. package/dist/outbound-body-CckPA5gc.mjs +83 -0
  85. package/dist/outbound-capture-DVgnRzlf.mjs +184 -0
  86. package/dist/outbound-document-BMgwLXTY.mjs +179 -0
  87. package/dist/{owner-C1-csjTo.mjs → owner-CKlRIkeF.mjs} +3 -3
  88. package/dist/{portable-collection-C0MNEEED.mjs → portable-collection-B7RBHVGl.mjs} +2 -2
  89. package/dist/{portable-collection-DkTLVfoM.js → portable-collection-CByyPjLc.js} +2 -2
  90. package/dist/{portable-collection-w6Qn-vJ3.cjs → portable-collection-CmLjeKsP.cjs} +2 -2
  91. package/dist/{portable-key-jtaxB7TD.mjs → portable-key-Clg0IH8C.mjs} +1 -1
  92. package/dist/{proof-CzWCRhnC.mjs → proof-tAb5_Dq4.mjs} +3 -3
  93. package/dist/{send-C9-dwpEH.mjs → send-DzfFREML.mjs} +153 -40
  94. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  95. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  96. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  97. package/dist/sig/compound-proof.test.mjs +2 -2
  98. package/dist/sig/http.test.mjs +38 -2
  99. package/dist/sig/key.test.mjs +1 -1
  100. package/dist/sig/ld.test.mjs +1 -1
  101. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  102. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  103. package/dist/sig/mod.cjs +2 -2
  104. package/dist/sig/mod.d.ts +3 -3
  105. package/dist/sig/mod.js +2 -2
  106. package/dist/sig/owner.test.mjs +1 -1
  107. package/dist/sig/portable-collection.test.mjs +1 -1
  108. package/dist/sig/portable-interop.test.mjs +3 -3
  109. package/dist/sig/portable-key.test.mjs +5 -5
  110. package/dist/sig/proof.test.mjs +3 -3
  111. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  112. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  113. package/dist/sig/verification.test.mjs +3 -3
  114. package/dist/{temporal-lDCl8Zxn.mjs → temporal-D9Nt8Fm4.mjs} +1 -1
  115. package/dist/testing/kv.test.mjs +1 -1
  116. package/dist/testing/mod.d.mts +1030 -412
  117. package/dist/utils/docloader.test.mjs +2 -2
  118. package/dist/utils/kv-cache.test.mjs +1 -1
  119. package/dist/utils/mod.cjs +1 -1
  120. package/dist/utils/mod.d.ts +1 -1
  121. package/dist/utils/mod.js +1 -1
  122. package/package.json +6 -6
  123. package/skills/fedify/SKILL.md +60 -1
@@ -0,0 +1,368 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
+ import { i as rsaPrivateKey2, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
7
+ import { o as createFederation, t as ContextImpl } from "../middleware-BkRqMGPU.mjs";
8
+ import { configure, getLogger, reset, withContext } from "@logtape/logtape";
9
+ import { Create, Person } from "@fedify/vocab";
10
+ import { mockDocumentLoader, test } from "@fedify/fixture";
11
+ import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
12
+ import { AsyncLocalStorage } from "node:async_hooks";
13
+ //#region src/federation/outbound-dispatch.test.ts
14
+ const activity = new Create({
15
+ id: new URL("https://example.com/dispatch"),
16
+ actor: new URL("https://example.com/person2")
17
+ });
18
+ const target = new Person({
19
+ id: new URL("https://remote.example/bob"),
20
+ inbox: new URL("https://remote.example/inbox")
21
+ });
22
+ const sender = {
23
+ keyId: rsaPublicKey2.id,
24
+ privateKey: rsaPrivateKey2
25
+ };
26
+ const loaders = {
27
+ documentLoaderFactory: () => mockDocumentLoader,
28
+ contextLoaderFactory: () => mockDocumentLoader
29
+ };
30
+ const modes = [
31
+ "send",
32
+ "outbox-send",
33
+ "inbox-forward",
34
+ "outbox-forward"
35
+ ];
36
+ function entry(federation, mode, identity = sender) {
37
+ const base = federation.createContext(new URL("https://example.com"), void 0);
38
+ ok(base instanceof ContextImpl);
39
+ const outbox = base.toOutboxContext("alice", {}, activity.id.href, Create.typeId.href);
40
+ const inbox = base.toInboxContext(null, {}, activity.id.href, Create.typeId.href);
41
+ if (mode === "send") return {
42
+ context: base,
43
+ invoke: (options) => base.sendActivity(identity, target, activity, Object.assign(options, { fanout: "force" }))
44
+ };
45
+ if (mode === "outbox-send") return {
46
+ context: outbox,
47
+ invoke: (options) => outbox.sendActivity(identity, target, activity, Object.assign(options, { fanout: "skip" }))
48
+ };
49
+ const context = mode === "inbox-forward" ? inbox : outbox;
50
+ return {
51
+ context,
52
+ invoke: (options) => context.forwardActivity(identity, target, Object.assign(options, { skipIfUnsigned: false }))
53
+ };
54
+ }
55
+ test("dispatch ID validation precedes send and forward side effects", async (t) => {
56
+ for (const { mode, observed } of [{
57
+ mode: "send",
58
+ observed: false
59
+ }, ...modes.map((mode) => ({
60
+ mode,
61
+ observed: true
62
+ }))]) await t.step(`${mode}: ${observed ? "observed" : "unobserved"}`, async () => {
63
+ let sideEffects = 0;
64
+ const queue = {
65
+ enqueue() {
66
+ sideEffects++;
67
+ return Promise.resolve();
68
+ },
69
+ listen: () => Promise.resolve()
70
+ };
71
+ const federation = createFederation({
72
+ ...loaders,
73
+ kv: new MemoryKvStore(),
74
+ manuallyStartQueue: true,
75
+ queue: {
76
+ outbox: queue,
77
+ fanout: queue
78
+ }
79
+ });
80
+ federation.setActorDispatcher("/users/{identifier}", () => {
81
+ sideEffects++;
82
+ return new Person({});
83
+ }).setKeyPairsDispatcher(() => {
84
+ sideEffects++;
85
+ return [];
86
+ });
87
+ const { context, invoke } = entry(federation, mode, { identifier: "alice" });
88
+ const provider = context.tracerProvider;
89
+ Object.defineProperty(context, "tracerProvider", { get() {
90
+ sideEffects++;
91
+ return provider;
92
+ } });
93
+ const events = [];
94
+ let deliveries = 0;
95
+ if (observed) federation.setOutboundObservers({
96
+ onBroadcast(_ctx, event) {
97
+ events.push(event);
98
+ },
99
+ onDelivery() {
100
+ deliveries++;
101
+ }
102
+ });
103
+ for (const value of [
104
+ null,
105
+ "x".repeat(1025),
106
+ "😀".repeat(257)
107
+ ]) {
108
+ const start = events.length;
109
+ let original;
110
+ await rejects(invoke({ dispatchId: value }), (error) => {
111
+ original = error;
112
+ ok(value === null ? error instanceof TypeError : error instanceof RangeError);
113
+ ok(error.message.includes("dispatchId"));
114
+ return true;
115
+ });
116
+ strictEqual(sideEffects, 0);
117
+ strictEqual(deliveries, 0);
118
+ if (observed) {
119
+ deepStrictEqual(events.slice(start).map((event) => event.stage), ["started", "preparation-finished"]);
120
+ const finished = events.at(-1);
121
+ ok(finished.stage === "preparation-finished" && finished.result.type === "failed");
122
+ strictEqual(finished.result.failure.phase, "validation");
123
+ strictEqual(finished.result.failure.error, original);
124
+ strictEqual(finished.provenance.dispatchId, void 0);
125
+ }
126
+ }
127
+ });
128
+ });
129
+ test("dispatch IDs correlate send and forward entries without deduplication or context leakage", async (t) => {
130
+ for (const mode of modes) await t.step(mode, async () => {
131
+ const messages = [];
132
+ const queue = {
133
+ enqueue(message) {
134
+ messages.push(message);
135
+ return Promise.resolve();
136
+ },
137
+ listen: () => Promise.resolve()
138
+ };
139
+ const federation = createFederation({
140
+ ...loaders,
141
+ kv: new MemoryKvStore(),
142
+ manuallyStartQueue: true,
143
+ queue: {
144
+ fanout: queue,
145
+ outbox: queue
146
+ }
147
+ });
148
+ const { context, invoke } = entry(federation, mode);
149
+ const broadcasts = [];
150
+ const deliveries = [];
151
+ const contexts = [];
152
+ let current = {};
153
+ federation.setOutboundObservers({
154
+ onBroadcast(ctx, event) {
155
+ broadcasts.push(event);
156
+ contexts.push(ctx);
157
+ if (event.stage === "started") current.dispatchId = "observer-mutated";
158
+ },
159
+ onDelivery(ctx, event) {
160
+ deliveries.push(event);
161
+ contexts.push(ctx);
162
+ }
163
+ });
164
+ const dispatchId = " app\\dispatch\"\n한글 ";
165
+ for (const value of [
166
+ dispatchId,
167
+ dispatchId,
168
+ void 0
169
+ ]) {
170
+ current = { dispatchId: value };
171
+ await invoke(current);
172
+ }
173
+ strictEqual(messages.length, 3);
174
+ const starts = broadcasts.filter((event) => event.stage === "started");
175
+ strictEqual(starts.length, 3);
176
+ strictEqual(new Set(starts.map((event) => event.broadcastId)).size, 3);
177
+ deepStrictEqual(starts.map((event) => event.activity), [
178
+ {},
179
+ {},
180
+ {}
181
+ ]);
182
+ const finishes = broadcasts.filter((event) => event.stage === "preparation-finished");
183
+ strictEqual(finishes.length, 3);
184
+ for (const event of finishes) {
185
+ deepStrictEqual(event.activity, {
186
+ id: activity.id.href,
187
+ type: Create.typeId.href
188
+ });
189
+ ok(Object.isFrozen(event.activity));
190
+ }
191
+ for (const ctx of contexts) strictEqual(ctx, context);
192
+ for (let index = 0; index < 3; index++) {
193
+ const message = messages[index];
194
+ ok(message.type === "outbox" || message.type === "fanout");
195
+ const expected = index < 2 ? dispatchId : void 0;
196
+ strictEqual(message.provenance?.dispatchId, expected);
197
+ strictEqual(starts[index].provenance.dispatchId, expected);
198
+ deepStrictEqual(JSON.parse(JSON.stringify(message.provenance ?? {})), message.provenance ?? {});
199
+ const delivery = deliveries[index * 2];
200
+ strictEqual(delivery.provenance.dispatchId, expected);
201
+ }
202
+ if (mode === "send") {
203
+ const fanout = messages[0];
204
+ ok(fanout.type === "fanout");
205
+ await federation.processQueuedTask(void 0, JSON.parse(JSON.stringify(fanout)));
206
+ await federation.processQueuedTask(void 0, structuredClone(fanout));
207
+ for (const child of messages.slice(3)) {
208
+ ok(child.type === "outbox");
209
+ strictEqual(child.provenance?.dispatchId, dispatchId);
210
+ deepStrictEqual(child.outbound, fanout.inboxes[target.inboxId.href].outbound);
211
+ }
212
+ strictEqual(messages.length, 5);
213
+ strictEqual(broadcasts.length, 6);
214
+ }
215
+ });
216
+ });
217
+ test("dispatch ID byte limits and queue fallback preserve exact values", async () => {
218
+ const messages = [];
219
+ const queue = {
220
+ enqueue(message) {
221
+ messages.push(message);
222
+ return Promise.resolve();
223
+ },
224
+ listen: () => Promise.resolve()
225
+ };
226
+ const { invoke } = entry(createFederation({
227
+ ...loaders,
228
+ kv: new MemoryKvStore(),
229
+ manuallyStartQueue: true,
230
+ queue: { outbox: queue }
231
+ }), "outbox-send");
232
+ const valid = [
233
+ "",
234
+ "x".repeat(1024),
235
+ "😀".repeat(256)
236
+ ];
237
+ for (const dispatchId of valid) await invoke({ dispatchId });
238
+ strictEqual(messages.length, 3);
239
+ const records = [];
240
+ await reset();
241
+ await configure({
242
+ sinks: { buffer: (record) => records.push(record) },
243
+ loggers: [
244
+ {
245
+ category: ["test", "dispatch"],
246
+ sinks: ["buffer"],
247
+ lowestLevel: "info"
248
+ },
249
+ {
250
+ category: [
251
+ "fedify",
252
+ "federation",
253
+ "queue"
254
+ ],
255
+ sinks: ["buffer"],
256
+ lowestLevel: "warning"
257
+ },
258
+ {
259
+ category: ["logtape", "meta"],
260
+ sinks: []
261
+ }
262
+ ],
263
+ contextLocalStorage: new AsyncLocalStorage()
264
+ });
265
+ const consumer = createFederation({
266
+ kv: new MemoryKvStore(),
267
+ allowPrivateAddress: true,
268
+ manuallyStartQueue: true,
269
+ queue: { outbox: queue }
270
+ });
271
+ const descendants = [];
272
+ consumer.setOutboundObservers({ onBroadcast(_ctx, event) {
273
+ descendants.push(event);
274
+ } });
275
+ consumer.setOutboxPermanentFailureHandler(async (ctx) => {
276
+ getLogger(["test", "dispatch"]).info("original dispatch");
277
+ await ctx.sendActivity(sender, [], activity);
278
+ });
279
+ esm_default.mockGlobal().post(target.inboxId.href, 410);
280
+ try {
281
+ const queued = messages[0];
282
+ ok(queued.type === "outbox");
283
+ const malformed = [
284
+ { dispatchId: null },
285
+ { dispatchId: "x".repeat(1025) },
286
+ { dispatchId: "😀".repeat(257) },
287
+ { inboxRequest: {
288
+ inboxRequestId: "",
289
+ requestId: "bad",
290
+ context: {
291
+ origin: "x",
292
+ host: "x",
293
+ hostname: "x",
294
+ canonicalOrigin: "x"
295
+ }
296
+ } }
297
+ ];
298
+ for (const provenance of malformed) await withContext({
299
+ requestId: "current-request",
300
+ dispatchId: "ambient-dispatch"
301
+ }, () => consumer.processQueuedTask(void 0, {
302
+ ...queued,
303
+ provenance
304
+ }));
305
+ ok(esm_default.callHistory.calls().length >= malformed.length);
306
+ const warnings = records.filter((record) => record.rawMessage === "Ignoring invalid provenance in queue message {messageId}: {error}");
307
+ strictEqual(warnings.length, malformed.length);
308
+ for (const warning of warnings) strictEqual(warning.properties.messageId, queued.id);
309
+ for (let index = 0; index < messages.length; index++) {
310
+ const message = messages[index];
311
+ ok(message.type === "outbox");
312
+ strictEqual(message.provenance?.dispatchId, valid[index]);
313
+ await withContext({
314
+ requestId: "current-request",
315
+ dispatchId: "ambient-dispatch"
316
+ }, () => consumer.processQueuedTask(void 0, JSON.parse(JSON.stringify(message))));
317
+ }
318
+ const logs = records.filter((record) => record.rawMessage === "original dispatch");
319
+ deepStrictEqual(logs.map((record) => record.properties.dispatchId), [...malformed.map(() => void 0), ...valid]);
320
+ for (const record of logs) strictEqual(record.properties.requestId, "current-request");
321
+ strictEqual(descendants.length, 2 * (malformed.length + valid.length));
322
+ for (const event of descendants) strictEqual(event.provenance.dispatchId, void 0);
323
+ } finally {
324
+ esm_default.hardReset();
325
+ await reset();
326
+ }
327
+ });
328
+ test("malformed queued provenance does not poison a retry", async () => {
329
+ const messages = [];
330
+ const federation = createFederation({
331
+ kv: new MemoryKvStore(),
332
+ allowPrivateAddress: true,
333
+ manuallyStartQueue: true,
334
+ outboxRetryPolicy: () => Temporal.Duration.from({ milliseconds: 1 }),
335
+ queue: { outbox: {
336
+ enqueue(message) {
337
+ messages.push(message);
338
+ return Promise.resolve();
339
+ },
340
+ listen: () => Promise.resolve()
341
+ } }
342
+ });
343
+ await federation.createContext(new URL("https://example.com"), void 0).sendActivity(sender, target, activity, { fanout: "skip" });
344
+ const original = messages[0];
345
+ ok(original.type === "outbox");
346
+ const malformed = {
347
+ ...original,
348
+ provenance: { inboxRequest: { inboxRequestId: "" } }
349
+ };
350
+ esm_default.mockGlobal().post(target.inboxId.href, 503);
351
+ try {
352
+ await federation.processQueuedTask(void 0, malformed);
353
+ strictEqual(messages.length, 2);
354
+ const retry = messages[1];
355
+ ok(retry.type === "outbox");
356
+ deepStrictEqual(retry.provenance, {});
357
+ strictEqual(retry.attempt, 1);
358
+ await federation.processQueuedTask(void 0, retry);
359
+ strictEqual(messages.length, 3);
360
+ const next = messages[2];
361
+ ok(next.type === "outbox");
362
+ deepStrictEqual(next.provenance, {});
363
+ } finally {
364
+ esm_default.hardReset();
365
+ }
366
+ });
367
+ //#endregion
368
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";
@@ -0,0 +1,171 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as projectSenderPayload } from "../outbound-document-BMgwLXTY.mjs";
5
+ import { n as FederationBuilderImpl, r as createFederationBuilder } from "../builder-B2H8GsyI.mjs";
6
+ import { test } from "@fedify/fixture";
7
+ import { deepStrictEqual, ok, strictEqual, throws } from "node:assert/strict";
8
+ //#region src/federation/outbound-document.test.ts
9
+ const as = "https://www.w3.org/ns/activitystreams";
10
+ const secret = "https://private.example/recipient";
11
+ test("sender payload projects nested compact expanded and proof-bearing blind fields", () => {
12
+ const source = {
13
+ "@context": [as, "https://w3id.org/security/data-integrity/v2"],
14
+ type: "Create",
15
+ bto: [secret],
16
+ object: {
17
+ bcc: [secret],
18
+ children: [{
19
+ "as:bto": secret,
20
+ [`${as}#bcc`]: secret,
21
+ content: "visible"
22
+ }],
23
+ proof: {
24
+ "@context": "https://w3id.org/identity/v1",
25
+ type: "DataIntegrityProof",
26
+ proofValue: "retained-proof",
27
+ "as:bcc": secret
28
+ }
29
+ },
30
+ signature: {
31
+ signatureValue: "retained-signature",
32
+ [`${as}#bto`]: secret
33
+ }
34
+ };
35
+ const original = structuredClone(source);
36
+ const result = projectSenderPayload(source);
37
+ ok(result.type === "available");
38
+ strictEqual(result.representation, "privacy-projection");
39
+ strictEqual(result.redacted, true);
40
+ strictEqual(JSON.stringify(result.value).includes(secret), false);
41
+ ok(JSON.stringify(result.value).includes("retained-proof"));
42
+ ok(JSON.stringify(result.value).includes("retained-signature"));
43
+ deepStrictEqual(source, original);
44
+ ok(Object.isFrozen(result));
45
+ ok(Object.isFrozen(result.value));
46
+ const value = result.value;
47
+ ok(Object.isFrozen(value.object));
48
+ source.object.children[0].content = "changed";
49
+ ok(JSON.stringify(result.value).includes("visible"));
50
+ const expanded = projectSenderPayload([{
51
+ "@type": [`${as}#Create`],
52
+ [`${as}#object`]: [{
53
+ [`${as}#bcc`]: [{ "@id": secret }],
54
+ [`${as}#content`]: [{ "@value": "visible" }]
55
+ }]
56
+ }]);
57
+ ok(expanded.type === "available");
58
+ strictEqual(expanded.redacted, true);
59
+ strictEqual(JSON.stringify(expanded.value).includes(secret), false);
60
+ });
61
+ test("sender payload fails closed without evaluating getters or JSON conversion", () => {
62
+ let getters = 0;
63
+ let conversions = 0;
64
+ const getter = {
65
+ "@context": as,
66
+ get object() {
67
+ getters++;
68
+ return "private";
69
+ }
70
+ };
71
+ const conversion = { "@context": as };
72
+ Object.defineProperty(conversion, "toJSON", { value() {
73
+ conversions++;
74
+ return {};
75
+ } });
76
+ const context = [as];
77
+ Object.defineProperty(context, "includes", {
78
+ get() {
79
+ getters++;
80
+ throw new Error("must not run");
81
+ },
82
+ enumerable: true
83
+ });
84
+ const cycle = { "@context": as };
85
+ cycle.object = cycle;
86
+ let deep = "leaf";
87
+ for (let i = 0; i < 65; i++) deep = { object: deep };
88
+ for (const source of [
89
+ {
90
+ "@context": [as, { private: `${as}#bcc` }],
91
+ private: secret
92
+ },
93
+ {
94
+ "@context": as,
95
+ object: {
96
+ "@context": { as: "https://other.example/" },
97
+ "as:bcc": secret
98
+ }
99
+ },
100
+ {
101
+ "@context": [as, "https://unknown.example/context"],
102
+ private: secret
103
+ },
104
+ {
105
+ "@context": [as, { sensitive: "as:bcc" }],
106
+ sensitive: secret
107
+ },
108
+ {
109
+ "@context": [as, {
110
+ toot: `${as}#`,
111
+ Emoji: "toot:bcc"
112
+ }],
113
+ Emoji: secret
114
+ },
115
+ {
116
+ type: "Create",
117
+ object: secret
118
+ },
119
+ getter,
120
+ conversion,
121
+ { "@context": context },
122
+ cycle,
123
+ {
124
+ "@context": as,
125
+ object: deep
126
+ },
127
+ {
128
+ "@context": as,
129
+ object: NaN
130
+ },
131
+ {
132
+ "@context": as,
133
+ object: /* @__PURE__ */ new Date()
134
+ },
135
+ {
136
+ "@context": as,
137
+ object: [void 0]
138
+ }
139
+ ]) deepStrictEqual(projectSenderPayload(source), {
140
+ stage: "sender-payload",
141
+ type: "unavailable",
142
+ reason: "privacy-filter-unavailable"
143
+ });
144
+ strictEqual(getters, 0);
145
+ strictEqual(conversions, 0);
146
+ });
147
+ test("sender payload opt-in validates atomically and retains builder selection", () => {
148
+ const builder = createFederationBuilder();
149
+ ok(builder instanceof FederationBuilderImpl);
150
+ const config = {
151
+ senderPayload: true,
152
+ onAttempt() {}
153
+ };
154
+ builder.setOutboundObservers(config);
155
+ const original = builder.outboundObservers;
156
+ config.senderPayload = false;
157
+ strictEqual(original.senderPayload, true);
158
+ for (const value of [
159
+ null,
160
+ 1,
161
+ "true",
162
+ {}
163
+ ]) {
164
+ throws(() => builder.setOutboundObservers({ senderPayload: value }), TypeError);
165
+ strictEqual(builder.outboundObservers, original);
166
+ }
167
+ builder.setOutboundObservers({});
168
+ strictEqual(builder.outboundObservers.senderPayload, false);
169
+ });
170
+ //#endregion
171
+ export {};
@@ -0,0 +1,2 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import { URLPattern } from "urlpattern-polyfill";