@hatk/hatk 0.0.1-alpha.11 → 0.0.1-alpha.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +113 -0
  4. package/dist/backfill.d.ts +32 -2
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +130 -37
  7. package/dist/car.js +1 -1
  8. package/dist/classifiers.d.ts +228 -0
  9. package/dist/classifiers.d.ts.map +1 -0
  10. package/dist/classifiers.js +688 -0
  11. package/dist/cli.js +248 -1043
  12. package/dist/config.d.ts +171 -1
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +77 -8
  15. package/dist/database/adapter-factory.d.ts +6 -0
  16. package/dist/database/adapter-factory.d.ts.map +1 -0
  17. package/dist/database/adapter-factory.js +20 -0
  18. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  19. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  20. package/dist/database/adapters/duckdb-search.js +27 -0
  21. package/dist/database/adapters/duckdb.d.ts +25 -0
  22. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  23. package/dist/database/adapters/duckdb.js +161 -0
  24. package/dist/database/adapters/sqlite-search.d.ts +16 -0
  25. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  26. package/dist/database/adapters/sqlite-search.js +76 -0
  27. package/dist/database/adapters/sqlite.d.ts +18 -0
  28. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  29. package/dist/database/adapters/sqlite.js +88 -0
  30. package/dist/database/db.d.ts +290 -0
  31. package/dist/database/db.d.ts.map +1 -0
  32. package/dist/database/db.js +2073 -0
  33. package/dist/database/dialect.d.ts +45 -0
  34. package/dist/database/dialect.d.ts.map +1 -0
  35. package/dist/database/dialect.js +72 -0
  36. package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
  37. package/dist/database/fts.d.ts.map +1 -0
  38. package/dist/{fts.js → database/fts.js} +122 -34
  39. package/dist/database/index.d.ts +7 -0
  40. package/dist/database/index.d.ts.map +1 -0
  41. package/dist/database/index.js +6 -0
  42. package/dist/database/ports.d.ts +50 -0
  43. package/dist/database/ports.d.ts.map +1 -0
  44. package/dist/database/ports.js +1 -0
  45. package/dist/database/schema.d.ts +91 -0
  46. package/dist/database/schema.d.ts.map +1 -0
  47. package/dist/{schema.js → database/schema.js} +153 -54
  48. package/dist/dev-entry.d.ts +8 -0
  49. package/dist/dev-entry.d.ts.map +1 -0
  50. package/dist/dev-entry.js +130 -0
  51. package/dist/feeds.d.ts +26 -8
  52. package/dist/feeds.d.ts.map +1 -1
  53. package/dist/feeds.js +72 -16
  54. package/dist/hooks.d.ts +102 -0
  55. package/dist/hooks.d.ts.map +1 -0
  56. package/dist/hooks.js +186 -0
  57. package/dist/hydrate.d.ts +55 -6
  58. package/dist/hydrate.d.ts.map +1 -1
  59. package/dist/hydrate.js +21 -20
  60. package/dist/indexer.d.ts +194 -3
  61. package/dist/indexer.d.ts.map +1 -1
  62. package/dist/indexer.js +627 -92
  63. package/dist/jetstream.d.ts +118 -0
  64. package/dist/jetstream.d.ts.map +1 -0
  65. package/dist/jetstream.js +238 -0
  66. package/dist/labels.d.ts +36 -0
  67. package/dist/labels.d.ts.map +1 -1
  68. package/dist/labels.js +71 -6
  69. package/dist/lex-types.d.ts +2 -0
  70. package/dist/lex-types.d.ts.map +1 -1
  71. package/dist/lexicon-resolve.d.ts.map +1 -1
  72. package/dist/lexicon-resolve.js +27 -112
  73. package/dist/lexicons/com/atproto/label/defs.json +75 -0
  74. package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
  75. package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
  76. package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
  77. package/dist/lexicons/dev/hatk/createRecord.json +40 -0
  78. package/dist/lexicons/dev/hatk/createReport.json +48 -0
  79. package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
  80. package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
  81. package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
  82. package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
  83. package/dist/lexicons/dev/hatk/getFeed.json +30 -0
  84. package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
  85. package/dist/lexicons/dev/hatk/getRecord.json +26 -0
  86. package/dist/lexicons/dev/hatk/getRecords.json +32 -0
  87. package/dist/lexicons/dev/hatk/putPreference.json +28 -0
  88. package/dist/lexicons/dev/hatk/putRecord.json +41 -0
  89. package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
  90. package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
  91. package/dist/logger.d.ts +29 -0
  92. package/dist/logger.d.ts.map +1 -1
  93. package/dist/logger.js +29 -0
  94. package/dist/main.js +211 -75
  95. package/dist/mst.d.ts +15 -0
  96. package/dist/mst.d.ts.map +1 -1
  97. package/dist/mst.js +13 -0
  98. package/dist/oauth/db.d.ts +27 -1
  99. package/dist/oauth/db.d.ts.map +1 -1
  100. package/dist/oauth/db.js +80 -19
  101. package/dist/oauth/discovery.d.ts +10 -0
  102. package/dist/oauth/discovery.d.ts.map +1 -1
  103. package/dist/oauth/discovery.js +54 -2
  104. package/dist/oauth/server.d.ts +131 -1
  105. package/dist/oauth/server.d.ts.map +1 -1
  106. package/dist/oauth/server.js +504 -39
  107. package/dist/oauth/service-describe.d.ts +42 -0
  108. package/dist/oauth/service-describe.d.ts.map +1 -0
  109. package/dist/oauth/service-describe.js +108 -0
  110. package/dist/oauth/session.d.ts +32 -0
  111. package/dist/oauth/session.d.ts.map +1 -0
  112. package/dist/oauth/session.js +131 -0
  113. package/dist/opengraph.d.ts +10 -0
  114. package/dist/opengraph.d.ts.map +1 -1
  115. package/dist/opengraph.js +94 -42
  116. package/dist/pds-proxy.d.ts +92 -0
  117. package/dist/pds-proxy.d.ts.map +1 -0
  118. package/dist/pds-proxy.js +396 -0
  119. package/dist/private-collections.d.ts +13 -0
  120. package/dist/private-collections.d.ts.map +1 -0
  121. package/dist/private-collections.js +27 -0
  122. package/dist/push.d.ts +54 -0
  123. package/dist/push.d.ts.map +1 -0
  124. package/dist/push.js +367 -0
  125. package/dist/renderer.d.ts +27 -0
  126. package/dist/renderer.d.ts.map +1 -0
  127. package/dist/renderer.js +46 -0
  128. package/dist/resolve-hatk.d.ts +6 -0
  129. package/dist/resolve-hatk.d.ts.map +1 -0
  130. package/dist/resolve-hatk.js +20 -0
  131. package/dist/response.d.ts +16 -0
  132. package/dist/response.d.ts.map +1 -0
  133. package/dist/response.js +69 -0
  134. package/dist/scanner.d.ts +22 -0
  135. package/dist/scanner.d.ts.map +1 -0
  136. package/dist/scanner.js +92 -0
  137. package/dist/seed.d.ts +19 -0
  138. package/dist/seed.d.ts.map +1 -1
  139. package/dist/seed.js +43 -4
  140. package/dist/server-init.d.ts +8 -0
  141. package/dist/server-init.d.ts.map +1 -0
  142. package/dist/server-init.js +75 -0
  143. package/dist/server.d.ts +46 -3
  144. package/dist/server.d.ts.map +1 -1
  145. package/dist/server.js +1137 -649
  146. package/dist/setup.d.ts +28 -1
  147. package/dist/setup.d.ts.map +1 -1
  148. package/dist/setup.js +50 -3
  149. package/dist/spaces/blob.d.ts +112 -0
  150. package/dist/spaces/blob.d.ts.map +1 -0
  151. package/dist/spaces/blob.js +460 -0
  152. package/dist/spaces/credential.d.ts +82 -0
  153. package/dist/spaces/credential.d.ts.map +1 -0
  154. package/dist/spaces/credential.js +220 -0
  155. package/dist/spaces/engine.d.ts +97 -0
  156. package/dist/spaces/engine.d.ts.map +1 -0
  157. package/dist/spaces/engine.js +563 -0
  158. package/dist/spaces/guard.d.ts +41 -0
  159. package/dist/spaces/guard.d.ts.map +1 -0
  160. package/dist/spaces/guard.js +79 -0
  161. package/dist/spaces/identity.d.ts +37 -0
  162. package/dist/spaces/identity.d.ts.map +1 -0
  163. package/dist/spaces/identity.js +93 -0
  164. package/dist/spaces/index.d.ts +56 -0
  165. package/dist/spaces/index.d.ts.map +1 -0
  166. package/dist/spaces/index.js +134 -0
  167. package/dist/spaces/notify.d.ts +60 -0
  168. package/dist/spaces/notify.d.ts.map +1 -0
  169. package/dist/spaces/notify.js +128 -0
  170. package/dist/spaces/records.d.ts +37 -0
  171. package/dist/spaces/records.d.ts.map +1 -0
  172. package/dist/spaces/records.js +63 -0
  173. package/dist/spaces/signature.d.ts +36 -0
  174. package/dist/spaces/signature.d.ts.map +1 -0
  175. package/dist/spaces/signature.js +83 -0
  176. package/dist/spaces/store.d.ts +51 -0
  177. package/dist/spaces/store.d.ts.map +1 -0
  178. package/dist/spaces/store.js +99 -0
  179. package/dist/spaces/uri.d.ts +60 -0
  180. package/dist/spaces/uri.d.ts.map +1 -0
  181. package/dist/spaces/uri.js +97 -0
  182. package/dist/spaces/verify.d.ts +46 -0
  183. package/dist/spaces/verify.d.ts.map +1 -0
  184. package/dist/spaces/verify.js +94 -0
  185. package/dist/spaces/viewer.d.ts +51 -0
  186. package/dist/spaces/viewer.d.ts.map +1 -0
  187. package/dist/spaces/viewer.js +167 -0
  188. package/dist/spaces/visibility.d.ts +57 -0
  189. package/dist/spaces/visibility.d.ts.map +1 -0
  190. package/dist/spaces/visibility.js +81 -0
  191. package/dist/templates/feed.tpl +18 -0
  192. package/dist/templates/hook.tpl +5 -0
  193. package/dist/templates/label.tpl +15 -0
  194. package/dist/templates/og.tpl +17 -0
  195. package/dist/templates/seed.tpl +11 -0
  196. package/dist/templates/setup.tpl +5 -0
  197. package/dist/templates/test-feed.tpl +19 -0
  198. package/dist/templates/test-xrpc.tpl +19 -0
  199. package/dist/templates/xrpc.tpl +53 -0
  200. package/dist/test.d.ts +3 -1
  201. package/dist/test.d.ts.map +1 -1
  202. package/dist/test.js +60 -36
  203. package/dist/views.js +1 -1
  204. package/dist/vite-plugin.d.ts +1 -1
  205. package/dist/vite-plugin.d.ts.map +1 -1
  206. package/dist/vite-plugin.js +254 -66
  207. package/dist/xrpc.d.ts +143 -12
  208. package/dist/xrpc.d.ts.map +1 -1
  209. package/dist/xrpc.js +282 -55
  210. package/package.json +29 -9
  211. package/public/admin-auth.js +271 -306
  212. package/public/admin.html +652 -61
  213. package/dist/db.d.ts +0 -135
  214. package/dist/db.d.ts.map +0 -1
  215. package/dist/db.js +0 -1365
  216. package/dist/fts.d.ts.map +0 -1
  217. package/dist/oauth/hooks.d.ts +0 -10
  218. package/dist/oauth/hooks.d.ts.map +0 -1
  219. package/dist/oauth/hooks.js +0 -40
  220. package/dist/schema.d.ts +0 -51
  221. package/dist/schema.d.ts.map +0 -1
  222. package/dist/test-browser.d.ts +0 -14
  223. package/dist/test-browser.d.ts.map +0 -1
  224. 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"}
