@oxy.so/federation 1.0.0

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 (78) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/dist/cjs/.tsbuildinfo +1 -0
  4. package/dist/cjs/actorObject.js +216 -0
  5. package/dist/cjs/apContext.js +48 -0
  6. package/dist/cjs/apUri.js +132 -0
  7. package/dist/cjs/httpSignature.js +187 -0
  8. package/dist/cjs/index.js +99 -0
  9. package/dist/cjs/networkIdentity.js +487 -0
  10. package/dist/cjs/node/actorResolver.js +625 -0
  11. package/dist/cjs/node/actorRouter.js +307 -0
  12. package/dist/cjs/node/delivery.js +415 -0
  13. package/dist/cjs/node/identityBridge.js +133 -0
  14. package/dist/cjs/node/inboundDispatch.js +268 -0
  15. package/dist/cjs/node/index.js +63 -0
  16. package/dist/cjs/node/signedFetch.js +122 -0
  17. package/dist/cjs/node/webfingerRouter.js +166 -0
  18. package/dist/cjs/urls.js +55 -0
  19. package/dist/esm/.tsbuildinfo +1 -0
  20. package/dist/esm/actorObject.js +210 -0
  21. package/dist/esm/apContext.js +45 -0
  22. package/dist/esm/apUri.js +126 -0
  23. package/dist/esm/httpSignature.js +179 -0
  24. package/dist/esm/index.js +65 -0
  25. package/dist/esm/networkIdentity.js +472 -0
  26. package/dist/esm/node/actorResolver.js +620 -0
  27. package/dist/esm/node/actorRouter.js +304 -0
  28. package/dist/esm/node/delivery.js +412 -0
  29. package/dist/esm/node/identityBridge.js +130 -0
  30. package/dist/esm/node/inboundDispatch.js +263 -0
  31. package/dist/esm/node/index.js +51 -0
  32. package/dist/esm/node/signedFetch.js +119 -0
  33. package/dist/esm/node/webfingerRouter.js +163 -0
  34. package/dist/esm/urls.js +50 -0
  35. package/dist/types/.tsbuildinfo +1 -0
  36. package/dist/types/actorObject.d.ts +182 -0
  37. package/dist/types/apContext.d.ts +35 -0
  38. package/dist/types/apUri.d.ts +107 -0
  39. package/dist/types/httpSignature.d.ts +113 -0
  40. package/dist/types/index.d.ts +336 -0
  41. package/dist/types/networkIdentity.d.ts +509 -0
  42. package/dist/types/node/actorResolver.d.ts +287 -0
  43. package/dist/types/node/actorRouter.d.ts +108 -0
  44. package/dist/types/node/delivery.d.ts +248 -0
  45. package/dist/types/node/identityBridge.d.ts +84 -0
  46. package/dist/types/node/inboundDispatch.d.ts +156 -0
  47. package/dist/types/node/index.d.ts +51 -0
  48. package/dist/types/node/signedFetch.d.ts +74 -0
  49. package/dist/types/node/webfingerRouter.d.ts +62 -0
  50. package/dist/types/urls.d.ts +55 -0
  51. package/package.json +119 -0
  52. package/src/__tests__/actorObject.test.ts +258 -0
  53. package/src/__tests__/actorResolver.test.ts +252 -0
  54. package/src/__tests__/actorResolverNetworkIdentity.test.ts +297 -0
  55. package/src/__tests__/apUri.test.ts +53 -0
  56. package/src/__tests__/delivery.test.ts +432 -0
  57. package/src/__tests__/federationHost.test.ts +281 -0
  58. package/src/__tests__/httpSignature.test.ts +343 -0
  59. package/src/__tests__/inboundDispatch.test.ts +381 -0
  60. package/src/__tests__/index.test.ts +8 -0
  61. package/src/__tests__/networkIdentity.test.ts +525 -0
  62. package/src/__tests__/routers.test.ts +460 -0
  63. package/src/__tests__/urls.test.ts +26 -0
  64. package/src/actorObject.ts +313 -0
  65. package/src/apContext.ts +45 -0
  66. package/src/apUri.ts +161 -0
  67. package/src/httpSignature.ts +282 -0
  68. package/src/index.ts +419 -0
  69. package/src/networkIdentity.ts +731 -0
  70. package/src/node/actorResolver.ts +839 -0
  71. package/src/node/actorRouter.ts +438 -0
  72. package/src/node/delivery.ts +729 -0
  73. package/src/node/identityBridge.ts +230 -0
  74. package/src/node/inboundDispatch.ts +420 -0
  75. package/src/node/index.ts +136 -0
  76. package/src/node/signedFetch.ts +177 -0
  77. package/src/node/webfingerRouter.ts +226 -0
  78. package/src/urls.ts +71 -0
