@hatk/hatk 0.0.1-alpha.11 → 0.0.1-alpha.111
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/adapter.d.ts +19 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +113 -0
- package/dist/backfill.d.ts +36 -2
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +153 -37
- package/dist/car.js +1 -1
- package/dist/classifiers.d.ts +228 -0
- package/dist/classifiers.d.ts.map +1 -0
- package/dist/classifiers.js +688 -0
- package/dist/cli.js +248 -1043
- package/dist/config.d.ts +179 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +78 -8
- package/dist/database/adapter-factory.d.ts +6 -0
- package/dist/database/adapter-factory.d.ts.map +1 -0
- package/dist/database/adapter-factory.js +20 -0
- package/dist/database/adapters/duckdb-search.d.ts +12 -0
- package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
- package/dist/database/adapters/duckdb-search.js +27 -0
- package/dist/database/adapters/duckdb.d.ts +25 -0
- package/dist/database/adapters/duckdb.d.ts.map +1 -0
- package/dist/database/adapters/duckdb.js +161 -0
- package/dist/database/adapters/sqlite-search.d.ts +16 -0
- package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-search.js +76 -0
- package/dist/database/adapters/sqlite.d.ts +18 -0
- package/dist/database/adapters/sqlite.d.ts.map +1 -0
- package/dist/database/adapters/sqlite.js +88 -0
- package/dist/database/db.d.ts +290 -0
- package/dist/database/db.d.ts.map +1 -0
- package/dist/database/db.js +2073 -0
- package/dist/database/dialect.d.ts +45 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +72 -0
- package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
- package/dist/database/fts.d.ts.map +1 -0
- package/dist/{fts.js → database/fts.js} +122 -34
- package/dist/database/index.d.ts +7 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +6 -0
- package/dist/database/ports.d.ts +50 -0
- package/dist/database/ports.d.ts.map +1 -0
- package/dist/database/ports.js +1 -0
- package/dist/database/schema.d.ts +91 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/{schema.js → database/schema.js} +153 -54
- package/dist/dev-entry.d.ts +8 -0
- package/dist/dev-entry.d.ts.map +1 -0
- package/dist/dev-entry.js +131 -0
- package/dist/feeds.d.ts +26 -8
- package/dist/feeds.d.ts.map +1 -1
- package/dist/feeds.js +72 -16
- package/dist/hooks.d.ts +102 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +186 -0
- package/dist/hydrate.d.ts +55 -6
- package/dist/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +21 -20
- package/dist/indexer.d.ts +194 -3
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +627 -92
- package/dist/jetstream.d.ts +118 -0
- package/dist/jetstream.d.ts.map +1 -0
- package/dist/jetstream.js +238 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.d.ts.map +1 -1
- package/dist/labels.js +71 -6
- package/dist/lex-types.d.ts +2 -0
- package/dist/lex-types.d.ts.map +1 -1
- package/dist/lexicon-resolve.d.ts.map +1 -1
- package/dist/lexicon-resolve.js +27 -112
- package/dist/lexicons/com/atproto/label/defs.json +75 -0
- package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
- package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
- package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
- package/dist/lexicons/dev/hatk/createRecord.json +40 -0
- package/dist/lexicons/dev/hatk/createReport.json +48 -0
- package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
- package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
- package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
- package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
- package/dist/lexicons/dev/hatk/getFeed.json +30 -0
- package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
- package/dist/lexicons/dev/hatk/getRecord.json +26 -0
- package/dist/lexicons/dev/hatk/getRecords.json +32 -0
- package/dist/lexicons/dev/hatk/putPreference.json +28 -0
- package/dist/lexicons/dev/hatk/putRecord.json +41 -0
- package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
- package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -0
- package/dist/main.js +212 -75
- package/dist/mst.d.ts +15 -0
- package/dist/mst.d.ts.map +1 -1
- package/dist/mst.js +13 -0
- package/dist/oauth/db.d.ts +27 -1
- package/dist/oauth/db.d.ts.map +1 -1
- package/dist/oauth/db.js +80 -19
- package/dist/oauth/discovery.d.ts +10 -0
- package/dist/oauth/discovery.d.ts.map +1 -1
- package/dist/oauth/discovery.js +54 -2
- package/dist/oauth/server.d.ts +131 -1
- package/dist/oauth/server.d.ts.map +1 -1
- package/dist/oauth/server.js +504 -39
- package/dist/oauth/service-describe.d.ts +42 -0
- package/dist/oauth/service-describe.d.ts.map +1 -0
- package/dist/oauth/service-describe.js +108 -0
- package/dist/oauth/session.d.ts +32 -0
- package/dist/oauth/session.d.ts.map +1 -0
- package/dist/oauth/session.js +131 -0
- package/dist/opengraph.d.ts +10 -0
- package/dist/opengraph.d.ts.map +1 -1
- package/dist/opengraph.js +94 -42
- package/dist/pds-proxy.d.ts +92 -0
- package/dist/pds-proxy.d.ts.map +1 -0
- package/dist/pds-proxy.js +396 -0
- package/dist/private-collections.d.ts +13 -0
- package/dist/private-collections.d.ts.map +1 -0
- package/dist/private-collections.js +27 -0
- package/dist/push.d.ts +54 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +367 -0
- package/dist/renderer.d.ts +27 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +46 -0
- package/dist/resolve-hatk.d.ts +6 -0
- package/dist/resolve-hatk.d.ts.map +1 -0
- package/dist/resolve-hatk.js +20 -0
- package/dist/response.d.ts +16 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +69 -0
- package/dist/scanner.d.ts +22 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +92 -0
- package/dist/seed.d.ts +19 -0
- package/dist/seed.d.ts.map +1 -1
- package/dist/seed.js +43 -4
- package/dist/server-init.d.ts +8 -0
- package/dist/server-init.d.ts.map +1 -0
- package/dist/server-init.js +75 -0
- package/dist/server.d.ts +46 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1137 -649
- package/dist/setup.d.ts +28 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +50 -3
- package/dist/spaces/blob.d.ts +112 -0
- package/dist/spaces/blob.d.ts.map +1 -0
- package/dist/spaces/blob.js +460 -0
- package/dist/spaces/credential.d.ts +82 -0
- package/dist/spaces/credential.d.ts.map +1 -0
- package/dist/spaces/credential.js +220 -0
- package/dist/spaces/engine.d.ts +97 -0
- package/dist/spaces/engine.d.ts.map +1 -0
- package/dist/spaces/engine.js +563 -0
- package/dist/spaces/guard.d.ts +41 -0
- package/dist/spaces/guard.d.ts.map +1 -0
- package/dist/spaces/guard.js +79 -0
- package/dist/spaces/identity.d.ts +37 -0
- package/dist/spaces/identity.d.ts.map +1 -0
- package/dist/spaces/identity.js +93 -0
- package/dist/spaces/index.d.ts +56 -0
- package/dist/spaces/index.d.ts.map +1 -0
- package/dist/spaces/index.js +134 -0
- package/dist/spaces/notify.d.ts +60 -0
- package/dist/spaces/notify.d.ts.map +1 -0
- package/dist/spaces/notify.js +128 -0
- package/dist/spaces/records.d.ts +37 -0
- package/dist/spaces/records.d.ts.map +1 -0
- package/dist/spaces/records.js +63 -0
- package/dist/spaces/signature.d.ts +36 -0
- package/dist/spaces/signature.d.ts.map +1 -0
- package/dist/spaces/signature.js +83 -0
- package/dist/spaces/store.d.ts +51 -0
- package/dist/spaces/store.d.ts.map +1 -0
- package/dist/spaces/store.js +99 -0
- package/dist/spaces/uri.d.ts +60 -0
- package/dist/spaces/uri.d.ts.map +1 -0
- package/dist/spaces/uri.js +97 -0
- package/dist/spaces/verify.d.ts +46 -0
- package/dist/spaces/verify.d.ts.map +1 -0
- package/dist/spaces/verify.js +94 -0
- package/dist/spaces/viewer.d.ts +51 -0
- package/dist/spaces/viewer.d.ts.map +1 -0
- package/dist/spaces/viewer.js +167 -0
- package/dist/spaces/visibility.d.ts +57 -0
- package/dist/spaces/visibility.d.ts.map +1 -0
- package/dist/spaces/visibility.js +81 -0
- package/dist/templates/feed.tpl +18 -0
- package/dist/templates/hook.tpl +5 -0
- package/dist/templates/label.tpl +15 -0
- package/dist/templates/og.tpl +17 -0
- package/dist/templates/seed.tpl +11 -0
- package/dist/templates/setup.tpl +5 -0
- package/dist/templates/test-feed.tpl +19 -0
- package/dist/templates/test-xrpc.tpl +19 -0
- package/dist/templates/xrpc.tpl +53 -0
- package/dist/test.d.ts +3 -1
- package/dist/test.d.ts.map +1 -1
- package/dist/test.js +60 -36
- package/dist/views.js +1 -1
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.d.ts.map +1 -1
- package/dist/vite-plugin.js +254 -66
- package/dist/xrpc.d.ts +143 -12
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +282 -55
- package/package.json +29 -9
- package/public/admin-auth.js +271 -306
- package/public/admin.html +652 -61
- package/dist/db.d.ts +0 -135
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -1365
- package/dist/fts.d.ts.map +0 -1
- package/dist/oauth/hooks.d.ts +0 -10
- package/dist/oauth/hooks.d.ts.map +0 -1
- package/dist/oauth/hooks.js +0 -40
- package/dist/schema.d.ts +0 -51
- package/dist/schema.d.ts.map +0 -1
- package/dist/test-browser.d.ts +0 -14
- package/dist/test-browser.d.ts.map +0 -1
- package/dist/test-browser.js +0 -26
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Node.js IncomingMessage to a Web Standard Request.
|
|
4
|
+
*/
|
|
5
|
+
export declare function toRequest(req: IncomingMessage, base: string): Request;
|
|
6
|
+
/**
|
|
7
|
+
* Pipe a Web Standard Response back to a Node.js ServerResponse.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sendResponse(res: ServerResponse, response: Response): Promise<void>;
|
|
10
|
+
/** Routes handled by hatk — everything else can fall through to a framework handler. */
|
|
11
|
+
export declare const HATK_ROUTES: string[];
|
|
12
|
+
export declare function isHatkRoute(pathname: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Create a Node.js HTTP server from a Web Standard fetch handler.
|
|
15
|
+
* If a fallback Node middleware is provided, non-hatk routes are sent to it
|
|
16
|
+
* (e.g. SvelteKit's handler from build/handler.js).
|
|
17
|
+
*/
|
|
18
|
+
export declare function serve(handler: (request: Request) => Promise<Response>, port: number, base?: string, fallback?: (req: IncomingMessage, res: ServerResponse, next: () => void) => void): import("node:http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
19
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,cAAc,EAAgB,MAAM,WAAW,CAAA;AAEnF;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA0BrE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBzF;AAED,wFAAwF;AACxF,eAAO,MAAM,WAAW,UAmBvB,CAAA;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErD;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CACnB,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAChD,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,6EA4BjF"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Convert a Node.js IncomingMessage to a Web Standard Request.
|
|
4
|
+
*/
|
|
5
|
+
export function toRequest(req, base) {
|
|
6
|
+
const url = new URL(req.url, base);
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
9
|
+
if (value) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
for (const v of value)
|
|
12
|
+
headers.append(key, v);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
headers.set(key, value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const init = {
|
|
20
|
+
method: req.method,
|
|
21
|
+
headers,
|
|
22
|
+
};
|
|
23
|
+
// GET and HEAD requests cannot have a body
|
|
24
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
25
|
+
// @ts-expect-error — Node.js streams are valid body sources
|
|
26
|
+
init.body = req;
|
|
27
|
+
init.duplex = 'half';
|
|
28
|
+
}
|
|
29
|
+
return new Request(url.href, init);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pipe a Web Standard Response back to a Node.js ServerResponse.
|
|
33
|
+
*/
|
|
34
|
+
export async function sendResponse(res, response) {
|
|
35
|
+
const rawHeaders = [];
|
|
36
|
+
response.headers.forEach((value, name) => {
|
|
37
|
+
rawHeaders.push(name, value);
|
|
38
|
+
});
|
|
39
|
+
res.writeHead(response.status, rawHeaders);
|
|
40
|
+
if (!response.body) {
|
|
41
|
+
res.end();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const reader = response.body.getReader();
|
|
45
|
+
try {
|
|
46
|
+
while (true) {
|
|
47
|
+
const { done, value } = await reader.read();
|
|
48
|
+
if (done)
|
|
49
|
+
break;
|
|
50
|
+
res.write(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
reader.releaseLock();
|
|
55
|
+
res.end();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Routes handled by hatk — everything else can fall through to a framework handler. */
|
|
59
|
+
export const HATK_ROUTES = [
|
|
60
|
+
'/xrpc/',
|
|
61
|
+
'/blob/', // dev image proxy — see blobUrl in xrpc.ts
|
|
62
|
+
'/space-blob', // a blob inside a permissioned space, served per viewer
|
|
63
|
+
'/.well-known/did.json', // this instance's own DID, for space notice delivery
|
|
64
|
+
'/oauth/',
|
|
65
|
+
'/oauth-client-metadata.json',
|
|
66
|
+
'/.well-known/oauth-authorization-server',
|
|
67
|
+
'/.well-known/oauth-protected-resource',
|
|
68
|
+
'/og/',
|
|
69
|
+
'/admin',
|
|
70
|
+
'/repos',
|
|
71
|
+
'/info/',
|
|
72
|
+
'/_health',
|
|
73
|
+
'/robots.txt',
|
|
74
|
+
'/auth/logout',
|
|
75
|
+
'/auth/switch',
|
|
76
|
+
'/auth/accounts',
|
|
77
|
+
'/__dev/',
|
|
78
|
+
];
|
|
79
|
+
export function isHatkRoute(pathname) {
|
|
80
|
+
return HATK_ROUTES.some((r) => pathname.startsWith(r) || pathname === r);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create a Node.js HTTP server from a Web Standard fetch handler.
|
|
84
|
+
* If a fallback Node middleware is provided, non-hatk routes are sent to it
|
|
85
|
+
* (e.g. SvelteKit's handler from build/handler.js).
|
|
86
|
+
*/
|
|
87
|
+
export function serve(handler, port, base, fallback) {
|
|
88
|
+
const origin = base || `http://localhost:${port}`;
|
|
89
|
+
const server = createServer(async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(req.url, origin);
|
|
92
|
+
// If we have a fallback (e.g. SvelteKit) and this isn't a hatk route, skip hatk
|
|
93
|
+
if (fallback && !isHatkRoute(url.pathname)) {
|
|
94
|
+
fallback(req, res, () => {
|
|
95
|
+
res.writeHead(404);
|
|
96
|
+
res.end('Not found');
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const request = toRequest(req, origin);
|
|
101
|
+
const response = await handler(request);
|
|
102
|
+
await sendResponse(res, response);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (!res.headersSent) {
|
|
106
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
107
|
+
}
|
|
108
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
server.listen(port);
|
|
112
|
+
return server;
|
|
113
|
+
}
|
package/dist/backfill.d.ts
CHANGED
|
@@ -1,15 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The collections whose local rows a full re-import may purge before
|
|
3
|
+
* re-inserting from the repo CAR.
|
|
4
|
+
*
|
|
5
|
+
* Private collections are AppView-authoritative and never present in a repo:
|
|
6
|
+
* purging their rows would destroy data the re-import can never restore (this
|
|
7
|
+
* happened — a login-triggered backfill wiped a deployment's activity rows).
|
|
8
|
+
* Exported for tests.
|
|
9
|
+
*/
|
|
10
|
+
export declare function purgeableCollections(collections: Iterable<string>): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Whether a repo with this stored status should be queued for backfill.
|
|
13
|
+
*
|
|
14
|
+
* Takendown repos are excluded: re-backfilling one ends in
|
|
15
|
+
* `setRepoStatus(did, 'active')`, which would silently reinstate an account an
|
|
16
|
+
* admin took down — takedowns must survive every redeploy. Reinstatement goes
|
|
17
|
+
* through `/admin/reverse-takedown` only. Deactivated and deleted repos are
|
|
18
|
+
* excluded for the same reason from the other side: the network said so, and
|
|
19
|
+
* only the network saying otherwise (an `#account` event) brings them back.
|
|
20
|
+
* Exported for tests.
|
|
21
|
+
*/
|
|
22
|
+
export declare function backfillEligible(status: string | null): boolean;
|
|
1
23
|
import type { BackfillConfig } from './config.ts';
|
|
2
24
|
/** Options passed to {@link runBackfill}. */
|
|
3
25
|
interface BackfillOpts {
|
|
4
26
|
/** Base URL of the relay or PDS to enumerate repos from (e.g. `wss://bsky.network`). */
|
|
5
27
|
pdsUrl: string;
|
|
28
|
+
/** Further relays/PDSes to enumerate — the HTTP side of `config.relays`. */
|
|
29
|
+
extraPdsUrls?: string[];
|
|
6
30
|
/** PLC directory URL used to resolve `did:plc` identifiers (e.g. `https://plc.directory`). */
|
|
7
31
|
plcUrl: string;
|
|
8
32
|
/** AT Protocol collection NSIDs to index (e.g. `app.bsky.feed.post`). */
|
|
9
33
|
collections: Set<string>;
|
|
10
|
-
/** Backfill behavior settings from `config.
|
|
34
|
+
/** Backfill behavior settings from `hatk.config.ts`. */
|
|
11
35
|
config: BackfillConfig;
|
|
12
36
|
}
|
|
37
|
+
/** Point DID resolution at a PLC directory. `runBackfill` does this too; call it
|
|
38
|
+
* at boot so resolvers that run before the first backfill have somewhere to go. */
|
|
39
|
+
export declare function configurePlc(url: string): void;
|
|
40
|
+
/** Set the PDS hosts whose repos {@link backfillRepo} leaves unimported; see
|
|
41
|
+
* `BackfillConfig.skipPds`. Matched on hostname, so a trailing slash or a
|
|
42
|
+
* scheme in the DID document makes no difference. */
|
|
43
|
+
export declare function configureSkipPds(hosts: string[] | undefined): void;
|
|
44
|
+
/** A repo's PDS endpoint, cached for the process. For serving blobs, where a
|
|
45
|
+
* stale answer after a migration costs one broken image, not a wrong row. */
|
|
46
|
+
export declare function pdsFor(did: string): Promise<string>;
|
|
13
47
|
/**
|
|
14
48
|
* Downloads and indexes a single user's repo via `com.atproto.sync.getRepo`.
|
|
15
49
|
*
|
|
@@ -65,6 +99,6 @@ export declare function backfillRepo(did: string, collections: Set<string>, fetc
|
|
|
65
99
|
* })
|
|
66
100
|
* ```
|
|
67
101
|
*/
|
|
68
|
-
export declare function runBackfill(opts: BackfillOpts): Promise<
|
|
102
|
+
export declare function runBackfill(opts: BackfillOpts): Promise<number>;
|
|
69
103
|
export {};
|
|
70
104
|
//# sourceMappingURL=backfill.d.ts.map
|
package/dist/backfill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backfill.d.ts","sourceRoot":"","sources":["../src/backfill.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backfill.d.ts","sourceRoot":"","sources":["../src/backfill.ts"],"names":[],"mappings":"AAkBA;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAE5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAE/D;AAMD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,6CAA6C;AAC7C,UAAU,YAAY;IACpB,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAA;IACd,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,CAAA;IACd,yEAAyE;IACzE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,wDAAwD;IACxD,MAAM,EAAE,cAAc,CAAA;CACvB;AAWD;oFACoF;AACpF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE9C;AAID;;sDAEsD;AACtD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,IAAI,CAElE;AAaD;8EAC8E;AAC9E,wBAAsB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMzD;AAsFD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2L/G;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAuIrE"}
|
package/dist/backfill.js
CHANGED
|
@@ -1,16 +1,81 @@
|
|
|
1
|
+
import { noteReferencedRepos } from "./indexer.js";
|
|
1
2
|
import { parseCarStream } from "./car.js";
|
|
2
3
|
import { cborDecode } from "./cbor.js";
|
|
3
4
|
import { walkMst } from "./mst.js";
|
|
4
|
-
import { setRepoStatus, getRepoStatus, getRepoRev, getRepoRetryInfo, listRetryEligibleRepos, listPendingRepos, querySQL, runSQL, getSchema, bulkInsertRecords, } from "./db.js";
|
|
5
|
+
import { setRepoStatus, getRepoStatus, getRepoRev, getRepoRetryInfo, listRetryEligibleRepos, listPendingRepos, querySQL, runSQL, getSchema, bulkInsertRecords, } from "./database/db.js";
|
|
6
|
+
import { isPrivateCollection } from "./private-collections.js";
|
|
7
|
+
/**
|
|
8
|
+
* The collections whose local rows a full re-import may purge before
|
|
9
|
+
* re-inserting from the repo CAR.
|
|
10
|
+
*
|
|
11
|
+
* Private collections are AppView-authoritative and never present in a repo:
|
|
12
|
+
* purging their rows would destroy data the re-import can never restore (this
|
|
13
|
+
* happened — a login-triggered backfill wiped a deployment's activity rows).
|
|
14
|
+
* Exported for tests.
|
|
15
|
+
*/
|
|
16
|
+
export function purgeableCollections(collections) {
|
|
17
|
+
return [...collections].filter((c) => !isPrivateCollection(c));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a repo with this stored status should be queued for backfill.
|
|
21
|
+
*
|
|
22
|
+
* Takendown repos are excluded: re-backfilling one ends in
|
|
23
|
+
* `setRepoStatus(did, 'active')`, which would silently reinstate an account an
|
|
24
|
+
* admin took down — takedowns must survive every redeploy. Reinstatement goes
|
|
25
|
+
* through `/admin/reverse-takedown` only. Deactivated and deleted repos are
|
|
26
|
+
* excluded for the same reason from the other side: the network said so, and
|
|
27
|
+
* only the network saying otherwise (an `#account` event) brings them back.
|
|
28
|
+
* Exported for tests.
|
|
29
|
+
*/
|
|
30
|
+
export function backfillEligible(status) {
|
|
31
|
+
return !status || !NOT_BACKFILLED.has(status);
|
|
32
|
+
}
|
|
33
|
+
/** Statuses a backfill must leave alone. See {@link backfillEligible}. */
|
|
34
|
+
const NOT_BACKFILLED = new Set(['active', 'takendown', 'deactivated', 'deleted']);
|
|
5
35
|
import { emit, timer } from "./logger.js";
|
|
6
|
-
|
|
7
|
-
|
|
36
|
+
import { validateRecord } from '@bigmoves/lexicon';
|
|
37
|
+
import { getLexiconArray } from "./database/schema.js";
|
|
8
38
|
let plcUrl;
|
|
39
|
+
/** Point DID resolution at a PLC directory. `runBackfill` does this too; call it
|
|
40
|
+
* at boot so resolvers that run before the first backfill have somewhere to go. */
|
|
41
|
+
export function configurePlc(url) {
|
|
42
|
+
plcUrl = url;
|
|
43
|
+
}
|
|
44
|
+
let skippedPdsHosts = new Set();
|
|
45
|
+
/** Set the PDS hosts whose repos {@link backfillRepo} leaves unimported; see
|
|
46
|
+
* `BackfillConfig.skipPds`. Matched on hostname, so a trailing slash or a
|
|
47
|
+
* scheme in the DID document makes no difference. */
|
|
48
|
+
export function configureSkipPds(hosts) {
|
|
49
|
+
skippedPdsHosts = new Set((hosts ?? []).map((h) => h.toLowerCase()));
|
|
50
|
+
}
|
|
51
|
+
function isSkippedPds(pdsUrl) {
|
|
52
|
+
if (!skippedPdsHosts.size)
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
return skippedPdsHosts.has(new URL(pdsUrl).hostname.toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const pdsCache = new Map();
|
|
62
|
+
/** A repo's PDS endpoint, cached for the process. For serving blobs, where a
|
|
63
|
+
* stale answer after a migration costs one broken image, not a wrong row. */
|
|
64
|
+
export async function pdsFor(did) {
|
|
65
|
+
const cached = pdsCache.get(did);
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
68
|
+
const { pds } = await resolvePds(did);
|
|
69
|
+
pdsCache.set(did, pds);
|
|
70
|
+
return pds;
|
|
71
|
+
}
|
|
9
72
|
/**
|
|
10
73
|
* Resolves a DID to its PDS endpoint and handle by fetching the DID document.
|
|
11
74
|
*
|
|
12
75
|
* Supports both `did:web` (fetches `/.well-known/did.json`) and `did:plc`
|
|
13
|
-
* (fetches from the PLC directory).
|
|
76
|
+
* (fetches from the PLC directory). Always fetches fresh — DID docs change
|
|
77
|
+
* (handle renames, PDS migrations) and a stale cache silently rewrites stale
|
|
78
|
+
* handles back into `_repos` on every backfill.
|
|
14
79
|
*
|
|
15
80
|
* @example
|
|
16
81
|
* ```ts
|
|
@@ -20,9 +85,6 @@ let plcUrl;
|
|
|
20
85
|
* ```
|
|
21
86
|
*/
|
|
22
87
|
async function resolvePds(did) {
|
|
23
|
-
const cached = pdsCache.get(did);
|
|
24
|
-
if (cached)
|
|
25
|
-
return cached;
|
|
26
88
|
let didDoc;
|
|
27
89
|
if (did.startsWith('did:web:')) {
|
|
28
90
|
const domain = did.slice('did:web:'.length);
|
|
@@ -40,12 +102,10 @@ async function resolvePds(did) {
|
|
|
40
102
|
const pds = didDoc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
|
|
41
103
|
if (!pds)
|
|
42
104
|
throw new Error(`No PDS endpoint in DID document for ${did}`);
|
|
43
|
-
//
|
|
105
|
+
// First at:// entry in alsoKnownAs is the canonical handle (per @atproto/identity convention)
|
|
44
106
|
const aka = didDoc.alsoKnownAs?.find((u) => u.startsWith('at://'));
|
|
45
107
|
const handle = aka ? aka.slice('at://'.length) : null;
|
|
46
|
-
|
|
47
|
-
pdsCache.set(did, result);
|
|
48
|
-
return result;
|
|
108
|
+
return { pds, handle };
|
|
49
109
|
}
|
|
50
110
|
/**
|
|
51
111
|
* Paginates through all active repos on a relay/PDS using `com.atproto.sync.listRepos`.
|
|
@@ -121,6 +181,20 @@ async function* listReposByCollection(pdsUrl, collection) {
|
|
|
121
181
|
* ```
|
|
122
182
|
*/
|
|
123
183
|
export async function backfillRepo(did, collections, fetchTimeout) {
|
|
184
|
+
// A takendown repo must never be re-imported: the success path ends in
|
|
185
|
+
// setRepoStatus('active'), which would reinstate the account. Nor a
|
|
186
|
+
// deactivated or deleted one, which the network has said is not there.
|
|
187
|
+
// Guarding here covers every caller (boot scan, auto-backfill, admin
|
|
188
|
+
// re-index).
|
|
189
|
+
const current = await getRepoStatus(did);
|
|
190
|
+
if (current === 'takendown') {
|
|
191
|
+
emit('backfill', 'takedown_skip', { did });
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
if (current === 'deactivated' || current === 'deleted') {
|
|
195
|
+
emit('backfill', 'inactive_skip', { did, status: current });
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
124
198
|
const elapsed = timer();
|
|
125
199
|
let count = 0;
|
|
126
200
|
let carSizeBytes;
|
|
@@ -137,6 +211,11 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
137
211
|
const { pds: pdsUrl, handle } = await resolvePds(did);
|
|
138
212
|
resolvedPds = pdsUrl;
|
|
139
213
|
resolvedHandle = handle;
|
|
214
|
+
if (isSkippedPds(pdsUrl)) {
|
|
215
|
+
status = 'skipped';
|
|
216
|
+
await setRepoStatus(did, 'active', undefined, { handle });
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
140
219
|
timeout = setTimeout(() => controller.abort(), fetchTimeout * 1000);
|
|
141
220
|
let lastRev = await getRepoRev(did);
|
|
142
221
|
const baseUrl = `${resolvedPds}/xrpc/com.atproto.sync.getRepo?did=${encodeURIComponent(did)}`;
|
|
@@ -153,39 +232,60 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
153
232
|
throw httpErr;
|
|
154
233
|
}
|
|
155
234
|
resolvedSince = lastRev;
|
|
156
|
-
|
|
235
|
+
let { roots, blocks, byteLength } = await parseCarStream(res.body);
|
|
157
236
|
carSizeBytes = byteLength;
|
|
158
|
-
// Decode commit to get MST root
|
|
159
|
-
|
|
237
|
+
// Decode commit to get MST root — if the diff CAR is missing the root block,
|
|
238
|
+
// fall back to a full import (the PDS compacted past our `since` rev)
|
|
239
|
+
let rootData = blocks.get(roots[0]);
|
|
240
|
+
if (!rootData && lastRev) {
|
|
241
|
+
lastRev = null;
|
|
242
|
+
resolvedSince = null;
|
|
243
|
+
res = await fetch(baseUrl, { signal: controller.signal });
|
|
244
|
+
if (!res.ok) {
|
|
245
|
+
const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
|
|
246
|
+
httpErr.httpStatus = res.status;
|
|
247
|
+
throw httpErr;
|
|
248
|
+
}
|
|
249
|
+
;
|
|
250
|
+
({ roots, blocks, byteLength } = await parseCarStream(res.body));
|
|
251
|
+
carSizeBytes = byteLength;
|
|
252
|
+
rootData = blocks.get(roots[0]);
|
|
253
|
+
}
|
|
160
254
|
if (!rootData)
|
|
161
255
|
throw new Error(`No root block for ${did}`);
|
|
162
256
|
const { value: commit } = cborDecode(rootData);
|
|
163
257
|
// Walk MST to find all record paths
|
|
164
258
|
const entries = walkMst(blocks, commit.data.$link);
|
|
165
259
|
// Delete existing records for this DID before re-importing so deletions are reflected
|
|
166
|
-
// Only on full imports (no since) — diff CARs only contain changes
|
|
260
|
+
// Only on full imports (no since) — diff CARs only contain changes.
|
|
261
|
+
//
|
|
262
|
+
// Only the rows this repo holds. A record the same account wrote into a
|
|
263
|
+
// permissioned space is not in its repo: it lives with the space, is read
|
|
264
|
+
// through the space sync, and stays put across a re-import of the repo.
|
|
167
265
|
if (!lastRev) {
|
|
168
|
-
for (const col of collections) {
|
|
266
|
+
for (const col of purgeableCollections(collections)) {
|
|
169
267
|
const schema = getSchema(col);
|
|
170
268
|
if (!schema)
|
|
171
269
|
continue;
|
|
172
|
-
|
|
270
|
+
const ownRows = `SELECT uri FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`;
|
|
173
271
|
for (const child of schema.children) {
|
|
174
|
-
await runSQL(`DELETE FROM ${child.tableName} WHERE
|
|
272
|
+
await runSQL(`DELETE FROM ${child.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
|
|
175
273
|
}
|
|
176
274
|
for (const union of schema.unions) {
|
|
177
275
|
for (const branch of union.branches) {
|
|
178
|
-
await runSQL(`DELETE FROM ${branch.tableName} WHERE
|
|
276
|
+
await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
|
|
179
277
|
}
|
|
180
278
|
}
|
|
279
|
+
await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`, [did]);
|
|
181
280
|
}
|
|
182
281
|
}
|
|
183
282
|
// Insert records in chunks to limit memory usage
|
|
184
283
|
const CHUNK_SIZE = 1000;
|
|
185
284
|
let chunk = [];
|
|
285
|
+
const validationSkips = {};
|
|
186
286
|
for (const entry of entries) {
|
|
187
287
|
const collection = entry.path.split('/')[0];
|
|
188
|
-
if (!collections.has(collection))
|
|
288
|
+
if (!collections.has(collection) || isPrivateCollection(collection))
|
|
189
289
|
continue;
|
|
190
290
|
const blockData = blocks.get(entry.cid);
|
|
191
291
|
if (!blockData)
|
|
@@ -197,6 +297,12 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
197
297
|
continue;
|
|
198
298
|
const rkey = entry.path.split('/').slice(1).join('/');
|
|
199
299
|
const uri = `at://${did}/${collection}/${rkey}`;
|
|
300
|
+
const validationError = validateRecord(getLexiconArray(), collection, record);
|
|
301
|
+
if (validationError) {
|
|
302
|
+
validationSkips[collection] = (validationSkips[collection] || 0) + 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
noteReferencedRepos(collection, rkey, record);
|
|
200
306
|
chunk.push({ collection, uri, cid: entry.cid, did, record });
|
|
201
307
|
if (chunk.length >= CHUNK_SIZE) {
|
|
202
308
|
count += await bulkInsertRecords(chunk);
|
|
@@ -215,6 +321,10 @@ export async function backfillRepo(did, collections, fetchTimeout) {
|
|
|
215
321
|
if (chunk.length > 0) {
|
|
216
322
|
count += await bulkInsertRecords(chunk);
|
|
217
323
|
}
|
|
324
|
+
const totalSkips = Object.values(validationSkips).reduce((a, b) => a + b, 0);
|
|
325
|
+
if (totalSkips > 0) {
|
|
326
|
+
emit('backfill', 'validation_skips', { did, total: totalSkips, by_collection: validationSkips });
|
|
327
|
+
}
|
|
218
328
|
await setRepoStatus(did, 'active', commit.rev, { handle });
|
|
219
329
|
return count;
|
|
220
330
|
}
|
|
@@ -312,6 +422,7 @@ async function runWorkerPool(items, parallelism, fn) {
|
|
|
312
422
|
export async function runBackfill(opts) {
|
|
313
423
|
const { pdsUrl, collections, config } = opts;
|
|
314
424
|
plcUrl = opts.plcUrl;
|
|
425
|
+
configureSkipPds(config.skipPds);
|
|
315
426
|
const signalCollections = config.signalCollections || [...collections];
|
|
316
427
|
const elapsed = timer();
|
|
317
428
|
const mode = config.repos?.length ? 'pinned repos' : config.fullNetwork ? 'full network' : 'collection signal';
|
|
@@ -323,26 +434,30 @@ export async function runBackfill(opts) {
|
|
|
323
434
|
}
|
|
324
435
|
}
|
|
325
436
|
else if (config.fullNetwork) {
|
|
326
|
-
for
|
|
327
|
-
|
|
437
|
+
for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
|
|
438
|
+
for await (const repo of listRepos(url)) {
|
|
439
|
+
dids.add(repo.did);
|
|
440
|
+
}
|
|
328
441
|
}
|
|
329
442
|
}
|
|
330
443
|
else {
|
|
331
|
-
for (const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
catch (err) {
|
|
338
|
-
// Fall back to listRepos if listReposByCollection not supported
|
|
339
|
-
if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
|
|
340
|
-
for await (const repo of listRepos(pdsUrl)) {
|
|
444
|
+
for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
|
|
445
|
+
for (const col of signalCollections) {
|
|
446
|
+
try {
|
|
447
|
+
for await (const repo of listReposByCollection(url, col)) {
|
|
341
448
|
dids.add(repo.did);
|
|
342
449
|
}
|
|
343
|
-
break;
|
|
344
450
|
}
|
|
345
|
-
|
|
451
|
+
catch (err) {
|
|
452
|
+
// Fall back to listRepos if listReposByCollection not supported
|
|
453
|
+
if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
|
|
454
|
+
for await (const repo of listRepos(url)) {
|
|
455
|
+
dids.add(repo.did);
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
346
461
|
}
|
|
347
462
|
}
|
|
348
463
|
}
|
|
@@ -350,7 +465,7 @@ export async function runBackfill(opts) {
|
|
|
350
465
|
const pending = [];
|
|
351
466
|
for (const did of dids) {
|
|
352
467
|
const status = await getRepoStatus(did);
|
|
353
|
-
if (status
|
|
468
|
+
if (backfillEligible(status)) {
|
|
354
469
|
if (!status)
|
|
355
470
|
await setRepoStatus(did, 'pending');
|
|
356
471
|
pending.push(did);
|
|
@@ -373,7 +488,7 @@ export async function runBackfill(opts) {
|
|
|
373
488
|
parallelism: config.parallelism,
|
|
374
489
|
status: 'success',
|
|
375
490
|
});
|
|
376
|
-
return;
|
|
491
|
+
return 0;
|
|
377
492
|
}
|
|
378
493
|
// 3. Backfill with worker pool
|
|
379
494
|
let totalRecords = 0;
|
|
@@ -397,7 +512,7 @@ export async function runBackfill(opts) {
|
|
|
397
512
|
retryRound++;
|
|
398
513
|
// Wait until the earliest retry_after has passed
|
|
399
514
|
const now = Math.floor(Date.now() / 1000);
|
|
400
|
-
const rows = await querySQL(`SELECT MIN(retry_after) as earliest FROM _repos WHERE status = 'failed' AND retry_after > $1 AND retry_count < $2`, [now, maxRetries]);
|
|
515
|
+
const rows = (await querySQL(`SELECT MIN(retry_after) as earliest FROM _repos WHERE status = 'failed' AND retry_after > $1 AND retry_count < $2`, [now, maxRetries]));
|
|
401
516
|
const earliest = rows[0]?.earliest ? Number(rows[0].earliest) : 0;
|
|
402
517
|
if (earliest > now) {
|
|
403
518
|
await new Promise((resolve) => setTimeout(resolve, (earliest - now) * 1000));
|
|
@@ -431,4 +546,5 @@ export async function runBackfill(opts) {
|
|
|
431
546
|
retry_rounds: retryRound,
|
|
432
547
|
status: failedCount > 0 ? 'partial' : 'success',
|
|
433
548
|
});
|
|
549
|
+
return totalRecords;
|
|
434
550
|
}
|
package/dist/car.js
CHANGED
|
@@ -107,7 +107,7 @@ export async function parseCarStream(body) {
|
|
|
107
107
|
while (len - pos < need) {
|
|
108
108
|
const { done, value } = await reader.read();
|
|
109
109
|
if (done)
|
|
110
|
-
return
|
|
110
|
+
return len - pos >= need;
|
|
111
111
|
byteLength += value.length;
|
|
112
112
|
// Compact: shift remaining data to front when read cursor passes midpoint
|
|
113
113
|
if (pos > 0 && pos > buf.length >>> 1) {
|