@@ -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
+ }
@@ -1,15 +1,45 @@
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.yaml`. */
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
+ /** A repo's PDS endpoint, cached for the process. For serving blobs, where a
41
+ * stale answer after a migration costs one broken image, not a wrong row. */
42
+ export declare function pdsFor(did: string): Promise<string>;
13
43
  /**
14
44
  * Downloads and indexes a single user's repo via `com.atproto.sync.getRepo`.
15
45
  *
@@ -65,6 +95,6 @@ export declare function backfillRepo(did: string, collections: Set<string>, fetc
65
95
  * })
66
96
  * ```
67
97
  */
68
- export declare function runBackfill(opts: BackfillOpts): Promise<void>;
98
+ export declare function runBackfill(opts: BackfillOpts): Promise<number>;
69
99
  export {};
70
100
  //# sourceMappingURL=backfill.d.ts.map
@@ -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;AAEjD,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,qDAAqD;IACrD,MAAM,EAAE,cAAc,CAAA;CACvB;AAuGD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAwI/G;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiInE"}
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;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,CAsL/G;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAsIrE"}
package/dist/backfill.js CHANGED
@@ -1,16 +1,64 @@
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
- /** In-memory cache of DID → PDS resolution results to avoid redundant lookups. */
7
- const pdsCache = new Map();
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
+ const pdsCache = new Map();
45
+ /** A repo's PDS endpoint, cached for the process. For serving blobs, where a
46
+ * stale answer after a migration costs one broken image, not a wrong row. */
47
+ export async function pdsFor(did) {
48
+ const cached = pdsCache.get(did);
49
+ if (cached)
50
+ return cached;
51
+ const { pds } = await resolvePds(did);
52
+ pdsCache.set(did, pds);
53
+ return pds;
54
+ }
9
55
  /**
10
56
  * Resolves a DID to its PDS endpoint and handle by fetching the DID document.
11
57
  *
12
58
  * Supports both `did:web` (fetches `/.well-known/did.json`) and `did:plc`
13
- * (fetches from the PLC directory). Results are cached for the lifetime of the process.
59
+ * (fetches from the PLC directory). Always fetches fresh — DID docs change
60
+ * (handle renames, PDS migrations) and a stale cache silently rewrites stale
61
+ * handles back into `_repos` on every backfill.
14
62
  *
15
63
  * @example
16
64
  * ```ts
@@ -20,9 +68,6 @@ let plcUrl;
20
68
  * ```
21
69
  */