@@ -0,0 +1,381 @@
1
+ import { createDomainPolicy } from '../apUri';
2
+ import {
3
+ createInboundDispatcher,
4
+ ActorResolutionPendingError,
5
+ type InboundDispatcherConfig,
6
+ type InboundActivityValidation,
7
+ } from '../node/inboundDispatch';
8
+
9
+ /**
10
+ * Phase 4b inbound dispatch proof.
11
+ *
12
+ * The engine owns Follow / Accept / Undo(Follow) / Reject and hands every content
13
+ * verb to `onContentActivity`. This locks: a signed Follow bridges the Oxy edge +
14
+ * sends Accept; Undo(Follow) unbridges; Accept marks the outbound follow; a
15
+ * Create/Announce/Like/Delete/Update (and a non-follow Undo) route to
16
+ * `onContentActivity`; the consent gate drops an OFF owner's inbound Follow; and a
17
+ * follower actor that has not yet resolved to an Oxy user DEFERS (throws) for
18
+ * retry.
19
+ */
20
+
21
+ const LOCAL_ACTOR = 'https://mention.earth/ap/users/alice';
22
+ const REMOTE_ACTOR = 'https://remote.example/users/bob';
23
+
24
+ function makeRig(overrides: {
25
+ localUser?: { _id?: string | null; id?: string | null } | null;
26
+ sharingEnabled?: boolean;
27
+ followerOxyUserId?: string | null;
28
+ inboundFollow?: { _id: unknown; localUserId: string } | null;
29
+ actorOxyUserIdForUndo?: string | null;
30
+ validate?: (activity: Record<string, unknown>) => InboundActivityValidation;
31
+ blockedHosts?: string[];
32
+ } = {}) {
33
+ const bridgeFollowCalls: Array<[string, string]> = [];
34
+ const bridgeUnfollowCalls: Array<[string, string]> = [];
35
+ const acceptsSent: Array<{ localOxyUserId: string; localUsername: string; followActivityId: string; remoteActorUri: string }> = [];
36
+ const inboundAcceptedUpserts: Array<{ localUserId: string; remoteActorUri: string; activityId: string }> = [];
37
+ const deletedFollowIds: unknown[] = [];
38
+ const contentActivities: Array<{ type: unknown; verifiedActorUri: string }> = [];
39
+ const outboundAcceptedByActivityId: Array<[string, string]> = [];
40
+ const outboundAcceptedAnyPending: string[] = [];
41
+ const outboundRejected: Array<[string, string | undefined]> = [];
42
+ const onInboundFollowAcceptedCalls: Array<[string, string, string]> = [];
43
+ const onOutboundFollowAcceptedCalls: string[] = [];
44
+ const domainPolicy =
45
+ overrides.blockedHosts === undefined
46
+ ? createDomainPolicy({ domain: 'mention.earth' })
47
+ : createDomainPolicy({ domain: 'mention.earth', blockedDomains: overrides.blockedHosts });
48
+ const validatedActivities: Array<Record<string, unknown>> = [];
49
+
50
+ const config: InboundDispatcherConfig = {
51
+ isBlockedDomain: domainPolicy.isBlockedDomain,
52
+ validateActivity: (activity) => {
53
+ validatedActivities.push(activity);
54
+ if (overrides.validate) return overrides.validate(activity);
55
+ const type = typeof activity.type === 'string' ? activity.type : undefined;
56
+ return type ? { ok: true, type } : { ok: false, summary: 'no type' };
57
+ },
58
+ identity: {
59
+ resolveUserByUsername: async () =>
60
+ overrides.localUser === undefined ? { _id: 'u-alice' } : overrides.localUser,
61
+ bridgeFollow: async (follower, local) => {
62
+ bridgeFollowCalls.push([follower, local]);
63
+ },
64
+ bridgeUnfollow: async (follower, local) => {
65
+ bridgeUnfollowCalls.push([follower, local]);
66
+ },
67
+ },
68
+ consent: { isSharingEnabledFromUser: () => overrides.sharingEnabled ?? true },
69
+ actorResolver: {
70
+ getOrFetchActor: async () => ({
71
+ oxyUserId: 'followerOxyUserId' in overrides ? overrides.followerOxyUserId : 'u-bob',
72
+ }),
73
+ },
74
+ follows: {
75
+ upsertInboundAccepted: async (localUserId, remoteActorUri, activityId) => {
76
+ inboundAcceptedUpserts.push({ localUserId, remoteActorUri, activityId });
77
+ },
78
+ findInboundFollow: async () => overrides.inboundFollow ?? null,
79
+ deleteFollowById: async (id) => {
80
+ deletedFollowIds.push(id);
81
+ },
82
+ findActorOxyUserId: async () => overrides.actorOxyUserIdForUndo ?? null,
83
+ markOutboundAcceptedByActivityId: async (uri, activityId) => {
84
+ outboundAcceptedByActivityId.push([uri, activityId]);
85
+ return true;
86
+ },
87
+ markOutboundAcceptedAnyPending: async (uri) => {
88
+ outboundAcceptedAnyPending.push(uri);
89
+ return true;
90
+ },
91
+ markOutboundRejected: async (uri, activityId) => {
92
+ outboundRejected.push([uri, activityId]);
93
+ },
94
+ },
95
+ delivery: {
96
+ sendAccept: async (localOxyUserId, localUsername, followActivityId, remoteActorUri) => {
97
+ acceptsSent.push({ localOxyUserId, localUsername, followActivityId, remoteActorUri });
98
+ },
99
+ },
100
+ onInboundFollowAccepted: async (localUserId, followerOxyUserId, actorUri) => {
101
+ onInboundFollowAcceptedCalls.push([localUserId, followerOxyUserId, actorUri]);
102
+ },
103
+ onOutboundFollowAccepted: async (actorUri) => {
104
+ onOutboundFollowAcceptedCalls.push(actorUri);
105
+ },
106
+ onContentActivity: async (activity, verifiedActorUri) => {
107
+ contentActivities.push({ type: activity.type, verifiedActorUri });
108
+ },
109
+ logger: { debug: () => {}, info: () => {}, warn: () => {} },
110
+ };
111
+
112
+ return {
113
+ dispatcher: createInboundDispatcher(config),
114
+ validatedActivities,
115
+ bridgeFollowCalls,
116
+ bridgeUnfollowCalls,
117
+ acceptsSent,
118
+ inboundAcceptedUpserts,
119
+ deletedFollowIds,
120
+ contentActivities,
121
+ outboundAcceptedByActivityId,
122
+ outboundAcceptedAnyPending,
123
+ outboundRejected,
124
+ onInboundFollowAcceptedCalls,
125
+ onOutboundFollowAcceptedCalls,
126
+ };
127
+ }
128
+
129
+ describe('inbound Follow', () => {
130
+ it('bridges the Oxy edge, records the follow, sends Accept, and notifies', async () => {
131
+ const rig = makeRig();
132
+ await rig.dispatcher.processInboxActivity(
133
+ { type: 'Follow', id: 'f1', actor: REMOTE_ACTOR, object: LOCAL_ACTOR },
134
+ REMOTE_ACTOR,
135
+ );
136
+
137
+ expect(rig.bridgeFollowCalls).toEqual([['u-bob', 'u-alice']]);
138
+ expect(rig.inboundAcceptedUpserts).toEqual([{ localUserId: 'u-alice', remoteActorUri: REMOTE_ACTOR, activityId: 'f1' }]);
139
+ expect(rig.acceptsSent).toEqual([
140
+ { localOxyUserId: 'u-alice', localUsername: 'alice', followActivityId: 'f1', remoteActorUri: REMOTE_ACTOR },
141
+ ]);
142
+ expect(rig.onInboundFollowAcceptedCalls).toEqual([['u-alice', 'u-bob', REMOTE_ACTOR]]);
143
+ });
144
+
145
+ it('drops the Follow (no bridge/accept) when the target has sharing OFF', async () => {
146
+ const rig = makeRig({ sharingEnabled: false });
147
+ await rig.dispatcher.processInboxActivity(
148
+ { type: 'Follow', id: 'f1', actor: REMOTE_ACTOR, object: LOCAL_ACTOR },
149
+ REMOTE_ACTOR,
150
+ );
151
+ expect(rig.bridgeFollowCalls).toHaveLength(0);
152
+ expect(rig.acceptsSent).toHaveLength(0);
153
+ });
154
+
155
+ it('defers (throws) when the follower actor has no Oxy user yet — for BullMQ retry', async () => {
156
+ const rig = makeRig({ followerOxyUserId: null });
157
+ await expect(
158
+ rig.dispatcher.processInboxActivity(
159
+ { type: 'Follow', id: 'f1', actor: REMOTE_ACTOR, object: LOCAL_ACTOR },
160
+ REMOTE_ACTOR,
161
+ ),
162
+ ).rejects.toBeInstanceOf(ActorResolutionPendingError);
163
+ expect(rig.bridgeFollowCalls).toHaveLength(0);
164
+ });
165
+
166
+ it('ignores a self-follow', async () => {
167
+ const rig = makeRig({ localUser: { _id: 'u-bob' }, followerOxyUserId: 'u-bob' });
168
+ await rig.dispatcher.processInboxActivity(
169
+ { type: 'Follow', id: 'f1', actor: REMOTE_ACTOR, object: LOCAL_ACTOR },
170
+ REMOTE_ACTOR,
171
+ );
172
+ expect(rig.bridgeFollowCalls).toHaveLength(0);
173
+ expect(rig.acceptsSent).toHaveLength(0);
174
+ });
175
+
176
+ it('drops a Follow for an unknown local user', async () => {
177
+ const rig = makeRig({ localUser: null });
178
+ await rig.dispatcher.processInboxActivity(
179
+ { type: 'Follow', id: 'f1', actor: REMOTE_ACTOR, object: LOCAL_ACTOR },
180
+ REMOTE_ACTOR,
181
+ );
182
+ expect(rig.bridgeFollowCalls).toHaveLength(0);
183
+ });
184
+ });
185
+
186
+ describe('inbound Undo(Follow)', () => {
187
+ it('unbridges the Oxy edge and deletes the follow row', async () => {
188
+ const rig = makeRig({
189
+ inboundFollow: { _id: 'row1', localUserId: 'u-alice' },
190
+ actorOxyUserIdForUndo: 'u-bob',
191
+ });
192
+ await rig.dispatcher.processInboxActivity(
193
+ { type: 'Undo', actor: REMOTE_ACTOR, object: { type: 'Follow', object: LOCAL_ACTOR } },
194
+ REMOTE_ACTOR,
195
+ );
196
+ expect(rig.bridgeUnfollowCalls).toEqual([['u-bob', 'u-alice']]);
197
+ expect(rig.deletedFollowIds).toEqual(['row1']);
198
+ expect(rig.contentActivities).toHaveLength(0);
199
+ });
200
+
201
+ it('is idempotent — no row means already processed (no unbridge)', async () => {
202
+ const rig = makeRig({ inboundFollow: null });
203
+ await rig.dispatcher.processInboxActivity(
204
+ { type: 'Undo', actor: REMOTE_ACTOR, object: { type: 'Follow', object: LOCAL_ACTOR } },
205
+ REMOTE_ACTOR,
206
+ );
207
+ expect(rig.bridgeUnfollowCalls).toHaveLength(0);
208
+ expect(rig.deletedFollowIds).toHaveLength(0);
209
+ });
210
+
211
+ it('routes Undo(Like) / Undo(Announce) to onContentActivity', async () => {
212
+ const rig = makeRig();
213
+ await rig.dispatcher.processInboxActivity(
214
+ { type: 'Undo', actor: REMOTE_ACTOR, object: { type: 'Like', object: 'https://x/1' } },
215
+ REMOTE_ACTOR,
216
+ );
217
+ expect(rig.contentActivities).toEqual([{ type: 'Undo', verifiedActorUri: REMOTE_ACTOR }]);
218
+ expect(rig.bridgeUnfollowCalls).toHaveLength(0);
219
+ });
220
+ });
221
+
222
+ describe('inbound Accept / Reject', () => {
223
+ it('marks the outbound follow accepted by activity id + triggers the outbox backfill', async () => {
224
+ const rig = makeRig();
225
+ await rig.dispatcher.processInboxActivity(
226
+ { type: 'Accept', actor: REMOTE_ACTOR, object: { type: 'Follow', id: 'https://mention.earth/ap/users/alice/follows/1' } },
227
+ REMOTE_ACTOR,
228
+ );
229
+ expect(rig.outboundAcceptedByActivityId).toEqual([[REMOTE_ACTOR, 'https://mention.earth/ap/users/alice/follows/1']]);
230
+ expect(rig.onOutboundFollowAcceptedCalls).toEqual([REMOTE_ACTOR]);
231
+ });
232
+
233
+ it('marks accepted (string ref, then any-pending fallback)', async () => {
234
+ const rig = makeRig();
235
+ await rig.dispatcher.processInboxActivity(
236
+ { type: 'Accept', actor: REMOTE_ACTOR, object: 'https://mention.earth/ap/users/alice/follows/1' },
237
+ REMOTE_ACTOR,
238
+ );
239
+ expect(rig.outboundAcceptedByActivityId).toEqual([[REMOTE_ACTOR, 'https://mention.earth/ap/users/alice/follows/1']]);
240
+ });
241
+
242
+ it('marks the outbound follow rejected on Reject(Follow)', async () => {
243
+ const rig = makeRig();
244
+ await rig.dispatcher.processInboxActivity(
245
+ { type: 'Reject', actor: REMOTE_ACTOR, object: { type: 'Follow', id: 'follow-1' } },
246
+ REMOTE_ACTOR,
247
+ );
248
+ expect(rig.outboundRejected).toEqual([[REMOTE_ACTOR, 'follow-1']]);
249
+ });
250
+ });
251
+
252
+ describe('content verbs → onContentActivity', () => {
253
+ it.each(['Create', 'Announce', 'Like', 'Delete', 'Update'])('routes %s to the app', async (type) => {
254
+ const rig = makeRig();
255
+ await rig.dispatcher.processInboxActivity({ type, actor: REMOTE_ACTOR }, REMOTE_ACTOR);
256
+ expect(rig.contentActivities).toEqual([{ type, verifiedActorUri: REMOTE_ACTOR }]);
257
+ expect(rig.bridgeFollowCalls).toHaveLength(0);
258
+ });
259
+
260
+ it('drops a malformed activity without dispatching', async () => {
261
+ const rig = makeRig({ validate: () => ({ ok: false, summary: 'bad shape' }) });
262
+ await rig.dispatcher.processInboxActivity({ type: 'Create' }, REMOTE_ACTOR);
263
+ expect(rig.contentActivities).toHaveLength(0);
264
+ });
265
+ });
266
+
267
+ /**
268
+ * Instance domain policy on the inbound path.
269
+ *
270
+ * A suspended instance must not be able to create ANYTHING here. The dispatcher is
271
+ * the one place every inbound transport and every verb converges, so this is where
272
+ * a blocked origin is refused — and, critically, it is refused on evidence the
273
+ * dispatcher holds itself (the verified origin URI) rather than on whether an actor
274
+ * happens to be cached, which is what made the blocklist inert before.
275
+ */
276
+ describe('blocked-origin domain policy', () => {
277
+ const BLOCKED_ACTOR = 'https://spam.example/users/mallory';
278
+
279
+ it.each(['Create', 'Announce', 'Like', 'Delete', 'Update'])(
280
+ 'drops %s from a blocked origin without reaching the app',
281
+ async (type) => {
282
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
283
+ await rig.dispatcher.processInboxActivity({ type, actor: BLOCKED_ACTOR }, BLOCKED_ACTOR);
284
+ expect(rig.contentActivities).toHaveLength(0);
285
+ },
286
+ );
287
+
288
+ it('drops a Follow from a blocked origin — no Oxy edge, no follow row, no Accept', async () => {
289
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
290
+ await rig.dispatcher.processInboxActivity(
291
+ { type: 'Follow', id: 'f1', actor: BLOCKED_ACTOR, object: LOCAL_ACTOR },
292
+ BLOCKED_ACTOR,
293
+ );
294
+ expect(rig.bridgeFollowCalls).toHaveLength(0);
295
+ expect(rig.inboundAcceptedUpserts).toHaveLength(0);
296
+ expect(rig.acceptsSent).toHaveLength(0);
297
+ expect(rig.onInboundFollowAcceptedCalls).toHaveLength(0);
298
+ });
299
+
300
+ it('drops an Accept from a blocked origin — no outbox backfill is triggered', async () => {
301
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
302
+ await rig.dispatcher.processInboxActivity(
303
+ { type: 'Accept', actor: BLOCKED_ACTOR, object: { type: 'Follow', id: 'follow-1' } },
304
+ BLOCKED_ACTOR,
305
+ );
306
+ expect(rig.outboundAcceptedByActivityId).toHaveLength(0);
307
+ expect(rig.onOutboundFollowAcceptedCalls).toHaveLength(0);
308
+ });
309
+
310
+ it('drops Undo(Follow) from a blocked origin — no unbridge or follow-row delete', async () => {
311
+ const rig = makeRig({
312
+ blockedHosts: ['spam.example'],
313
+ inboundFollow: { _id: 'row1', localUserId: 'u-alice' },
314
+ actorOxyUserIdForUndo: 'u-mallory',
315
+ });
316
+ await rig.dispatcher.processInboxActivity(
317
+ { type: 'Undo', actor: BLOCKED_ACTOR, object: { type: 'Follow', object: LOCAL_ACTOR } },
318
+ BLOCKED_ACTOR,
319
+ );
320
+ expect(rig.bridgeUnfollowCalls).toHaveLength(0);
321
+ expect(rig.deletedFollowIds).toHaveLength(0);
322
+ expect(rig.validatedActivities).toHaveLength(0);
323
+ });
324
+
325
+ it('drops Reject(Follow) from a blocked origin — no outbound rejection is recorded', async () => {
326
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
327
+ await rig.dispatcher.processInboxActivity(
328
+ { type: 'Reject', actor: BLOCKED_ACTOR, object: { type: 'Follow', id: 'follow-1' } },
329
+ BLOCKED_ACTOR,
330
+ );
331
+ expect(rig.outboundRejected).toHaveLength(0);
332
+ expect(rig.validatedActivities).toHaveLength(0);
333
+ });
334
+
335
+ it('blocks a www.-prefixed host via createDomainPolicy canonicalization', async () => {
336
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
337
+ const wwwActor = 'https://www.spam.example/users/mallory';
338
+ await rig.dispatcher.processInboxActivity({ type: 'Create', actor: wwwActor }, wwwActor);
339
+ expect(rig.contentActivities).toHaveLength(0);
340
+ expect(rig.validatedActivities).toHaveLength(0);
341
+ });
342
+
343
+ it('refuses the origin BEFORE the payload is parsed', async () => {
344
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
345
+ await rig.dispatcher.processInboxActivity({ type: 'Create', actor: BLOCKED_ACTOR }, BLOCKED_ACTOR);
346
+ expect(rig.validatedActivities).toHaveLength(0);
347
+ });
348
+
349
+ it('matches the host case-insensitively and ignores port/path/userinfo', async () => {
350
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
351
+ await rig.dispatcher.processInboxActivity(
352
+ { type: 'Create', actor: 'https://SPAM.Example:443/users/mallory' },
353
+ 'https://SPAM.Example:443/users/mallory',
354
+ );
355
+ expect(rig.contentActivities).toHaveLength(0);
356
+ });
357
+
358
+ it('fails closed on an origin URI with no parseable host', async () => {
359
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
360
+ await rig.dispatcher.processInboxActivity({ type: 'Create', actor: 'not-a-uri' }, 'not-a-uri');
361
+ expect(rig.contentActivities).toHaveLength(0);
362
+ expect(rig.validatedActivities).toHaveLength(0);
363
+ });
364
+
365
+ it('still dispatches an allowed origin while another host is blocked', async () => {
366
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
367
+ await rig.dispatcher.processInboxActivity({ type: 'Create', actor: REMOTE_ACTOR }, REMOTE_ACTOR);
368
+ expect(rig.contentActivities).toEqual([{ type: 'Create', verifiedActorUri: REMOTE_ACTOR }]);
369
+ });
370
+
371
+ it('keys the decision on the VERIFIED origin, not the actor the payload claims', async () => {
372
+ const rig = makeRig({ blockedHosts: ['spam.example'] });
373
+ // A blocked instance cannot buy itself a pass by naming an allowed actor in the
374
+ // body: the signature proved `BLOCKED_ACTOR`, and that is what is checked.
375
+ await rig.dispatcher.processInboxActivity(
376
+ { type: 'Create', actor: REMOTE_ACTOR },
377
+ BLOCKED_ACTOR,
378
+ );
379
+ expect(rig.contentActivities).toHaveLength(0);
380
+ });
381
+ });
@@ -0,0 +1,8 @@
1
+ import type { NetworkId } from '../index';
2
+
3
+ describe('@oxy.so/federation', () => {
4
+ it('accepts the supported network ids', () => {
5
+ const networks: NetworkId[] = ['activitypub', 'atproto'];
6
+ expect(networks).toEqual(['activitypub', 'atproto']);
7
+ });
8
+ });