@hatk/hatk 0.0.1-alpha.60 → 0.0.1-alpha.62
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/backfill.d.ts.map +1 -1
- package/dist/backfill.js +5 -10
- package/dist/dev-entry.d.ts.map +1 -1
- package/dist/dev-entry.js +1 -0
- package/dist/indexer.d.ts +1 -0
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +64 -8
- package/dist/main.js +1 -0
- package/dist/oauth/server.d.ts +14 -0
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +30 -9
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +14 -5
- package/package.json +1 -1
package/dist/backfill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backfill.d.ts","sourceRoot":"","sources":["../src/backfill.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,6CAA6C;AAC7C,UAAU,YAAY;IACpB,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAA;IACd,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAA;CACvB;
|
|
1
|
+
{"version":3,"file":"backfill.d.ts","sourceRoot":"","sources":["../src/backfill.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,6CAA6C;AAC7C,UAAU,YAAY;IACpB,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAA;IACd,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAA;CACvB;AA+FD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAkK/G;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAkIrE"}
|
package/dist/backfill.js
CHANGED
|
@@ -5,14 +5,14 @@ import { setRepoStatus, getRepoStatus, getRepoRev, getRepoRetryInfo, listRetryEl
|
|
|
5
5
|
import { emit, timer } from "./logger.js";
|
|
6
6
|
import { validateRecord } from '@bigmoves/lexicon';
|
|
7
7
|
import { getLexiconArray } from "./database/schema.js";
|
|
8
|
-
/** In-memory cache of DID → PDS resolution results to avoid redundant lookups. */
|
|
9
|
-
const pdsCache = new Map();
|
|
10
8
|
let plcUrl;
|
|
11
9
|
/**
|
|
12
10
|
* Resolves a DID to its PDS endpoint and handle by fetching the DID document.
|
|
13
11
|
*
|
|
14
12
|
* Supports both `did:web` (fetches `/.well-known/did.json`) and `did:plc`
|
|
15
|
-
* (fetches from the PLC directory).
|
|
13
|
+
* (fetches from the PLC directory). Always fetches fresh — DID docs change
|
|
14
|
+
* (handle renames, PDS migrations) and a stale cache silently rewrites stale
|
|
15
|
+
* handles back into `_repos` on every backfill.
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
18
|
* ```ts
|
|
@@ -22,9 +22,6 @@ let plcUrl;
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
async function resolvePds(did) {
|
|
25
|
-
const cached = pdsCache.get(did);
|
|
26
|
-
if (cached)
|
|
27
|
-
return cached;
|
|
28
25
|
let didDoc;
|
|
29
26
|
if (did.startsWith('did:web:')) {
|
|
30
27
|
const domain = did.slice('did:web:'.length);
|
|
@@ -42,12 +39,10 @@ async function resolvePds(did) {
|
|
|
42
39
|
const pds = didDoc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
|
|
43
40
|
if (!pds)
|
|
44
41
|
throw new Error(`No PDS endpoint in DID document for ${did}`);
|
|
45
|
-
//
|
|
42
|
+
// First at:// entry in alsoKnownAs is the canonical handle (per @atproto/identity convention)
|
|
46
43
|
const aka = didDoc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
47
44
|
const handle = aka ? aka.slice('at://'.length) : null;
|
|
48
|
-
|
|
49
|
-
pdsCache.set(did, result);
|
|
50
|
-
return result;
|
|
45
|
+
return { pds, handle };
|
|
51
46
|
}
|
|
52
47
|
/**
|
|
53
48
|
* Paginates through all active repos on a relay/PDS using `com.atproto.sync.listRepos`.
|
package/dist/dev-entry.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dev-entry.d.ts","sourceRoot":"","sources":["../src/dev-entry.ts"],"names":[],"mappings":"AA8GA,eAAO,MAAM,OAAO,yCAKlB,CAAA;AAEF,yEAAyE;AACzE,wBAAsB,YAAY,kBAEjC;AAED,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAA"}
|
package/dist/dev-entry.js
CHANGED
|
@@ -73,6 +73,7 @@ const collectionSet = new Set(collections);
|
|
|
73
73
|
const cursor = await getCursor('relay');
|
|
74
74
|
startIndexer({
|
|
75
75
|
relayUrl: config.relay,
|
|
76
|
+
plcUrl: config.plc,
|
|
76
77
|
collections: collectionSet,
|
|
77
78
|
signalCollections: config.backfill.signalCollections ? new Set(config.backfill.signalCollections) : undefined,
|
|
78
79
|
pinnedRepos: config.backfill.repos ? new Set(config.backfill.repos) : undefined,
|
package/dist/indexer.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export declare function triggerAutoBackfill(did: string, attempt?: number): Prom
|
|
|
12
12
|
/** Configuration for the firehose indexer. */
|
|
13
13
|
interface IndexerOpts {
|
|
14
14
|
relayUrl: string;
|
|
15
|
+
plcUrl: string;
|
|
15
16
|
collections: Set<string>;
|
|
16
17
|
signalCollections?: Set<string>;
|
|
17
18
|
pinnedRepos?: Set<string>;
|
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":"AAoKA;;;;;;;GAOG;AACH,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGxD;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CA4EjF;AAED,8CAA8C;AAC9C,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAC/B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B;AAyBD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAoDxE"}
|
package/dist/indexer.js
CHANGED
|
@@ -29,6 +29,7 @@ let indexerSignalCollections;
|
|
|
29
29
|
let indexerPinnedRepos = null;
|
|
30
30
|
let indexerFetchTimeout;
|
|
31
31
|
let indexerMaxRetries;
|
|
32
|
+
let indexerPlcUrl;
|
|
32
33
|
let maxConcurrentBackfills = 3;
|
|
33
34
|
/**
|
|
34
35
|
* Flush the write buffer — insert all buffered records, update the relay cursor,
|
|
@@ -259,6 +260,7 @@ export async function startIndexer(opts) {
|
|
|
259
260
|
indexerPinnedRepos = opts.pinnedRepos || null;
|
|
260
261
|
indexerFetchTimeout = fetchTimeout;
|
|
261
262
|
indexerMaxRetries = opts.maxRetries;
|
|
263
|
+
indexerPlcUrl = opts.plcUrl;
|
|
262
264
|
maxConcurrentBackfills = opts.parallelism ?? 3;
|
|
263
265
|
// Pre-populate repo status cache from DB so non-signal updates
|
|
264
266
|
// (e.g. profile changes) are processed for already-tracked DIDs
|
|
@@ -287,8 +289,8 @@ export async function startIndexer(opts) {
|
|
|
287
289
|
const bytes = new Uint8Array(event.data);
|
|
288
290
|
processMessage(bytes, collections);
|
|
289
291
|
}
|
|
290
|
-
catch {
|
|
291
|
-
|
|
292
|
+
catch (err) {
|
|
293
|
+
emit('indexer', 'decode_error', { error: err instanceof Error ? err.message : String(err) });
|
|
292
294
|
}
|
|
293
295
|
});
|
|
294
296
|
ws.addEventListener('open', () => log('[indexer] Connected to relay'));
|
|
@@ -298,6 +300,60 @@ export async function startIndexer(opts) {
|
|
|
298
300
|
});
|
|
299
301
|
return ws;
|
|
300
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Handle a `#identity` firehose event for a DID. The `handle` field on the
|
|
305
|
+
* event is optional per the lexicon, and some emitters omit it (signalling
|
|
306
|
+
* "re-resolve"). When absent, we re-resolve from the PLC directory so handle
|
|
307
|
+
* renames propagate even when the relay payload is sparse.
|
|
308
|
+
*
|
|
309
|
+
* Only updates DIDs we already track (present in repoStatusCache) to avoid
|
|
310
|
+
* writing rows for the entire network.
|
|
311
|
+
*/
|
|
312
|
+
async function handleIdentityEvent(did, payloadHandle) {
|
|
313
|
+
if (!repoStatusCache.has(did))
|
|
314
|
+
return;
|
|
315
|
+
let handle = payloadHandle;
|
|
316
|
+
const payloadHadHandle = handle !== undefined;
|
|
317
|
+
if (!handle) {
|
|
318
|
+
try {
|
|
319
|
+
// Bound the PLC fetch so a slow plc.directory can't pile up unbounded
|
|
320
|
+
// promises during an identity-event burst (fire-and-forget caller).
|
|
321
|
+
const res = await fetch(`${indexerPlcUrl}/${did}`, {
|
|
322
|
+
signal: AbortSignal.timeout(indexerFetchTimeout * 1000),
|
|
323
|
+
});
|
|
324
|
+
if (res.ok) {
|
|
325
|
+
const doc = (await res.json());
|
|
326
|
+
// First at:// entry is the canonical handle (per @atproto/identity convention)
|
|
327
|
+
const aka = doc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
328
|
+
handle = aka ? aka.slice('at://'.length) : undefined;
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
emit('indexer', 'identity_resolve_error', { did, status: res.status });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
emit('indexer', 'identity_resolve_error', {
|
|
336
|
+
did,
|
|
337
|
+
error: err instanceof Error ? err.message : String(err),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!handle) {
|
|
342
|
+
emit('indexer', 'identity_no_handle', { did, payload_had_handle: payloadHadHandle });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await updateRepoHandle(did, handle);
|
|
347
|
+
emit('indexer', 'identity_handle_update', { did, handle, payload_had_handle: payloadHadHandle });
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
emit('indexer', 'identity_update_error', {
|
|
351
|
+
did,
|
|
352
|
+
handle,
|
|
353
|
+
error: err instanceof Error ? err.message : String(err),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
301
357
|
/**
|
|
302
358
|
* Process a single firehose message. Decodes the CBOR header/body, filters
|
|
303
359
|
* for relevant collections, validates records against lexicons, and routes
|
|
@@ -306,13 +362,13 @@ export async function startIndexer(opts) {
|
|
|
306
362
|
function processMessage(bytes, collections) {
|
|
307
363
|
const header = cborDecode(bytes, 0);
|
|
308
364
|
const body = cborDecode(bytes, header.offset);
|
|
309
|
-
// Handle identity events (handle changes)
|
|
365
|
+
// Handle identity events (handle changes). Fire-and-forget — keeps
|
|
366
|
+
// processMessage synchronous so the WS event loop drains without backpressure.
|
|
310
367
|
if (header.value.t === '#identity') {
|
|
311
|
-
const did = body.value.did;
|
|
312
|
-
const handle = body.value.handle;
|
|
313
|
-
if (did
|
|
314
|
-
|
|
315
|
-
}
|
|
368
|
+
const did = typeof body.value.did === 'string' ? body.value.did : undefined;
|
|
369
|
+
const handle = typeof body.value.handle === 'string' ? body.value.handle : undefined;
|
|
370
|
+
if (did)
|
|
371
|
+
handleIdentityEvent(did, handle);
|
|
316
372
|
return;
|
|
317
373
|
}
|
|
318
374
|
if (header.value.op !== 1 || header.value.t !== '#commit')
|
package/dist/main.js
CHANGED
|
@@ -200,6 +200,7 @@ log(` Feeds: ${listFeeds()
|
|
|
200
200
|
const cursor = await getCursor('relay');
|
|
201
201
|
startIndexer({
|
|
202
202
|
relayUrl: config.relay,
|
|
203
|
+
plcUrl: config.plc,
|
|
203
204
|
collections: collectionSet,
|
|
204
205
|
signalCollections: config.backfill.signalCollections ? new Set(config.backfill.signalCollections) : undefined,
|
|
205
206
|
pinnedRepos: config.backfill.repos ? new Set(config.backfill.repos) : undefined,
|
package/dist/oauth/server.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { OAuthConfig } from '../config.ts';
|
|
2
|
+
/**
|
|
3
|
+
* RFC 6749 §5.2 OAuth token-endpoint error.
|
|
4
|
+
*
|
|
5
|
+
* Surfaces as HTTP `status` (default 400) with body
|
|
6
|
+
* `{ error: code, error_description: description }` so OAuth clients can
|
|
7
|
+
* distinguish a permanently-rejected refresh token (`invalid_grant`) from
|
|
8
|
+
* a transient server failure and decide whether to log the user out.
|
|
9
|
+
*/
|
|
10
|
+
export declare class OAuthError extends Error {
|
|
11
|
+
readonly code: 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope';
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly status: number;
|
|
14
|
+
constructor(code: 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope', description: string, status?: number);
|
|
15
|
+
}
|
|
2
16
|
export declare function initOAuth(_config: OAuthConfig, plcUrl: string, relayUrl: string): Promise<void>;
|
|
3
17
|
export declare function getAuthServerMetadata(issuer: string, config: OAuthConfig): {
|
|
4
18
|
issuer: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAuC/C;;;;;;;GAOG;AACH,qBAAa,UAAW,SAAQ,KAAK;aAEjB,IAAI,EAChB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,qBAAqB,GACrB,wBAAwB,GACxB,eAAe;aACH,WAAW,EAAE,MAAM;aACnB,MAAM,EAAE,MAAM;gBARd,IAAI,EAChB,iBAAiB,GACjB,gBAAgB,GAChB,eAAe,GACf,qBAAqB,GACrB,wBAAwB,GACxB,eAAe,EACH,WAAW,EAAE,MAAM,EACnB,MAAM,GAAE,MAAY;CAKvC;AAuCD,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;EAcpE;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAwKtD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAShF;AAID;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,MAAM,CAAC,CA6HjB;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CA2HrG;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA2JD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChH,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAsEpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
|
package/dist/oauth/server.js
CHANGED
|
@@ -9,6 +9,26 @@ import { emit } from "../logger.js";
|
|
|
9
9
|
import { querySQL } from "../database/db.js";
|
|
10
10
|
import { fireOnLoginHook } from "../hooks.js";
|
|
11
11
|
const SERVER_KEY_KID = 'appview-oauth-key';
|
|
12
|
+
/**
|
|
13
|
+
* RFC 6749 §5.2 OAuth token-endpoint error.
|
|
14
|
+
*
|
|
15
|
+
* Surfaces as HTTP `status` (default 400) with body
|
|
16
|
+
* `{ error: code, error_description: description }` so OAuth clients can
|
|
17
|
+
* distinguish a permanently-rejected refresh token (`invalid_grant`) from
|
|
18
|
+
* a transient server failure and decide whether to log the user out.
|
|
19
|
+
*/
|
|
20
|
+
export class OAuthError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
description;
|
|
23
|
+
status;
|
|
24
|
+
constructor(code, description, status = 400) {
|
|
25
|
+
super(description);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.description = description;
|
|
28
|
+
this.status = status;
|
|
29
|
+
this.name = 'OAuthError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
12
32
|
async function resolveHandleForDid(did) {
|
|
13
33
|
const rows = (await querySQL('SELECT handle FROM _repos WHERE did = $1', [did]));
|
|
14
34
|
return rows[0]?.handle || undefined;
|
|
@@ -530,14 +550,14 @@ export async function handleCallback(config, code, state, iss) {
|
|
|
530
550
|
export async function handleToken(config, body, dpopHeader, requestUrl) {
|
|
531
551
|
const grantType = body.grant_type;
|
|
532
552
|
if (!grantType)
|
|
533
|
-
throw new
|
|
553
|
+
throw new OAuthError('invalid_request', 'grant_type is required');
|
|
534
554
|
if (grantType === 'authorization_code') {
|
|
535
555
|
return handleAuthorizationCodeGrant(config, body, dpopHeader, requestUrl);
|
|
536
556
|
}
|
|
537
557
|
else if (grantType === 'refresh_token') {
|
|
538
558
|
return handleRefreshTokenGrant(config, body, dpopHeader, requestUrl);
|
|
539
559
|
}
|
|
540
|
-
throw new
|
|
560
|
+
throw new OAuthError('unsupported_grant_type', `Unsupported grant_type: ${grantType}`);
|
|
541
561
|
}
|
|
542
562
|
async function handleAuthorizationCodeGrant(config, body, dpopHeader, requestUrl) {
|
|
543
563
|
const dpop = await parseDpopProof(dpopHeader, 'POST', requestUrl);
|
|
@@ -610,20 +630,21 @@ async function handleRefreshTokenGrant(config, body, dpopHeader, requestUrl) {
|
|
|
610
630
|
const dpop = await parseDpopProof(dpopHeader, 'POST', requestUrl);
|
|
611
631
|
const fresh = await checkAndStoreDpopJti(dpop.jti, dpop.iat + 300);
|
|
612
632
|
if (!fresh)
|
|
613
|
-
throw new
|
|
633
|
+
throw new OAuthError('invalid_request', 'DPoP jti replay detected');
|
|
614
634
|
const { refresh_token, client_id } = body;
|
|
615
635
|
if (!refresh_token || !client_id)
|
|
616
|
-
throw new
|
|
617
|
-
// Look up and validate refresh token
|
|
636
|
+
throw new OAuthError('invalid_request', 'Missing required parameters');
|
|
637
|
+
// Look up and validate refresh token. Per RFC 6749 §5.2, all of these are
|
|
638
|
+
// `invalid_grant` — the client must treat them as terminal and re-authenticate.
|
|
618
639
|
const stored = await getRefreshToken(refresh_token);
|
|
619
640
|
if (!stored)
|
|
620
|
-
throw new
|
|
641
|
+
throw new OAuthError('invalid_grant', 'Invalid refresh token');
|
|
621
642
|
if (stored.revoked)
|
|
622
|
-
throw new
|
|
643
|
+
throw new OAuthError('invalid_grant', 'Refresh token revoked');
|
|
623
644
|
if (stored.expires_at && stored.expires_at < Math.floor(Date.now() / 1000))
|
|
624
|
-
throw new
|
|
645
|
+
throw new OAuthError('invalid_grant', 'Refresh token expired');
|
|
625
646
|
if (stored.client_id !== client_id)
|
|
626
|
-
throw new
|
|
647
|
+
throw new OAuthError('invalid_grant', 'client_id mismatch');
|
|
627
648
|
// Revoke old refresh token (rotation)
|
|
628
649
|
await revokeRefreshToken(refresh_token);
|
|
629
650
|
const did = stored.did;
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AA0DA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AA2B9C;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAgM3F;AAED,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,CA00B5F;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
|
@@ -9,7 +9,7 @@ import { handleOpengraphRequest, buildOgMeta } from "./opengraph.js";
|
|
|
9
9
|
import { getLabelDefinitions, rescanLabels } from "./labels.js";
|
|
10
10
|
import { triggerAutoBackfill } from "./indexer.js";
|
|
11
11
|
import { emit, timer } from "./logger.js";
|
|
12
|
-
import { getAuthServerMetadata, getProtectedResourceMetadata, getJwks, getClientMetadata, handlePar, buildAuthorizeRedirect, handleCallback, serverLogin, handleToken, authenticate, } from "./oauth/server.js";
|
|
12
|
+
import { getAuthServerMetadata, getProtectedResourceMetadata, getJwks, getClientMetadata, handlePar, buildAuthorizeRedirect, handleCallback, serverLogin, handleToken, authenticate, OAuthError, } from "./oauth/server.js";
|
|
13
13
|
import { createSessionCookie, sessionCookieHeader, clearSessionCookieHeader, parseSessionCookie, } from "./oauth/session.js";
|
|
14
14
|
import { getOAuthRequest } from "./oauth/db.js";
|
|
15
15
|
import { pdsCreateRecord, pdsDeleteRecord, pdsPutRecord, pdsApplyWrites, pdsUploadBlob, ProxyError, ScopeMissingProxyError, } from "./pds-proxy.js";
|
|
@@ -849,10 +849,19 @@ export function createHandler(config) {
|
|
|
849
849
|
body = JSON.parse(rawBody);
|
|
850
850
|
}
|
|
851
851
|
const dpopHeader = request.headers.get('dpop');
|
|
852
|
-
if (!dpopHeader)
|
|
853
|
-
return withCors(
|
|
854
|
-
|
|
855
|
-
|
|
852
|
+
if (!dpopHeader) {
|
|
853
|
+
return withCors(json({ error: 'invalid_request', error_description: 'DPoP header required' }, 400, acceptEncoding));
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
const result = await handleToken(oauth, body, dpopHeader, `${requestOrigin}/oauth/token`);
|
|
857
|
+
return withCors(json(result, 200, acceptEncoding));
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
if (err instanceof OAuthError) {
|
|
861
|
+
return withCors(json({ error: err.code, error_description: err.description }, err.status, acceptEncoding));
|
|
862
|
+
}
|
|
863
|
+
throw err;
|
|
864
|
+
}
|
|
856
865
|
}
|
|
857
866
|
// POST /xrpc/dev.hatk.createRecord — proxy write to user's PDS
|
|
858
867
|
if (url.pathname === coreXrpc('createRecord') && request.method === 'POST' && oauth) {
|