@oxy.so/federation 2.2.1 → 2.4.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.
- package/README.md +31 -0
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/apUri.js +4 -2
- package/dist/cjs/httpSignature.js +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/networkIdentity.js +3 -2
- package/dist/cjs/node/actorResolver.js +24 -20
- package/dist/cjs/node/actorRouter.js +16 -6
- package/dist/cjs/node/delivery.js +35 -10
- package/dist/cjs/node/httpCacheControl.js +11 -0
- package/dist/cjs/node/inboundDispatch.js +4 -3
- package/dist/cjs/node/index.js +8 -1
- package/dist/cjs/node/tcoLinks.js +97 -0
- package/dist/cjs/node/webfingerRouter.js +6 -4
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/apUri.js +4 -2
- package/dist/esm/httpSignature.js +1 -1
- package/dist/esm/index.js +3 -3
- package/dist/esm/networkIdentity.js +3 -2
- package/dist/esm/node/actorResolver.js +24 -20
- package/dist/esm/node/actorRouter.js +16 -6
- package/dist/esm/node/delivery.js +33 -10
- package/dist/esm/node/httpCacheControl.js +8 -0
- package/dist/esm/node/inboundDispatch.js +4 -3
- package/dist/esm/node/index.js +6 -1
- package/dist/esm/node/tcoLinks.js +94 -0
- package/dist/esm/node/webfingerRouter.js +6 -4
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/httpSignature.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/node/actorRouter.d.ts +2 -0
- package/dist/types/node/delivery.d.ts +23 -4
- package/dist/types/node/httpCacheControl.d.ts +2 -0
- package/dist/types/node/index.d.ts +6 -1
- package/dist/types/node/tcoLinks.d.ts +41 -0
- package/dist/types/node/webfingerRouter.d.ts +2 -0
- package/package.json +2 -3
- package/src/__tests__/actorCollectionCounts.test.ts +27 -18
- package/src/__tests__/actorObject.test.ts +27 -7
- package/src/__tests__/actorResolver.test.ts +37 -22
- package/src/__tests__/actorResolverNetworkIdentity.test.ts +73 -36
- package/src/__tests__/apUri.test.ts +9 -5
- package/src/__tests__/delivery.test.ts +165 -25
- package/src/__tests__/federationHost.test.ts +13 -3
- package/src/__tests__/httpSignature.test.ts +1 -3
- package/src/__tests__/inboundDispatch.test.ts +80 -30
- package/src/__tests__/networkIdentity.test.ts +240 -113
- package/src/__tests__/routers.test.ts +157 -27
- package/src/__tests__/tcoLinks.test.ts +21 -0
- package/src/actorObject.ts +8 -3
- package/src/apUri.ts +6 -2
- package/src/httpSignature.ts +1 -1
- package/src/index.ts +8 -3
- package/src/networkIdentity.ts +14 -5
- package/src/node/actorResolver.ts +91 -50
- package/src/node/actorRouter.ts +44 -14
- package/src/node/delivery.ts +112 -30
- package/src/node/httpCacheControl.ts +8 -0
- package/src/node/identityBridge.ts +38 -18
- package/src/node/inboundDispatch.ts +33 -13
- package/src/node/index.ts +13 -0
- package/src/node/signedFetch.ts +3 -1
- package/src/node/tcoLinks.ts +114 -0
- package/src/node/webfingerRouter.ts +8 -4
|
@@ -50,7 +50,8 @@ function makeWebfingerApp(overrides: Partial<WebfingerRouterConfig> = {}): Expre
|
|
|
50
50
|
resolveUser: async (username) => (username === 'alice' ? { _id: 'u-alice' } : null),
|
|
51
51
|
consent: {
|
|
52
52
|
isSharingEnabledFromUser: () => true,
|
|
53
|
-
getSharingStateByUsername: async (username) =>
|
|
53
|
+
getSharingStateByUsername: async (username) =>
|
|
54
|
+
username === 'alice' ? 'enabled' : 'unknown-user',
|
|
54
55
|
},
|
|
55
56
|
cache: { get: async () => null, set: () => {} },
|
|
56
57
|
logger: { error: () => {} },
|
|
@@ -60,10 +61,12 @@ function makeWebfingerApp(overrides: Partial<WebfingerRouterConfig> = {}): Expre
|
|
|
60
61
|
return app;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function makeActorApp(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
function makeActorApp(
|
|
65
|
+
overrides: {
|
|
66
|
+
actorConfig?: Partial<ActorRouterConfig>;
|
|
67
|
+
onDispatch?: (activity: Record<string, unknown>, verifiedActorUri: string) => void;
|
|
68
|
+
} = {},
|
|
69
|
+
): Express {
|
|
67
70
|
const app = express();
|
|
68
71
|
app.use(
|
|
69
72
|
express.json({
|
|
@@ -80,17 +83,30 @@ function makeActorApp(overrides: {
|
|
|
80
83
|
apContentType: 'application/activity+json',
|
|
81
84
|
urls,
|
|
82
85
|
wantsActivityPub,
|
|
83
|
-
getPublicKey: async () => ({
|
|
84
|
-
|
|
86
|
+
getPublicKey: async () => ({
|
|
87
|
+
keyId: `${urls.actor('alice')}#main-key`,
|
|
88
|
+
publicKeyPem: 'LOCALPEM',
|
|
89
|
+
}),
|
|
90
|
+
resolveUser: async (username) =>
|
|
91
|
+
username === 'alice'
|
|
92
|
+
? { _id: 'u-alice', name: { displayName: 'Alice' }, _count: { followers: 3, following: 1 } }
|
|
93
|
+
: null,
|
|
85
94
|
consent: {
|
|
86
95
|
isSharingEnabledFromUser: () => true,
|
|
87
|
-
getSharingStateByUsername: async (username) =>
|
|
96
|
+
getSharingStateByUsername: async (username) =>
|
|
97
|
+
username === 'alice' ? 'enabled' : 'unknown-user',
|
|
88
98
|
},
|
|
89
|
-
buildLocalActorObject: (params) => ({
|
|
99
|
+
buildLocalActorObject: (params) => ({
|
|
100
|
+
id: urls.actor(params.username),
|
|
101
|
+
type: 'Person',
|
|
102
|
+
name: params.displayName,
|
|
103
|
+
}),
|
|
90
104
|
getBanner: async () => null,
|
|
91
105
|
inbound: {
|
|
92
106
|
fetchPublicKey: async (keyId) =>
|
|
93
|
-
keyId === REMOTE_KEY_ID
|
|
107
|
+
keyId === REMOTE_KEY_ID
|
|
108
|
+
? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR }
|
|
109
|
+
: null,
|
|
94
110
|
trustForwardedHost: true,
|
|
95
111
|
enqueueInboxActivity: async () => false, // force the inline dispatch path
|
|
96
112
|
processInboxActivity: async (activity, verifiedActorUri) => {
|
|
@@ -107,24 +123,82 @@ function makeActorApp(overrides: {
|
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
describe('webfinger router', () => {
|
|
126
|
+
it('keeps the existing one-hour HTTP caching default', async () => {
|
|
127
|
+
const response = await request(makeWebfingerApp())
|
|
128
|
+
.get('/.well-known/webfinger')
|
|
129
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
130
|
+
expect(response.headers['cache-control']).toBe('max-age=3600');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 86401])(
|
|
134
|
+
'rejects invalid HTTP cache TTL %s at construction',
|
|
135
|
+
(httpCacheTtlSeconds) => {
|
|
136
|
+
expect(() => makeWebfingerApp({ httpCacheTtlSeconds })).toThrow(RangeError);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
it('makes fresh, cached and instance discovery noncacheable when TTL is zero', async () => {
|
|
141
|
+
const cacheGet = jest.fn(async () => null as { subject: string; links: [] } | null);
|
|
142
|
+
const app = makeWebfingerApp({
|
|
143
|
+
httpCacheTtlSeconds: 0,
|
|
144
|
+
cache: { get: cacheGet, set: () => {} },
|
|
145
|
+
});
|
|
146
|
+
const fresh = await request(app)
|
|
147
|
+
.get('/.well-known/webfinger')
|
|
148
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
149
|
+
expect(fresh.status).toBe(200);
|
|
150
|
+
expect(fresh.headers['cache-control']).toBe('no-store');
|
|
151
|
+
cacheGet.mockResolvedValue({ subject: 'acct:alice@mention.earth', links: [] });
|
|
152
|
+
const cached = await request(app)
|
|
153
|
+
.get('/.well-known/webfinger')
|
|
154
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
155
|
+
expect(cached.status).toBe(200);
|
|
156
|
+
expect(cached.headers['cache-control']).toBe('no-store');
|
|
157
|
+
expect(cached.body.links).toEqual([]);
|
|
158
|
+
const instance = await request(app)
|
|
159
|
+
.get('/.well-known/webfinger')
|
|
160
|
+
.query({ resource: 'acct:instance@mention.earth' });
|
|
161
|
+
expect(instance.status).toBe(200);
|
|
162
|
+
expect(instance.headers['cache-control']).toBe('no-store');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('accepts the documented maximum HTTP cache TTL', async () => {
|
|
166
|
+
const response = await request(makeWebfingerApp({ httpCacheTtlSeconds: 86400 }))
|
|
167
|
+
.get('/.well-known/webfinger')
|
|
168
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
169
|
+
expect(response.headers['cache-control']).toBe('max-age=86400');
|
|
170
|
+
});
|
|
171
|
+
|
|
110
172
|
it('resolves acct:alice@mention.earth to the actor self link (200)', async () => {
|
|
111
|
-
const res = await request(makeWebfingerApp())
|
|
173
|
+
const res = await request(makeWebfingerApp())
|
|
174
|
+
.get('/.well-known/webfinger')
|
|
175
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
112
176
|
expect(res.status).toBe(200);
|
|
113
177
|
expect(res.body.subject).toBe('acct:alice@mention.earth');
|
|
114
|
-
expect(res.body.links[0]).toEqual({
|
|
178
|
+
expect(res.body.links[0]).toEqual({
|
|
179
|
+
rel: 'self',
|
|
180
|
+
type: 'application/activity+json',
|
|
181
|
+
href: urls.actor('alice'),
|
|
182
|
+
});
|
|
115
183
|
});
|
|
116
184
|
|
|
117
185
|
it('normalizes mixed-case acct local-parts before resolve', async () => {
|
|
118
|
-
const resolveUser = jest.fn(async (username: string) =>
|
|
186
|
+
const resolveUser = jest.fn(async (username: string) =>
|
|
187
|
+
username === 'alice' ? { _id: 'u-alice' } : null,
|
|
188
|
+
);
|
|
119
189
|
const app = makeWebfingerApp({ resolveUser });
|
|
120
|
-
const res = await request(app)
|
|
190
|
+
const res = await request(app)
|
|
191
|
+
.get('/.well-known/webfinger')
|
|
192
|
+
.query({ resource: 'acct:Alice@mention.earth' });
|
|
121
193
|
expect(res.status).toBe(200);
|
|
122
194
|
expect(resolveUser).toHaveBeenCalledWith('alice');
|
|
123
195
|
expect(res.body.subject).toBe('acct:alice@mention.earth');
|
|
124
196
|
});
|
|
125
197
|
|
|
126
198
|
it('404s an unknown domain', async () => {
|
|
127
|
-
const res = await request(makeWebfingerApp())
|
|
199
|
+
const res = await request(makeWebfingerApp())
|
|
200
|
+
.get('/.well-known/webfinger')
|
|
201
|
+
.query({ resource: 'acct:alice@other.example' });
|
|
128
202
|
expect(res.status).toBe(404);
|
|
129
203
|
});
|
|
130
204
|
|
|
@@ -138,9 +212,14 @@ describe('webfinger router', () => {
|
|
|
138
212
|
|
|
139
213
|
it('404s a sharing-disabled user indistinguishably', async () => {
|
|
140
214
|
const app = makeWebfingerApp({
|
|
141
|
-
consent: {
|
|
215
|
+
consent: {
|
|
216
|
+
isSharingEnabledFromUser: () => false,
|
|
217
|
+
getSharingStateByUsername: async () => 'disabled',
|
|
218
|
+
},
|
|
142
219
|
});
|
|
143
|
-
const res = await request(app)
|
|
220
|
+
const res = await request(app)
|
|
221
|
+
.get('/.well-known/webfinger')
|
|
222
|
+
.query({ resource: 'acct:alice@mention.earth' });
|
|
144
223
|
expect(res.status).toBe(404);
|
|
145
224
|
expect(res.body).toEqual({ error: 'User not found' });
|
|
146
225
|
});
|
|
@@ -193,9 +272,7 @@ describe('webfinger router — the instance (server) actor', () => {
|
|
|
193
272
|
expect(webfinger.status).toBe(200);
|
|
194
273
|
expect(actor.status).toBe(200);
|
|
195
274
|
|
|
196
|
-
const selfLink = webfinger.body.links.find(
|
|
197
|
-
(link: { rel: string }) => link.rel === 'self',
|
|
198
|
-
);
|
|
275
|
+
const selfLink = webfinger.body.links.find((link: { rel: string }) => link.rel === 'self');
|
|
199
276
|
expect(selfLink).toBeDefined();
|
|
200
277
|
// Positive control: neither side may be vacuously undefined.
|
|
201
278
|
expect(typeof actor.body.id).toBe('string');
|
|
@@ -298,6 +375,30 @@ describe('webfinger router — the instance (server) actor', () => {
|
|
|
298
375
|
});
|
|
299
376
|
|
|
300
377
|
describe('actor router — GET /ap/users/:username', () => {
|
|
378
|
+
it('keeps the existing half-hour actor caching default', async () => {
|
|
379
|
+
const response = await request(makeActorApp())
|
|
380
|
+
.get('/ap/users/alice')
|
|
381
|
+
.set('Accept', 'application/activity+json');
|
|
382
|
+
expect(response.status).toBe(200);
|
|
383
|
+
expect(response.headers['cache-control']).toBe('max-age=1800');
|
|
384
|
+
});
|
|
385
|
+
it.each(['alice', 'instance'])(
|
|
386
|
+
'serves %s without HTTP caching when TTL is zero',
|
|
387
|
+
async (username) => {
|
|
388
|
+
const response = await request(makeActorApp({ actorConfig: { httpCacheTtlSeconds: 0 } }))
|
|
389
|
+
.get(`/ap/users/${username}`)
|
|
390
|
+
.set('Accept', 'application/activity+json');
|
|
391
|
+
expect(response.status).toBe(200);
|
|
392
|
+
expect(response.headers['cache-control']).toBe('no-store');
|
|
393
|
+
},
|
|
394
|
+
);
|
|
395
|
+
it.each([-1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 86401])(
|
|
396
|
+
'rejects invalid actor cache TTL %s',
|
|
397
|
+
(httpCacheTtlSeconds) => {
|
|
398
|
+
expect(() => makeActorApp({ actorConfig: { httpCacheTtlSeconds } })).toThrow(RangeError);
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
|
|
301
402
|
it('serves the actor as 200 (NOT a redirect) for an AP Accept', async () => {
|
|
302
403
|
const res = await request(makeActorApp())
|
|
303
404
|
.get('/ap/users/alice')
|
|
@@ -333,17 +434,34 @@ describe('actor router — GET /ap/users/:username', () => {
|
|
|
333
434
|
|
|
334
435
|
describe('actor router — inbox POST', () => {
|
|
335
436
|
/** Build a validly-signed inbox request body + headers for `activity`. */
|
|
336
|
-
async function signedInbox(
|
|
437
|
+
async function signedInbox(
|
|
438
|
+
path: string,
|
|
439
|
+
activity: Record<string, unknown>,
|
|
440
|
+
): Promise<{ body: string; headers: Record<string, string> }> {
|
|
337
441
|
const body = JSON.stringify(activity);
|
|
338
|
-
const headers = await signRequest(
|
|
442
|
+
const headers = await signRequest(
|
|
443
|
+
sign,
|
|
444
|
+
REMOTE_KEY_ID,
|
|
445
|
+
'POST',
|
|
446
|
+
`https://${DOMAIN}${path}`,
|
|
447
|
+
body,
|
|
448
|
+
);
|
|
339
449
|
return { body, headers };
|
|
340
450
|
}
|
|
341
451
|
|
|
342
452
|
it('accepts a validly-signed activity (202) and reaches the dispatcher — no redirect', async () => {
|
|
343
453
|
const dispatched: Array<{ activity: Record<string, unknown>; verifiedActorUri: string }> = [];
|
|
344
|
-
const app = makeActorApp({
|
|
454
|
+
const app = makeActorApp({
|
|
455
|
+
onDispatch: (activity, verifiedActorUri) => dispatched.push({ activity, verifiedActorUri }),
|
|
456
|
+
});
|
|
345
457
|
|
|
346
|
-
const activity = {
|
|
458
|
+
const activity = {
|
|
459
|
+
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
460
|
+
id: 'https://remote.example/f/1',
|
|
461
|
+
type: 'Follow',
|
|
462
|
+
actor: REMOTE_ACTOR,
|
|
463
|
+
object: urls.actor('alice'),
|
|
464
|
+
};
|
|
347
465
|
const { body, headers } = await signedInbox('/ap/inbox', activity);
|
|
348
466
|
|
|
349
467
|
const res = await request(app)
|
|
@@ -399,7 +517,9 @@ describe('actor router — inbox POST', () => {
|
|
|
399
517
|
actorConfig: {
|
|
400
518
|
inbound: {
|
|
401
519
|
fetchPublicKey: async (keyId) =>
|
|
402
|
-
keyId === REMOTE_KEY_ID
|
|
520
|
+
keyId === REMOTE_KEY_ID
|
|
521
|
+
? { publicKeyPem: REMOTE_PUBLIC_PEM, actorUri: REMOTE_ACTOR }
|
|
522
|
+
: null,
|
|
403
523
|
trustForwardedHost: true,
|
|
404
524
|
enqueueInboxActivity: async () => false,
|
|
405
525
|
processInboxActivity: dispatcher.processInboxActivity,
|
|
@@ -436,7 +556,12 @@ describe('actor router — inbox POST', () => {
|
|
|
436
556
|
});
|
|
437
557
|
|
|
438
558
|
it('rejects an actor mismatch with 403', async () => {
|
|
439
|
-
const activity = {
|
|
559
|
+
const activity = {
|
|
560
|
+
id: 'https://remote.example/f/2',
|
|
561
|
+
type: 'Follow',
|
|
562
|
+
actor: 'https://evil.example/users/x',
|
|
563
|
+
object: urls.actor('alice'),
|
|
564
|
+
};
|
|
440
565
|
const { body, headers } = await signedInbox('/ap/inbox', activity);
|
|
441
566
|
const res = await request(makeActorApp())
|
|
442
567
|
.post('/ap/inbox')
|
|
@@ -448,7 +573,12 @@ describe('actor router — inbox POST', () => {
|
|
|
448
573
|
});
|
|
449
574
|
|
|
450
575
|
it('404s a user inbox for a sharing-disabled user (before signature verify)', async () => {
|
|
451
|
-
const activity = {
|
|
576
|
+
const activity = {
|
|
577
|
+
id: 'https://remote.example/f/3',
|
|
578
|
+
type: 'Follow',
|
|
579
|
+
actor: REMOTE_ACTOR,
|
|
580
|
+
object: urls.actor('ghost'),
|
|
581
|
+
};
|
|
452
582
|
const { body, headers } = await signedInbox('/ap/users/ghost/inbox', activity);
|
|
453
583
|
const res = await request(makeActorApp())
|
|
454
584
|
.post('/ap/users/ghost/inbox')
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createTcoLinkExpander } from '../node/tcoLinks';
|
|
2
|
+
import type { SingleHopFetch } from '../node/signedFetch';
|
|
3
|
+
|
|
4
|
+
const fetchSingleHop: SingleHopFetch = async (url) => {
|
|
5
|
+
const location = { 'https://t.co/abc123': 'https://example.com/', 'https://t.co/self12': url }[
|
|
6
|
+
url
|
|
7
|
+
];
|
|
8
|
+
return location
|
|
9
|
+
? new Response(null, { status: 301, headers: { location } })
|
|
10
|
+
: new Response(null, { status: 404 });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
it('replaces a t.co wrapper with its target and leaves anything else as written', async () => {
|
|
14
|
+
const expand = createTcoLinkExpander({ fetchSingleHop, userAgent: 'test' });
|
|
15
|
+
|
|
16
|
+
await expect(
|
|
17
|
+
expand('a https://t.co/abc123 b https://t.co/gone12 c https://t.co/self12 d https://bit.ly/x'),
|
|
18
|
+
).resolves.toBe(
|
|
19
|
+
'a https://example.com/ b https://t.co/gone12 c https://t.co/self12 d https://bit.ly/x',
|
|
20
|
+
);
|
|
21
|
+
});
|
package/src/actorObject.ts
CHANGED
|
@@ -257,7 +257,9 @@ function buildActorIcon(
|
|
|
257
257
|
if (!avatar) return undefined;
|
|
258
258
|
const resolved = config.media.resolveAvatar(avatar);
|
|
259
259
|
if (!resolved || !isAbsoluteHttpUrl(resolved)) {
|
|
260
|
-
config.onWarn?.(
|
|
260
|
+
config.onWarn?.(
|
|
261
|
+
`[Federation] Omitting actor icon — avatar did not resolve to an absolute URL (ref: ${avatar})`,
|
|
262
|
+
);
|
|
261
263
|
return undefined;
|
|
262
264
|
}
|
|
263
265
|
return apImageObject(resolved);
|
|
@@ -275,7 +277,9 @@ function buildActorImage(
|
|
|
275
277
|
if (!banner) return undefined;
|
|
276
278
|
const resolved = config.media.resolveBanner(banner);
|
|
277
279
|
if (!resolved || !isAbsoluteHttpUrl(resolved)) {
|
|
278
|
-
config.onWarn?.(
|
|
280
|
+
config.onWarn?.(
|
|
281
|
+
`[Federation] Omitting actor image — banner did not resolve to an absolute URL (ref: ${banner})`,
|
|
282
|
+
);
|
|
279
283
|
return undefined;
|
|
280
284
|
}
|
|
281
285
|
return apImageObject(resolved);
|
|
@@ -309,7 +313,8 @@ export function normalizeAlsoKnownAs(values: readonly string[] | null | undefine
|
|
|
309
313
|
*/
|
|
310
314
|
export function createLocalActorBuilder(config: LocalActorBuilderConfig): LocalActorBuilder {
|
|
311
315
|
return (params: BuildLocalActorParams): Record<string, unknown> => {
|
|
312
|
-
const { username, displayName, kind, bio, avatar, profileHeaderImage, publicKey, createdAt } =
|
|
316
|
+
const { username, displayName, kind, bio, avatar, profileHeaderImage, publicKey, createdAt } =
|
|
317
|
+
params;
|
|
313
318
|
|
|
314
319
|
const actorObject: Record<string, unknown> = {
|
|
315
320
|
id: config.urls.actor(username),
|
package/src/apUri.ts
CHANGED
|
@@ -135,7 +135,9 @@ export function createDomainPolicy(config: DomainPolicyConfig): DomainPolicy {
|
|
|
135
135
|
canonicalFederationHost(config.domain),
|
|
136
136
|
canonicalFederationHost(config.actorDomain ?? config.domain),
|
|
137
137
|
]);
|
|
138
|
-
const identityApex = config.identityApex
|
|
138
|
+
const identityApex = config.identityApex
|
|
139
|
+
? canonicalFederationHost(config.identityApex)
|
|
140
|
+
: undefined;
|
|
139
141
|
const blocked = new Set<string>();
|
|
140
142
|
for (const d of config.blockedDomains ?? []) {
|
|
141
143
|
blocked.add(canonicalFederationHost(d));
|
|
@@ -144,7 +146,9 @@ export function createDomainPolicy(config: DomainPolicyConfig): DomainPolicy {
|
|
|
144
146
|
return {
|
|
145
147
|
isBlockedDomain(domain: string): boolean {
|
|
146
148
|
const d = canonicalFederationHost(domain);
|
|
147
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
localDomains.has(d) || (identityApex !== undefined && d === identityApex) || blocked.has(d)
|
|
151
|
+
);
|
|
148
152
|
},
|
|
149
153
|
extractLocalPostId(objectUri: string): string | null {
|
|
150
154
|
let parsed: URL;
|
package/src/httpSignature.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Mention runs behind a CF-proxied apex that rewrites the origin `Host`, so it
|
|
17
17
|
* passes `true`; a directly-exposed origin leaves it `false`.
|
|
18
18
|
*
|
|
19
|
-
* Lives in the isomorphic `.` entry (no Express /
|
|
19
|
+
* Lives in the isomorphic `.` entry (no Express / database driver): it depends only on
|
|
20
20
|
* the runtime `crypto` builtin (Node / Bun) and is never invoked from browser /
|
|
21
21
|
* React-Native bundles — RN consumers import only the connector TYPES, which are
|
|
22
22
|
* erased at compile time.
|
package/src/index.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* connector produces, the local-event union connectors deliver outbound, and
|
|
9
9
|
* the connector interface itself.
|
|
10
10
|
*
|
|
11
|
-
* IMPORTANT: this entry is intentionally free of
|
|
12
|
-
* Native so it can be imported from any Oxy app backend (and, in later phases,
|
|
11
|
+
* IMPORTANT: this entry is intentionally free of database drivers / Express /
|
|
12
|
+
* React Native so it can be imported from any Oxy app backend (and, in later phases,
|
|
13
13
|
* share the pure HTTP-signature + actor-object surface with browser/isomorphic
|
|
14
14
|
* callers). The runnable Express/Node engine — signed fetch, delivery transport,
|
|
15
15
|
* webfinger/actor/inbox routers, remote-actor resolution — lives under the
|
|
@@ -43,7 +43,12 @@ export {
|
|
|
43
43
|
* Domain-parameterized ActivityPub URL builders — each app instantiates them once
|
|
44
44
|
* with its own `FEDERATION_DOMAIN` so every actor stays `@user@its-own-domain`.
|
|
45
45
|
*/
|
|
46
|
-
export {
|
|
46
|
+
export {
|
|
47
|
+
createUrlBuilders,
|
|
48
|
+
normalizeActorUsername,
|
|
49
|
+
INSTANCE_ACTOR_USERNAME,
|
|
50
|
+
type UrlBuilders,
|
|
51
|
+
} from './urls';
|
|
47
52
|
|
|
48
53
|
/**
|
|
49
54
|
* Network identity: the MECHANISM for re-labelling an account republished by a
|
package/src/networkIdentity.ts
CHANGED
|
@@ -168,7 +168,8 @@ export function parseUpstreamProfileUrl(
|
|
|
168
168
|
|
|
169
169
|
const host = canonicalFederationHost(url.hostname);
|
|
170
170
|
for (const network of networks) {
|
|
171
|
-
if (!network.profileHosts.some((allowed) => canonicalFederationHost(allowed) === host))
|
|
171
|
+
if (!network.profileHosts.some((allowed) => canonicalFederationHost(allowed) === host))
|
|
172
|
+
continue;
|
|
172
173
|
const handle = profileUrlHandle(url.href, network.profileHosts, network.profilePathPrefix);
|
|
173
174
|
if (handle !== undefined && handle.length > 0) return { network, handle };
|
|
174
175
|
}
|
|
@@ -457,10 +458,16 @@ export function upstreamHandleFromProfileField(options: {
|
|
|
457
458
|
// Observed bird.makeup Official assertion, 2026-09-13:
|
|
458
459
|
// https://https://twitter.com/jordievole. Repair exactly one duplicated
|
|
459
460
|
// HTTPS prefix, then run the ordinary host/path/credential validation.
|
|
460
|
-
const candidateHref =
|
|
461
|
-
|
|
461
|
+
const candidateHref =
|
|
462
|
+
options.repairRepeatedHttpsScheme && href.startsWith('https://https://')
|
|
463
|
+
? href.slice('https://'.length)
|
|
464
|
+
: href;
|
|
462
465
|
let handle: string | undefined;
|
|
463
|
-
try {
|
|
466
|
+
try {
|
|
467
|
+
handle = profileUrlHandle(candidateHref, options.hosts, prefix);
|
|
468
|
+
} catch {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
464
471
|
if (handle !== undefined && handle.length > 0) handles.set(handle.toLowerCase(), handle);
|
|
465
472
|
}
|
|
466
473
|
}
|
|
@@ -681,7 +688,9 @@ export function createBridgeRelabeller(
|
|
|
681
688
|
vouchesForNetwork: (actorHost, networkDomain) => {
|
|
682
689
|
const bridge = findBridge(actorHost);
|
|
683
690
|
if (!bridge) return false;
|
|
684
|
-
return
|
|
691
|
+
return (
|
|
692
|
+
canonicalFederationHost(bridge.network.domain) === canonicalFederationHost(networkDomain)
|
|
693
|
+
);
|
|
685
694
|
},
|
|
686
695
|
deriveNetworkIdentity: (candidate) => {
|
|
687
696
|
const entry = findBridge(candidate.host);
|