@hatk/hatk 0.0.1-alpha.83 → 0.0.1-alpha.85
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/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +201 -140
- package/dist/database/dialect.js +2 -2
- package/dist/database/fts.d.ts.map +1 -1
- package/dist/database/fts.js +6 -2
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +6 -1
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -1
- package/dist/indexer.d.ts +4 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +81 -64
- package/dist/oauth/service-describe.d.ts +18 -7
- package/dist/oauth/service-describe.d.ts.map +1 -1
- package/dist/oauth/service-describe.js +52 -5
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +16 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +68 -5
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +1 -1
- package/package.json +4 -2
package/dist/hooks.d.ts
CHANGED
|
@@ -65,6 +65,14 @@ export declare function defineHook(event: 'on-commit', options: {
|
|
|
65
65
|
/**
|
|
66
66
|
* Discover and load the on-login hook from the project's `hooks/` directory.
|
|
67
67
|
* Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
|
|
68
|
+
*
|
|
69
|
+
* The documented contract is `export default defineHook('on-login', handler)`,
|
|
70
|
+
* which is an object carrying the handler — the same shape the `server/`
|
|
71
|
+
* scanner recognizes by its `__type` tag. A bare async function is still
|
|
72
|
+
* accepted, because that is what the legacy `hooks/` directory used before
|
|
73
|
+
* `defineHook` existed. Anything else is reported rather than installed: a
|
|
74
|
+
* non-callable hook would otherwise fail silently on every login, since
|
|
75
|
+
* `fireOnLoginHook` swallows what the hook throws.
|
|
68
76
|
*/
|
|
69
77
|
export declare function loadOnLoginHook(hooksDir: string): Promise<void>;
|
|
70
78
|
/** Register a hook from a scanned server/ module. */
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks.js
CHANGED
|
@@ -63,6 +63,14 @@ const onCommitHooks = [];
|
|
|
63
63
|
/**
|
|
64
64
|
* Discover and load the on-login hook from the project's `hooks/` directory.
|
|
65
65
|
* Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
|
|
66
|
+
*
|
|
67
|
+
* The documented contract is `export default defineHook('on-login', handler)`,
|
|
68
|
+
* which is an object carrying the handler — the same shape the `server/`
|
|
69
|
+
* scanner recognizes by its `__type` tag. A bare async function is still
|
|
70
|
+
* accepted, because that is what the legacy `hooks/` directory used before
|
|
71
|
+
* `defineHook` existed. Anything else is reported rather than installed: a
|
|
72
|
+
* non-callable hook would otherwise fail silently on every login, since
|
|
73
|
+
* `fireOnLoginHook` swallows what the hook throws.
|
|
66
74
|
*/
|
|
67
75
|
export async function loadOnLoginHook(hooksDir) {
|
|
68
76
|
const tsPath = resolve(hooksDir, 'on-login.ts');
|
|
@@ -71,9 +79,24 @@ export async function loadOnLoginHook(hooksDir) {
|
|
|
71
79
|
if (!path)
|
|
72
80
|
return;
|
|
73
81
|
const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${path}?t=${Date.now()}`));
|
|
74
|
-
|
|
82
|
+
const handler = resolveHookHandler(mod.default);
|
|
83
|
+
if (!handler) {
|
|
84
|
+
emit('hooks', 'on_login_load_error', {
|
|
85
|
+
path,
|
|
86
|
+
error: 'default export is not a function or a defineHook("on-login", ...) result',
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
onLoginHook = handler;
|
|
75
91
|
log('[hooks] on-login hook loaded');
|
|
76
92
|
}
|
|
93
|
+
/** Unwrap a hook module's default export into the callable handler, if it has one. */
|
|
94
|
+
function resolveHookHandler(exported) {
|
|
95
|
+
if (typeof exported === 'function')
|
|
96
|
+
return exported;
|
|
97
|
+
const handler = exported?.handler;
|
|
98
|
+
return typeof handler === 'function' ? handler : null;
|
|
99
|
+
}
|
|
77
100
|
/** Mark a DID as pending, trigger auto-backfill, and wait for completion. */
|
|
78
101
|
async function ensureRepo(did) {
|
|
79
102
|
await setRepoStatus(did, 'pending');
|
package/dist/indexer.d.ts
CHANGED
|
@@ -33,9 +33,10 @@ export declare function resumeCursor(liveSeq: number | null, bootCursor: string
|
|
|
33
33
|
/** Reset module cursor state between tests. */
|
|
34
34
|
export declare function _resetCursorStateForTests(): void;
|
|
35
35
|
/**
|
|
36
|
-
* Drain the write buffer
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Drain the write buffer instead of waiting out FLUSH_INTERVAL_MS. Lets
|
|
37
|
+
* end-to-end tests assert on rows immediately after feeding a frame — deletes
|
|
38
|
+
* included, since they are buffered alongside puts and so are covered by the
|
|
39
|
+
* same await.
|
|
39
40
|
*/
|
|
40
41
|
export declare function _flushForTests(): Promise<void>;
|
|
41
42
|
/**
|
package/dist/indexer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"indexer.d.ts","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"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"}
|
package/dist/indexer.js
CHANGED
|
@@ -45,9 +45,10 @@ let indexerMaxRetries;
|
|
|
45
45
|
let indexerPlcUrl;
|
|
46
46
|
let maxConcurrentBackfills = 3;
|
|
47
47
|
/**
|
|
48
|
-
* Flush the write buffer —
|
|
49
|
-
* run label rules on inserted records, and
|
|
50
|
-
* threshold is reached. Emits a wide event
|
|
48
|
+
* Flush the write buffer — apply all buffered puts and deletes in arrival
|
|
49
|
+
* order, update the relay cursor, run label rules on inserted records, and
|
|
50
|
+
* trigger FTS rebuilds when the write threshold is reached. Emits a wide event
|
|
51
|
+
* with batch stats.
|
|
51
52
|
*/
|
|
52
53
|
async function flushBuffer() {
|
|
53
54
|
if (buffer.length === 0)
|
|
@@ -55,14 +56,38 @@ async function flushBuffer() {
|
|
|
55
56
|
const elapsed = timer();
|
|
56
57
|
const batch = buffer.splice(0);
|
|
57
58
|
let insertedCount = 0;
|
|
59
|
+
let deletedCount = 0;
|
|
58
60
|
const errors = [];
|
|
59
61
|
let cursorError;
|
|
60
62
|
const inserted = [];
|
|
63
|
+
const applied = [];
|
|
64
|
+
// Strictly sequential: two writes to the same URI in one batch must land in
|
|
65
|
+
// the order they arrived, so nothing here may run concurrently.
|
|
61
66
|
for (const item of batch) {
|
|
62
67
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
if (item.action === 'delete') {
|
|
69
|
+
await deleteRecord(item.collection, item.uri);
|
|
70
|
+
deletedCount++;
|
|
71
|
+
applied.push({
|
|
72
|
+
action: 'delete',
|
|
73
|
+
collection: item.collection,
|
|
74
|
+
uri: item.uri,
|
|
75
|
+
authorDid: item.authorDid,
|
|
76
|
+
record: null,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
await insertRecord(item.collection, item.uri, item.cid, item.authorDid, item.record);
|
|
81
|
+
insertedCount++;
|
|
82
|
+
inserted.push(item);
|
|
83
|
+
applied.push({
|
|
84
|
+
action: 'create',
|
|
85
|
+
collection: item.collection,
|
|
86
|
+
uri: item.uri,
|
|
87
|
+
authorDid: item.authorDid,
|
|
88
|
+
record: item.record,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
66
91
|
}
|
|
67
92
|
catch (err) {
|
|
68
93
|
errors.push(err.message);
|
|
@@ -88,14 +113,9 @@ async function flushBuffer() {
|
|
|
88
113
|
value: item.record,
|
|
89
114
|
}).catch(() => { });
|
|
90
115
|
}
|
|
91
|
-
// Fire on-commit hooks for
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
collection: item.collection,
|
|
95
|
-
uri: item.uri,
|
|
96
|
-
authorDid: item.authorDid,
|
|
97
|
-
record: item.record,
|
|
98
|
-
})));
|
|
116
|
+
// Fire on-commit hooks for everything the batch applied, in the order it was
|
|
117
|
+
// applied (async, non-blocking)
|
|
118
|
+
fireOnCommitHooks(applied);
|
|
99
119
|
// Aggregate collection counts and unique DIDs for wide event
|
|
100
120
|
const collections = {};
|
|
101
121
|
const dids = new Set();
|
|
@@ -106,6 +126,7 @@ async function flushBuffer() {
|
|
|
106
126
|
emit('indexer', 'flush', {
|
|
107
127
|
batch_size: batch.length,
|
|
108
128
|
inserted_count: insertedCount,
|
|
129
|
+
deleted_count: deletedCount,
|
|
109
130
|
error_count: errors.length,
|
|
110
131
|
cursor_seq: lastSeq,
|
|
111
132
|
duration_ms: elapsed(),
|
|
@@ -125,16 +146,32 @@ async function flushBuffer() {
|
|
|
125
146
|
}
|
|
126
147
|
}
|
|
127
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Run a flush once every flush already queued has finished.
|
|
151
|
+
*
|
|
152
|
+
* Nothing awaits the flush a full batch triggers, and the interval timer can
|
|
153
|
+
* fire while that flush is still waiting on the database. Two flushes in flight
|
|
154
|
+
* hold disjoint batches but interleave their awaits, so a write in the later
|
|
155
|
+
* batch can reach the database before one in the earlier batch — which would
|
|
156
|
+
* give back exactly the cross-batch reordering the single buffer removes.
|
|
157
|
+
* Chaining them costs nothing on an idle indexer and keeps the queue global.
|
|
158
|
+
*/
|
|
159
|
+
let flushChain = Promise.resolve();
|
|
160
|
+
function enqueueFlush() {
|
|
161
|
+
const next = flushChain.then(() => flushBuffer(), () => flushBuffer());
|
|
162
|
+
flushChain = next;
|
|
163
|
+
return next;
|
|
164
|
+
}
|
|
128
165
|
/** Schedule a flush after FLUSH_INTERVAL_MS if one isn't already pending. */
|
|
129
166
|
function scheduleFlush() {
|
|
130
167
|
if (flushTimer)
|
|
131
168
|
return;
|
|
132
|
-
flushTimer = setTimeout(
|
|
169
|
+
flushTimer = setTimeout(() => {
|
|
133
170
|
flushTimer = null;
|
|
134
|
-
|
|
171
|
+
void enqueueFlush().catch(() => { });
|
|
135
172
|
}, FLUSH_INTERVAL_MS);
|
|
136
173
|
}
|
|
137
|
-
/** Add a
|
|
174
|
+
/** Add a write to the buffer. Flushes immediately if BATCH_SIZE is reached. */
|
|
138
175
|
function bufferWrite(item) {
|
|
139
176
|
buffer.push(item);
|
|
140
177
|
if (buffer.length >= BATCH_SIZE) {
|
|
@@ -142,7 +179,7 @@ function bufferWrite(item) {
|
|
|
142
179
|
clearTimeout(flushTimer);
|
|
143
180
|
flushTimer = null;
|
|
144
181
|
}
|
|
145
|
-
|
|
182
|
+
void enqueueFlush().catch(() => { });
|
|
146
183
|
}
|
|
147
184
|
else {
|
|
148
185
|
scheduleFlush();
|
|
@@ -196,16 +233,17 @@ export function _resetCursorStateForTests() {
|
|
|
196
233
|
cursorKey = 'relay';
|
|
197
234
|
}
|
|
198
235
|
/**
|
|
199
|
-
* Drain the write buffer
|
|
200
|
-
*
|
|
201
|
-
*
|
|
236
|
+
* Drain the write buffer instead of waiting out FLUSH_INTERVAL_MS. Lets
|
|
237
|
+
* end-to-end tests assert on rows immediately after feeding a frame — deletes
|
|
238
|
+
* included, since they are buffered alongside puts and so are covered by the
|
|
239
|
+
* same await.
|
|
202
240
|
*/
|
|
203
241
|
export async function _flushForTests() {
|
|
204
242
|
if (flushTimer) {
|
|
205
243
|
clearTimeout(flushTimer);
|
|
206
244
|
flushTimer = null;
|
|
207
245
|
}
|
|
208
|
-
await
|
|
246
|
+
await enqueueFlush();
|
|
209
247
|
}
|
|
210
248
|
/**
|
|
211
249
|
* Auto-backfill a DID's repo when first seen on the firehose.
|
|
@@ -262,7 +300,14 @@ export async function triggerAutoBackfill(did, attempt = 0) {
|
|
|
262
300
|
backfillInFlight.delete(did);
|
|
263
301
|
for (const item of buffered) {
|
|
264
302
|
try {
|
|
265
|
-
|
|
303
|
+
if (item.action === 'delete') {
|
|
304
|
+
// A delete that arrived mid-backfill has to be replayed after the CAR
|
|
305
|
+
// export lands, or the export's snapshot of the record outlives it.
|
|
306
|
+
await deleteRecord(item.collection, item.uri);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
await insertRecord(item.collection, item.uri, item.cid, item.authorDid, item.record);
|
|
310
|
+
}
|
|
266
311
|
}
|
|
267
312
|
catch {
|
|
268
313
|
replayErrors++;
|
|
@@ -295,28 +340,6 @@ export async function triggerAutoBackfill(did, attempt = 0) {
|
|
|
295
340
|
}, delayMs);
|
|
296
341
|
}
|
|
297
342
|
}
|
|
298
|
-
/** Emit a memory diagnostics wide event every 30s for observability. */
|
|
299
|
-
function startMemoryDiagnostics() {
|
|
300
|
-
setInterval(() => {
|
|
301
|
-
const mem = process.memoryUsage();
|
|
302
|
-
let pendingBufferItems = 0;
|
|
303
|
-
for (const [, items] of pendingBuffers) {
|
|
304
|
-
pendingBufferItems += items.length;
|
|
305
|
-
}
|
|
306
|
-
emit('diagnostics', 'memory', {
|
|
307
|
-
heap_used_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
|
308
|
-
heap_total_mb: Math.round(mem.heapTotal / 1024 / 1024),
|
|
309
|
-
rss_mb: Math.round(mem.rss / 1024 / 1024),
|
|
310
|
-
external_mb: Math.round(mem.external / 1024 / 1024),
|
|
311
|
-
array_buffers_mb: Math.round(mem.arrayBuffers / 1024 / 1024),
|
|
312
|
-
write_buffer_len: buffer.length,
|
|
313
|
-
pending_buffer_dids: pendingBuffers.size,
|
|
314
|
-
pending_buffer_items: pendingBufferItems,
|
|
315
|
-
backfill_in_flight: backfillInFlight.size,
|
|
316
|
-
repo_status_cache_size: repoStatusCache.size,
|
|
317
|
-
});
|
|
318
|
-
}, 30_000);
|
|
319
|
-
}
|
|
320
343
|
/**
|
|
321
344
|
* Connect to the AT Protocol relay firehose and begin indexing.
|
|
322
345
|
*
|
|
@@ -346,7 +369,6 @@ export async function configureIndexer(opts) {
|
|
|
346
369
|
}
|
|
347
370
|
log(`[indexer] Warmed repo status cache with ${statuses.length} entries`);
|
|
348
371
|
}
|
|
349
|
-
// startMemoryDiagnostics()
|
|
350
372
|
// Checkpoint the cursor on a timer regardless of write activity (see
|
|
351
373
|
// checkpointCursor). Guarded so reconnects don't stack intervals; unref'd so
|
|
352
374
|
// it never keeps the process alive.
|
|
@@ -558,19 +580,21 @@ export function applyCommit(did, ops) {
|
|
|
558
580
|
// For non-signal ops (e.g. profile updates), only process if this DID is already tracked
|
|
559
581
|
if (!hasSignalOp && repoStatus === null)
|
|
560
582
|
return;
|
|
583
|
+
/** Queue a write behind anything already queued for this DID. */
|
|
584
|
+
const enqueue = (item) => {
|
|
585
|
+
// If DID is mid-backfill, buffer instead of writing directly
|
|
586
|
+
if (pendingBuffers.has(did))
|
|
587
|
+
pendingBuffers.get(did).push(item);
|
|
588
|
+
else
|
|
589
|
+
bufferWrite(item);
|
|
590
|
+
};
|
|
561
591
|
for (const op of ops) {
|
|
562
592
|
const uri = `at://${did}/${op.collection}/${op.rkey}`;
|
|
563
593
|
if (op.action === 'delete') {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
collection: op.collection,
|
|
569
|
-
uri,
|
|
570
|
-
authorDid: did,
|
|
571
|
-
record: null,
|
|
572
|
-
},
|
|
573
|
-
]);
|
|
594
|
+
// Buffered, not applied here: see {@link WriteBuffer}. The on-commit hook
|
|
595
|
+
// fires from the flush too, so a handler never sees a delete announced
|
|
596
|
+
// before the row is actually gone.
|
|
597
|
+
enqueue({ action: 'delete', collection: op.collection, uri, authorDid: did });
|
|
574
598
|
continue;
|
|
575
599
|
}
|
|
576
600
|
const record = op.record;
|
|
@@ -588,14 +612,7 @@ export function applyCommit(did, ops) {
|
|
|
588
612
|
});
|
|
589
613
|
continue;
|
|
590
614
|
}
|
|
591
|
-
|
|
592
|
-
// If DID is mid-backfill, buffer instead of writing directly
|
|
593
|
-
if (pendingBuffers.has(did)) {
|
|
594
|
-
pendingBuffers.get(did).push(item);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
bufferWrite(item);
|
|
598
|
-
}
|
|
615
|
+
enqueue({ action: 'put', collection: op.collection, uri, cid: op.cid, authorDid: did, record });
|
|
599
616
|
}
|
|
600
617
|
}
|
|
601
618
|
/**
|
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
* What a PDS says it serves, and the scopes worth asking it for.
|
|
3
3
|
*
|
|
4
4
|
* `community.lexicon.service.describe` answers with a service's roles and every
|
|
5
|
-
* XRPC method it routes. It is the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* XRPC method it routes. It is the direct question and the answer to trust when
|
|
6
|
+
* it comes — but it is still a proposal, and implementations exist that serve
|
|
7
|
+
* the features while serving no describe. The reference spaces build of
|
|
8
|
+
* `@atproto/pds` is one: it routes every space method and does not contain the
|
|
9
|
+
* describe NSID at all. Reading its silence as "no optional features" denies it
|
|
10
|
+
* scopes it could honor.
|
|
11
|
+
*
|
|
12
|
+
* So when describe says nothing, the methods a caller actually cares about are
|
|
13
|
+
* probed directly. Calling one and reading the failure cannot tell a missing
|
|
14
|
+
* feature from a broken server *on its own* — but it can against a control: ask
|
|
15
|
+
* for an NSID that certainly does not exist, learn how this server says "no such
|
|
16
|
+
* method", and a different answer for a real method is evidence it has one.
|
|
8
17
|
*
|
|
9
18
|
* Used before pushing an authorization request, so a PDS is asked for a
|
|
10
19
|
* feature's scopes only when it implements the feature.
|
|
11
20
|
*/
|
|
12
21
|
import type { OAuthConfig } from '../config.ts';
|
|
13
22
|
/**
|
|
14
|
-
* Every XRPC method a PDS serves
|
|
15
|
-
*
|
|
16
|
-
*
|
|
23
|
+
* Every XRPC method a PDS serves, as far as it can be established.
|
|
24
|
+
*
|
|
25
|
+
* `candidates` are the methods the caller needs an answer about. They are only
|
|
26
|
+
* used when describe says nothing — an explicit list is always preferred to
|
|
27
|
+
* inference, and a server that answers describe is taken at its word.
|
|
17
28
|
*/
|
|
18
|
-
export declare function describeMethods(pdsEndpoint: string): Promise<Set<string>>;
|
|
29
|
+
export declare function describeMethods(pdsEndpoint: string, candidates?: string[]): Promise<Set<string>>;
|
|
19
30
|
/**
|
|
20
31
|
* The scope string to request from this PDS: `base`, plus any conditional set
|
|
21
32
|
* whose `whenMethod` the server serves.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-describe.d.ts","sourceRoot":"","sources":["../../src/oauth/service-describe.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"service-describe.d.ts","sourceRoot":"","sources":["../../src/oauth/service-describe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAkD/C;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CA+B1G;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,MAAM,CAAC,CAajB;AAED,+CAA+C;AAC/C,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -4,12 +4,53 @@ const PROBE_TIMEOUT_MS = 5000;
|
|
|
4
4
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
5
5
|
/** Answers are a property of the server, so one entry covers every account on it. */
|
|
6
6
|
const cache = new Map();
|
|
7
|
+
/** An NSID no server routes, for learning how one says "no such method". */
|
|
8
|
+
const CONTROL_NSID = 'dev.hatk.unspecced.methodThatDoesNotExist';
|
|
9
|
+
/** How a server answered, as a comparable string: status and error code. */
|
|
10
|
+
async function signature(pdsEndpoint, nsid) {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(`${pdsEndpoint.replace(/\/$/, '')}/xrpc/${nsid}`, {
|
|
13
|
+
headers: { accept: 'application/json' },
|
|
14
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
15
|
+
});
|
|
16
|
+
const body = (await res.json().catch(() => null));
|
|
17
|
+
return `${res.status}:${body?.error ?? ''}`;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
24
|
+
* Which of `candidates` this server appears to route, judged against how it
|
|
25
|
+
* answers for a method that does not exist.
|
|
26
|
+
*
|
|
27
|
+
* Conservative by construction. A server that answers identically either way —
|
|
28
|
+
* bsky.social returns `401 AuthMissing` for both — yields nothing, so a feature
|
|
29
|
+
* it does not have is never claimed. The cost is failing to see support that
|
|
30
|
+
* cannot be distinguished, which is where this started anyway.
|
|
11
31
|
*/
|
|
12
|
-
|
|
32
|
+
async function probeMethods(pdsEndpoint, candidates) {
|
|
33
|
+
const control = await signature(pdsEndpoint, CONTROL_NSID);
|
|
34
|
+
// Unreachable is not evidence about the server, so claim nothing.
|
|
35
|
+
if (control === null)
|
|
36
|
+
return new Set();
|
|
37
|
+
const answers = await Promise.all(candidates.map((n) => signature(pdsEndpoint, n)));
|
|
38
|
+
const found = new Set();
|
|
39
|
+
candidates.forEach((nsid, i) => {
|
|
40
|
+
const a = answers[i];
|
|
41
|
+
if (a !== null && a !== control)
|
|
42
|
+
found.add(nsid);
|
|
43
|
+
});
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every XRPC method a PDS serves, as far as it can be established.
|
|
48
|
+
*
|
|
49
|
+
* `candidates` are the methods the caller needs an answer about. They are only
|
|
50
|
+
* used when describe says nothing — an explicit list is always preferred to
|
|
51
|
+
* inference, and a server that answers describe is taken at its word.
|
|
52
|
+
*/
|
|
53
|
+
export async function describeMethods(pdsEndpoint, candidates = []) {
|
|
13
54
|
const cached = cache.get(pdsEndpoint);
|
|
14
55
|
if (cached && cached.expires > Date.now())
|
|
15
56
|
return cached.methods;
|
|
@@ -32,6 +73,12 @@ export async function describeMethods(pdsEndpoint) {
|
|
|
32
73
|
catch {
|
|
33
74
|
methods = new Set();
|
|
34
75
|
}
|
|
76
|
+
// No describe, or one that listed nothing: fall back to asking about the
|
|
77
|
+
// methods this caller actually needs. Only those — a probe per method is a
|
|
78
|
+
// request per method, and the conditional sets name two.
|
|
79
|
+
if (methods.size === 0 && candidates.length > 0) {
|
|
80
|
+
methods = await probeMethods(pdsEndpoint, candidates);
|
|
81
|
+
}
|
|
35
82
|
cache.set(pdsEndpoint, { methods, expires: Date.now() + CACHE_TTL_MS });
|
|
36
83
|
return methods;
|
|
37
84
|
}
|
|
@@ -48,7 +95,7 @@ export async function negotiateScope(config, base, pdsEndpoint, isLoopback) {
|
|
|
48
95
|
const conditional = config.conditionalScopes ?? [];
|
|
49
96
|
if (conditional.length === 0 || !pdsEndpoint || isLoopback)
|
|
50
97
|
return base;
|
|
51
|
-
const methods = await describeMethods(pdsEndpoint);
|
|
98
|
+
const methods = await describeMethods(pdsEndpoint, conditional.map((c) => c.whenMethod));
|
|
52
99
|
const granted = conditional.filter((c) => methods.has(c.whenMethod)).flatMap((c) => c.scopes);
|
|
53
100
|
if (granted.length === 0)
|
|
54
101
|
return base;
|
package/dist/opengraph.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opengraph.d.ts","sourceRoot":"","sources":["../src/opengraph.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC3B,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAAA;QAC3C,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KACnB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACpD;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,UAAU,CAAA;IACnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,CAAA;IAC5D,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAChD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC;;;oBAA7C,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC;EAEnG;
|
|
1
|
+
{"version":3,"file":"opengraph.d.ts","sourceRoot":"","sources":["../src/opengraph.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC3B,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAAA;QAC3C,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KACnB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACpD;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,UAAU,CAAA;IACnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,CAAA;IAC5D,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAChD;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC;;;oBAA7C,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC;EAEnG;AAiDD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyEhE;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;CAC9D,GAAG,IAAI,CAqDP;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BrF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyC3E"}
|
package/dist/opengraph.js
CHANGED
|
@@ -32,6 +32,20 @@ let defaultFont = null;
|
|
|
32
32
|
const cache = new Map();
|
|
33
33
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
34
34
|
const CACHE_MAX = 200;
|
|
35
|
+
/**
|
|
36
|
+
* Read a font file as an ArrayBuffer holding *only* that font's bytes.
|
|
37
|
+
*
|
|
38
|
+
* `readFileSync` returns a Buffer that, for small files, is a view into Node's
|
|
39
|
+
* shared 64 KB allocation pool — so `buffer.buffer` is the whole pool, not the
|
|
40
|
+
* font, and the font generally starts at a nonzero `byteOffset`. Handing that
|
|
41
|
+
* to satori makes it read whatever happened to be pooled before the font and
|
|
42
|
+
* reject the result with "Unsupported OpenType signature". Slice out the
|
|
43
|
+
* view's own range instead.
|
|
44
|
+
*/
|
|
45
|
+
function readFontData(fontPath) {
|
|
46
|
+
const fontData = readFileSync(fontPath);
|
|
47
|
+
return fontData.buffer.slice(fontData.byteOffset, fontData.byteOffset + fontData.byteLength);
|
|
48
|
+
}
|
|
35
49
|
function compilePath(path) {
|
|
36
50
|
const paramNames = [];
|
|
37
51
|
const re = path.replace(/:([^/]+)/g, (_, name) => {
|
|
@@ -44,8 +58,7 @@ export async function initOpengraph(ogDir) {
|
|
|
44
58
|
// Load default font
|
|
45
59
|
try {
|
|
46
60
|
const fontPath = resolve(import.meta.dirname, '..', 'fonts', 'Inter-Regular.woff');
|
|
47
|
-
|
|
48
|
-
defaultFont = { name: 'Inter', data: fontData.buffer, weight: 400, style: 'normal' };
|
|
61
|
+
defaultFont = { name: 'Inter', data: readFontData(fontPath), weight: 400, style: 'normal' };
|
|
49
62
|
log('[opengraph] loaded default font: Inter');
|
|
50
63
|
}
|
|
51
64
|
catch {
|
|
@@ -121,8 +134,7 @@ export function registerOgHandler(ogMod) {
|
|
|
121
134
|
if (!defaultFont) {
|
|
122
135
|
try {
|
|
123
136
|
const fontPath = resolve(import.meta.dirname, '..', 'fonts', 'Inter-Regular.woff');
|
|
124
|
-
|
|
125
|
-
defaultFont = { name: 'Inter', data: fontData.buffer, weight: 400, style: 'normal' };
|
|
137
|
+
defaultFont = { name: 'Inter', data: readFontData(fontPath), weight: 400, style: 'normal' };
|
|
126
138
|
}
|
|
127
139
|
catch { }
|
|
128
140
|
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAsEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAsEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAyD9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAuM3F;AAgDD,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAE,WAAW,GAAG,IAAI,CAAA;IACzB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxF,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACtB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAwkC5F;AAGD,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EAAE,EACrB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,GAAE,MAAM,EAAO,EACrB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC5D,QAAQ,CAAC,EAAE,MAAM,IAAI,GACpB,OAAO,WAAW,EAAE,MAAM,CAG5B"}
|
package/dist/server.js
CHANGED
|
@@ -20,6 +20,36 @@ import { json, jsonError, cors, withCors, file, notFound } from "./response.js";
|
|
|
20
20
|
import { collectionFromUri, isPrivateCollection } from "./private-collections.js";
|
|
21
21
|
import { serve } from "./adapter.js";
|
|
22
22
|
import { renderPage } from "./renderer.js";
|
|
23
|
+
/**
|
|
24
|
+
* Parse a client-supplied `limit`, or null if it is not a usable one.
|
|
25
|
+
*
|
|
26
|
+
* `parseInt` alone lets `limit=abc` through as NaN and `limit=-5` through as a
|
|
27
|
+
* negative bound, both of which reach the database driver: the first as a
|
|
28
|
+
* datatype error, the second as "no limit at all" on SQLite. Neither is a
|
|
29
|
+
* server fault, so the caller turns a null into a 400.
|
|
30
|
+
*/
|
|
31
|
+
function parseLimit(raw, fallback) {
|
|
32
|
+
if (raw === null || raw === '')
|
|
33
|
+
return fallback;
|
|
34
|
+
if (!/^\d+$/.test(raw))
|
|
35
|
+
return null;
|
|
36
|
+
const value = Number(raw);
|
|
37
|
+
return value >= 1 ? value : null;
|
|
38
|
+
}
|
|
39
|
+
/** Whether `sort` names a column queryRecords can order this collection by. */
|
|
40
|
+
function isSortableField(collection, sort) {
|
|
41
|
+
if (sort === 'indexed_at')
|
|
42
|
+
return true;
|
|
43
|
+
const schema = getSchema(collection);
|
|
44
|
+
return !!schema?.columns.some((c) => c.originalName === sort || c.name === sort);
|
|
45
|
+
}
|
|
46
|
+
/** Narrow a client-supplied `order` to the two values that may reach ORDER BY. */
|
|
47
|
+
function parseOrder(raw) {
|
|
48
|
+
if (!raw)
|
|
49
|
+
return undefined;
|
|
50
|
+
const lowered = raw.toLowerCase();
|
|
51
|
+
return lowered === 'asc' || lowered === 'desc' ? lowered : null;
|
|
52
|
+
}
|
|
23
53
|
function scopeMissingResponse(acceptEncoding, handle) {
|
|
24
54
|
const res = withCors(json({ error: 'ScopeMissingError', ...(handle ? { handle } : {}) }, 401, acceptEncoding));
|
|
25
55
|
res.headers.append('Set-Cookie', clearSessionCookieHeader());
|
|
@@ -45,7 +75,11 @@ export function registerCoreHandlers(collections, oauth) {
|
|
|
45
75
|
if (!getSchema(collection))
|
|
46
76
|
throw new NotFoundError(`Unknown collection: ${collection}`);
|
|
47
77
|
const sort = params.sort || undefined;
|
|
48
|
-
|
|
78
|
+
if (sort && !isSortableField(collection, sort))
|
|
79
|
+
throw new InvalidRequestError(`Invalid sort field: ${sort}`);
|
|
80
|
+
const order = parseOrder(params.order);
|
|
81
|
+
if (order === null)
|
|
82
|
+
throw new InvalidRequestError(`Invalid order: ${params.order}`);
|
|
49
83
|
const reserved = new Set(['collection', 'limit', 'cursor', 'sort', 'order']);
|
|
50
84
|
const filters = {};
|
|
51
85
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -358,11 +392,23 @@ export function createHandler(config) {
|
|
|
358
392
|
return withCors(jsonError(404, `Unknown collection: ${collection}`, acceptEncoding));
|
|
359
393
|
if (!getSchema(collection))
|
|
360
394
|
return withCors(jsonError(404, `Unknown collection: ${collection}`, acceptEncoding));
|
|
361
|
-
|
|
395
|
+
// Every one of these is attacker-controlled, and queryRecords trusts
|
|
396
|
+
// what it is handed: `sort` and `order` reach the ORDER BY clause and a
|
|
397
|
+
// non-numeric `limit` reaches the driver. Rejecting them here keeps a
|
|
398
|
+
// bad query string a 400 instead of a 500 (or worse).
|
|
399
|
+
const limit = parseLimit(url.searchParams.get('limit'), 20);
|
|
400
|
+
if (limit === null)
|
|
401
|
+
return withCors(jsonError(400, 'Invalid limit parameter', acceptEncoding));
|
|
362
402
|
const cursor = url.searchParams.get('cursor') || undefined;
|
|
363
403
|
const sort = url.searchParams.get('sort') || undefined;
|
|
364
|
-
const order = (url.searchParams.get('order')
|
|
365
|
-
|
|
404
|
+
const order = parseOrder(url.searchParams.get('order'));
|
|
405
|
+
if (order === null)
|
|
406
|
+
return withCors(jsonError(400, `Invalid order: ${url.searchParams.get('order')}`, acceptEncoding));
|
|
407
|
+
if (sort && !isSortableField(collection, sort))
|
|
408
|
+
return withCors(jsonError(400, `Invalid sort field: ${sort}`, acceptEncoding));
|
|
409
|
+
// Collect field filters (everything except reserved params). An
|
|
410
|
+
// unknown field is ignored rather than rejected — queryRecords does
|
|
411
|
+
// the same, and clients rely on passing through extra params.
|
|
366
412
|
const reserved = new Set(['collection', 'limit', 'cursor', 'sort', 'order']);
|
|
367
413
|
const filters = {};
|
|
368
414
|
for (const [key, value] of url.searchParams) {
|
|
@@ -920,7 +966,24 @@ export function createHandler(config) {
|
|
|
920
966
|
}
|
|
921
967
|
if (!code)
|
|
922
968
|
return withCors(jsonError(400, 'Missing code', acceptEncoding));
|
|
923
|
-
|
|
969
|
+
let result;
|
|
970
|
+
try {
|
|
971
|
+
result = await handleCallback(oauth, code, state, iss);
|
|
972
|
+
}
|
|
973
|
+
catch (err) {
|
|
974
|
+
// Same convention as the token endpoint below: an error that names
|
|
975
|
+
// its own status keeps it, anything else is a genuine failure and
|
|
976
|
+
// falls through to the 500.
|
|
977
|
+
if (err instanceof OAuthError)
|
|
978
|
+
return withCors(json({ error: err.code, error_description: err.description }, err.status, acceptEncoding));
|
|
979
|
+
// A callback matching no pending authorization request is a stale
|
|
980
|
+
// redirect, not a server fault — the request expired while a login
|
|
981
|
+
// tab sat open, or the user came back to a login they already
|
|
982
|
+
// finished. Answer like the other client errors on this route.
|
|
983
|
+
if (err instanceof Error && err.message === 'No matching authorization request found')
|
|
984
|
+
return withCors(jsonError(400, err.message, acceptEncoding));
|
|
985
|
+
throw err;
|
|
986
|
+
}
|
|
924
987
|
const isSecure = requestOrigin.startsWith('https');
|
|
925
988
|
const handle = (await getRepoHandle(result.did)) ?? result.did;
|
|
926
989
|
const session = { did: result.did, handle };
|