22
70
  async function resolvePds(did) {
23
- const cached = pdsCache.get(did);
24
- if (cached)
25
- return cached;
26
71
  let didDoc;
27
72
  if (did.startsWith('did:web:')) {
28
73
  const domain = did.slice('did:web:'.length);
@@ -40,12 +85,10 @@ async function resolvePds(did) {
40
85
  const pds = didDoc.service?.find((s) => s.id === '#atproto_pds')?.serviceEndpoint;
41
86
  if (!pds)
42
87
  throw new Error(`No PDS endpoint in DID document for ${did}`);
43
- // Extract handle from alsoKnownAs (format: "at://handle")
88
+ // First at:// entry in alsoKnownAs is the canonical handle (per @atproto/identity convention)
44
89
  const aka = didDoc.alsoKnownAs?.find((u) => u.startsWith('at://'));
45
90
  const handle = aka ? aka.slice('at://'.length) : null;
46
- const result = { pds, handle };
47
- pdsCache.set(did, result);
48
- return result;
91
+ return { pds, handle };
49
92
  }
50
93
  /**
51
94
  * Paginates through all active repos on a relay/PDS using `com.atproto.sync.listRepos`.
@@ -121,6 +164,20 @@ async function* listReposByCollection(pdsUrl, collection) {
121
164
  * ```
122
165
  */
123
166
  export async function backfillRepo(did, collections, fetchTimeout) {
167
+ // A takendown repo must never be re-imported: the success path ends in
168
+ // setRepoStatus('active'), which would reinstate the account. Nor a
169
+ // deactivated or deleted one, which the network has said is not there.
170
+ // Guarding here covers every caller (boot scan, auto-backfill, admin
171
+ // re-index).
172
+ const current = await getRepoStatus(did);
173
+ if (current === 'takendown') {
174
+ emit('backfill', 'takedown_skip', { did });
175
+ return 0;
176
+ }
177
+ if (current === 'deactivated' || current === 'deleted') {
178
+ emit('backfill', 'inactive_skip', { did, status: current });
179
+ return 0;
180
+ }
124
181
  const elapsed = timer();
125
182
  let count = 0;
126
183
  let carSizeBytes;
@@ -153,39 +210,60 @@ export async function backfillRepo(did, collections, fetchTimeout) {
153
210
  throw httpErr;
154
211
  }
155
212
  resolvedSince = lastRev;
156
- const { roots, blocks, byteLength } = await parseCarStream(res.body);
213
+ let { roots, blocks, byteLength } = await parseCarStream(res.body);
157
214
  carSizeBytes = byteLength;
158
- // Decode commit to get MST root
159
- const rootData = blocks.get(roots[0]);
215
+ // Decode commit to get MST root — if the diff CAR is missing the root block,
216
+ // fall back to a full import (the PDS compacted past our `since` rev)
217
+ let rootData = blocks.get(roots[0]);
218
+ if (!rootData && lastRev) {
219
+ lastRev = null;
220
+ resolvedSince = null;
221
+ res = await fetch(baseUrl, { signal: controller.signal });
222
+ if (!res.ok) {
223
+ const httpErr = new Error(`getRepo failed for ${did}: ${res.status}`);
224
+ httpErr.httpStatus = res.status;
225
+ throw httpErr;
226
+ }
227
+ ;
228
+ ({ roots, blocks, byteLength } = await parseCarStream(res.body));
229
+ carSizeBytes = byteLength;
230
+ rootData = blocks.get(roots[0]);
231
+ }
160
232
  if (!rootData)
161
233
  throw new Error(`No root block for ${did}`);
162
234
  const { value: commit } = cborDecode(rootData);
163
235
  // Walk MST to find all record paths
164
236
  const entries = walkMst(blocks, commit.data.$link);
165
237
  // 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
238
+ // Only on full imports (no since) — diff CARs only contain changes.
239
+ //
240
+ // Only the rows this repo holds. A record the same account wrote into a
241
+ // permissioned space is not in its repo: it lives with the space, is read
242
+ // through the space sync, and stays put across a re-import of the repo.
167
243
  if (!lastRev) {
168
- for (const col of collections) {
244
+ for (const col of purgeableCollections(collections)) {
169
245
  const schema = getSchema(col);
170
246
  if (!schema)
171
247
  continue;
172
- await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1`, did);
248
+ const ownRows = `SELECT uri FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`;
173
249
  for (const child of schema.children) {
174
- await runSQL(`DELETE FROM ${child.tableName} WHERE parent_did = $1`, did);
250
+ await runSQL(`DELETE FROM ${child.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
175
251
  }
176
252
  for (const union of schema.unions) {
177
253
  for (const branch of union.branches) {
178
- await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_did = $1`, did);
254
+ await runSQL(`DELETE FROM ${branch.tableName} WHERE parent_uri IN (${ownRows})`, [did]);
179
255
  }
180
256
  }
257
+ await runSQL(`DELETE FROM ${schema.tableName} WHERE did = $1 AND space IS NULL`, [did]);
181
258
  }
182
259
  }
183
260
  // Insert records in chunks to limit memory usage
184
261
  const CHUNK_SIZE = 1000;
185
262
  let chunk = [];
263
+ const validationSkips = {};
186
264
  for (const entry of entries) {
187
265
  const collection = entry.path.split('/')[0];
188
- if (!collections.has(collection))
266
+ if (!collections.has(collection) || isPrivateCollection(collection))
189
267
  continue;
190
268
  const blockData = blocks.get(entry.cid);
191
269
  if (!blockData)
@@ -197,6 +275,12 @@ export async function backfillRepo(did, collections, fetchTimeout) {
197
275
  continue;
198
276
  const rkey = entry.path.split('/').slice(1).join('/');
199
277
  const uri = `at://${did}/${collection}/${rkey}`;
278
+ const validationError = validateRecord(getLexiconArray(), collection, record);
279
+ if (validationError) {
280
+ validationSkips[collection] = (validationSkips[collection] || 0) + 1;
281
+ continue;
282
+ }
283
+ noteReferencedRepos(collection, rkey, record);
200
284
  chunk.push({ collection, uri, cid: entry.cid, did, record });
201
285
  if (chunk.length >= CHUNK_SIZE) {
202
286
  count += await bulkInsertRecords(chunk);
@@ -215,6 +299,10 @@ export async function backfillRepo(did, collections, fetchTimeout) {
215
299
  if (chunk.length > 0) {
216
300
  count += await bulkInsertRecords(chunk);
217
301
  }
302
+ const totalSkips = Object.values(validationSkips).reduce((a, b) => a + b, 0);
303
+ if (totalSkips > 0) {
304
+ emit('backfill', 'validation_skips', { did, total: totalSkips, by_collection: validationSkips });
305
+ }
218
306
  await setRepoStatus(did, 'active', commit.rev, { handle });
219
307
  return count;
220
308
  }
@@ -323,26 +411,30 @@ export async function runBackfill(opts) {
323
411
  }
324
412
  }
325
413
  else if (config.fullNetwork) {
326
- for await (const repo of listRepos(pdsUrl)) {
327
- dids.add(repo.did);
414
+ for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
415
+ for await (const repo of listRepos(url)) {
416
+ dids.add(repo.did);
417
+ }
328
418
  }
329
419
  }
330
420
  else {
331
- for (const col of signalCollections) {
332
- try {
333
- for await (const repo of listReposByCollection(pdsUrl, col)) {
334
- dids.add(repo.did);
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)) {
421
+ for (const url of [pdsUrl, ...(opts.extraPdsUrls ?? [])]) {
422
+ for (const col of signalCollections) {
423
+ try {
424
+ for await (const repo of listReposByCollection(url, col)) {
341
425
  dids.add(repo.did);
342
426
  }
343
- break;
344
427
  }
345
- throw err;
428
+ catch (err) {
429
+ // Fall back to listRepos if listReposByCollection not supported
430
+ if (err.message.includes('400') || err.message.includes('401') || err.message.includes('501')) {
431
+ for await (const repo of listRepos(url)) {
432
+ dids.add(repo.did);
433
+ }
434
+ break;
435
+ }
436
+ throw err;
437
+ }
346
438
  }
347
439
  }
348
440
  }
@@ -350,7 +442,7 @@ export async function runBackfill(opts) {
350
442
  const pending = [];
351
443
  for (const did of dids) {
352
444
  const status = await getRepoStatus(did);
353
- if (status !== 'active') {
445
+ if (backfillEligible(status)) {
354
446
  if (!status)
355
447
  await setRepoStatus(did, 'pending');
356
448
  pending.push(did);
@@ -373,7 +465,7 @@ export async function runBackfill(opts) {
373
465
  parallelism: config.parallelism,
374
466
  status: 'success',
375
467
  });
376
- return;
468
+ return 0;
377
469
  }
378
470
  // 3. Backfill with worker pool
379
471
  let totalRecords = 0;
@@ -397,7 +489,7 @@ export async function runBackfill(opts) {
397
489
  retryRound++;
398
490
  // Wait until the earliest retry_after has passed
399
491
  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]);
492
+ 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
493
  const earliest = rows[0]?.earliest ? Number(rows[0].earliest) : 0;
402
494
  if (earliest > now) {
403
495
  await new Promise((resolve) => setTimeout(resolve, (earliest - now) * 1000));
@@ -431,4 +523,5 @@ export async function runBackfill(opts) {
431
523
  retry_rounds: retryRound,
432
524
  status: failedCount > 0 ? 'partial' : 'success',
433
525
  });
526
+ return totalRecords;
434
527
  }
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 (len - pos) >= need;
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) {