@hatk/hatk 0.0.1-alpha.9 → 0.0.1-alpha.92

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 (218) 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 +29 -2
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +142 -51
  7. package/dist/car.d.ts +20 -0
  8. package/dist/car.d.ts.map +1 -1
  9. package/dist/car.js +108 -0
  10. package/dist/cli.js +247 -1043
  11. package/dist/config.d.ts +145 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +61 -8
  14. package/dist/database/adapter-factory.d.ts +6 -0
  15. package/dist/database/adapter-factory.d.ts.map +1 -0
  16. package/dist/database/adapter-factory.js +20 -0
  17. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  18. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  19. package/dist/database/adapters/duckdb-search.js +27 -0
  20. package/dist/database/adapters/duckdb.d.ts +25 -0
  21. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  22. package/dist/database/adapters/duckdb.js +161 -0
  23. package/dist/database/adapters/sqlite-search.d.ts +16 -0
  24. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  25. package/dist/database/adapters/sqlite-search.js +76 -0
  26. package/dist/database/adapters/sqlite.d.ts +18 -0
  27. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  28. package/dist/database/adapters/sqlite.js +88 -0
  29. package/dist/{db.d.ts → database/db.d.ts} +68 -6
  30. package/dist/database/db.d.ts.map +1 -0
  31. package/dist/database/db.js +1775 -0
  32. package/dist/database/dialect.d.ts +45 -0
  33. package/dist/database/dialect.d.ts.map +1 -0
  34. package/dist/database/dialect.js +72 -0
  35. package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
  36. package/dist/database/fts.d.ts.map +1 -0
  37. package/dist/{fts.js → database/fts.js} +122 -34
  38. package/dist/database/index.d.ts +7 -0
  39. package/dist/database/index.d.ts.map +1 -0
  40. package/dist/database/index.js +6 -0
  41. package/dist/database/ports.d.ts +50 -0
  42. package/dist/database/ports.d.ts.map +1 -0
  43. package/dist/database/ports.js +1 -0
  44. package/dist/{schema.d.ts → database/schema.d.ts} +16 -3
  45. package/dist/database/schema.d.ts.map +1 -0
  46. package/dist/{schema.js → database/schema.js} +126 -44
  47. package/dist/dev-entry.d.ts +8 -0
  48. package/dist/dev-entry.d.ts.map +1 -0
  49. package/dist/dev-entry.js +127 -0
  50. package/dist/feeds.d.ts +26 -8
  51. package/dist/feeds.d.ts.map +1 -1
  52. package/dist/feeds.js +72 -16
  53. package/dist/hooks.d.ts +102 -0
  54. package/dist/hooks.d.ts.map +1 -0
  55. package/dist/hooks.js +186 -0
  56. package/dist/hydrate.d.ts +49 -6
  57. package/dist/hydrate.d.ts.map +1 -1
  58. package/dist/hydrate.js +21 -20
  59. package/dist/indexer.d.ts +167 -3
  60. package/dist/indexer.d.ts.map +1 -1
  61. package/dist/indexer.js +556 -94
  62. package/dist/jetstream.d.ts +118 -0
  63. package/dist/jetstream.d.ts.map +1 -0
  64. package/dist/jetstream.js +225 -0
  65. package/dist/labels.d.ts +36 -0
  66. package/dist/labels.d.ts.map +1 -1
  67. package/dist/labels.js +71 -6
  68. package/dist/lex-types.d.ts +2 -0
  69. package/dist/lex-types.d.ts.map +1 -1
  70. package/dist/lexicon-resolve.d.ts.map +1 -1
  71. package/dist/lexicon-resolve.js +27 -112
  72. package/dist/lexicons/com/atproto/label/defs.json +75 -0
  73. package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
  74. package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
  75. package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
  76. package/dist/lexicons/dev/hatk/createRecord.json +40 -0
  77. package/dist/lexicons/dev/hatk/createReport.json +48 -0
  78. package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
  79. package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
  80. package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
  81. package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
  82. package/dist/lexicons/dev/hatk/getFeed.json +30 -0
  83. package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
  84. package/dist/lexicons/dev/hatk/getRecord.json +26 -0
  85. package/dist/lexicons/dev/hatk/getRecords.json +32 -0
  86. package/dist/lexicons/dev/hatk/putPreference.json +28 -0
  87. package/dist/lexicons/dev/hatk/putRecord.json +41 -0
  88. package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
  89. package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
  90. package/dist/logger.d.ts +29 -0
  91. package/dist/logger.d.ts.map +1 -1
  92. package/dist/logger.js +29 -0
  93. package/dist/main.js +198 -75
  94. package/dist/mst.d.ts +15 -0
  95. package/dist/mst.d.ts.map +1 -1
  96. package/dist/mst.js +13 -0
  97. package/dist/oauth/db.d.ts +16 -1
  98. package/dist/oauth/db.d.ts.map +1 -1
  99. package/dist/oauth/db.js +74 -19
  100. package/dist/oauth/discovery.d.ts +10 -0
  101. package/dist/oauth/discovery.d.ts.map +1 -1
  102. package/dist/oauth/discovery.js +54 -2
  103. package/dist/oauth/server.d.ts +93 -1
  104. package/dist/oauth/server.d.ts.map +1 -1
  105. package/dist/oauth/server.js +416 -39
  106. package/dist/oauth/service-describe.d.ts +42 -0
  107. package/dist/oauth/service-describe.d.ts.map +1 -0
  108. package/dist/oauth/service-describe.js +108 -0
  109. package/dist/oauth/session.d.ts +32 -0
  110. package/dist/oauth/session.d.ts.map +1 -0
  111. package/dist/oauth/session.js +131 -0
  112. package/dist/opengraph.d.ts +10 -0
  113. package/dist/opengraph.d.ts.map +1 -1
  114. package/dist/opengraph.js +94 -42
  115. package/dist/pds-proxy.d.ts +82 -0
  116. package/dist/pds-proxy.d.ts.map +1 -0
  117. package/dist/pds-proxy.js +356 -0
  118. package/dist/private-collections.d.ts +13 -0
  119. package/dist/private-collections.d.ts.map +1 -0
  120. package/dist/private-collections.js +27 -0
  121. package/dist/push.d.ts +54 -0
  122. package/dist/push.d.ts.map +1 -0
  123. package/dist/push.js +367 -0
  124. package/dist/renderer.d.ts +27 -0
  125. package/dist/renderer.d.ts.map +1 -0
  126. package/dist/renderer.js +46 -0
  127. package/dist/resolve-hatk.d.ts +6 -0
  128. package/dist/resolve-hatk.d.ts.map +1 -0
  129. package/dist/resolve-hatk.js +20 -0
  130. package/dist/response.d.ts +16 -0
  131. package/dist/response.d.ts.map +1 -0
  132. package/dist/response.js +69 -0
  133. package/dist/scanner.d.ts +21 -0
  134. package/dist/scanner.d.ts.map +1 -0
  135. package/dist/scanner.js +88 -0
  136. package/dist/seed.d.ts +19 -0
  137. package/dist/seed.d.ts.map +1 -1
  138. package/dist/seed.js +43 -4
  139. package/dist/server-init.d.ts +8 -0
  140. package/dist/server-init.d.ts.map +1 -0
  141. package/dist/server-init.js +62 -0
  142. package/dist/server.d.ts +34 -3
  143. package/dist/server.d.ts.map +1 -1
  144. package/dist/server.js +1032 -649
  145. package/dist/setup.d.ts +28 -1
  146. package/dist/setup.d.ts.map +1 -1
  147. package/dist/setup.js +50 -3
  148. package/dist/spaces/blob.d.ts +51 -0
  149. package/dist/spaces/blob.d.ts.map +1 -0
  150. package/dist/spaces/blob.js +111 -0
  151. package/dist/spaces/credential.d.ts +62 -0
  152. package/dist/spaces/credential.d.ts.map +1 -0
  153. package/dist/spaces/credential.js +184 -0
  154. package/dist/spaces/engine.d.ts +97 -0
  155. package/dist/spaces/engine.d.ts.map +1 -0
  156. package/dist/spaces/engine.js +559 -0
  157. package/dist/spaces/guard.d.ts +41 -0
  158. package/dist/spaces/guard.d.ts.map +1 -0
  159. package/dist/spaces/guard.js +79 -0
  160. package/dist/spaces/identity.d.ts +37 -0
  161. package/dist/spaces/identity.d.ts.map +1 -0
  162. package/dist/spaces/identity.js +88 -0
  163. package/dist/spaces/index.d.ts +55 -0
  164. package/dist/spaces/index.d.ts.map +1 -0
  165. package/dist/spaces/index.js +133 -0
  166. package/dist/spaces/notify.d.ts +51 -0
  167. package/dist/spaces/notify.d.ts.map +1 -0
  168. package/dist/spaces/notify.js +121 -0
  169. package/dist/spaces/records.d.ts +37 -0
  170. package/dist/spaces/records.d.ts.map +1 -0
  171. package/dist/spaces/records.js +63 -0
  172. package/dist/spaces/store.d.ts +51 -0
  173. package/dist/spaces/store.d.ts.map +1 -0
  174. package/dist/spaces/store.js +99 -0
  175. package/dist/spaces/uri.d.ts +60 -0
  176. package/dist/spaces/uri.d.ts.map +1 -0
  177. package/dist/spaces/uri.js +97 -0
  178. package/dist/spaces/verify.d.ts +39 -0
  179. package/dist/spaces/verify.d.ts.map +1 -0
  180. package/dist/spaces/verify.js +87 -0
  181. package/dist/spaces/viewer.d.ts +51 -0
  182. package/dist/spaces/viewer.d.ts.map +1 -0
  183. package/dist/spaces/viewer.js +167 -0
  184. package/dist/spaces/visibility.d.ts +57 -0
  185. package/dist/spaces/visibility.d.ts.map +1 -0
  186. package/dist/spaces/visibility.js +81 -0
  187. package/dist/templates/feed.tpl +18 -0
  188. package/dist/templates/hook.tpl +5 -0
  189. package/dist/templates/label.tpl +15 -0
  190. package/dist/templates/og.tpl +17 -0
  191. package/dist/templates/seed.tpl +11 -0
  192. package/dist/templates/setup.tpl +5 -0
  193. package/dist/templates/test-feed.tpl +19 -0
  194. package/dist/templates/test-xrpc.tpl +19 -0
  195. package/dist/templates/xrpc.tpl +53 -0
  196. package/dist/test.d.ts +3 -1
  197. package/dist/test.d.ts.map +1 -1
  198. package/dist/test.js +60 -36
  199. package/dist/views.js +1 -1
  200. package/dist/vite-plugin.d.ts +1 -1
  201. package/dist/vite-plugin.d.ts.map +1 -1
  202. package/dist/vite-plugin.js +254 -66
  203. package/dist/xrpc.d.ts +101 -12
  204. package/dist/xrpc.d.ts.map +1 -1
  205. package/dist/xrpc.js +255 -55
  206. package/package.json +21 -9
  207. package/public/admin-auth.js +271 -306
  208. package/public/admin.html +177 -61
  209. package/dist/db.d.ts.map +0 -1
  210. package/dist/db.js +0 -1361
  211. package/dist/fts.d.ts.map +0 -1
  212. package/dist/oauth/hooks.d.ts +0 -10
  213. package/dist/oauth/hooks.d.ts.map +0 -1
  214. package/dist/oauth/hooks.js +0 -40
  215. package/dist/schema.d.ts.map +0 -1
  216. package/dist/test-browser.d.ts +0 -14
  217. package/dist/test-browser.d.ts.map +0 -1
  218. package/dist/test-browser.js +0 -26
package/dist/setup.d.ts CHANGED
@@ -1,8 +1,35 @@
1
+ import type { BulkInserter } from './database/ports.ts';
2
+ /** Context passed to each setup script's handler function. */
1
3
  export interface SetupContext {
2
4
  db: {
3
5
  query: (sql: string, params?: any[]) => Promise<any[]>;
4
- run: (sql: string, ...params: any[]) => Promise<void>;
6
+ run: (sql: string, params?: any[]) => Promise<void>;
7
+ runBatch: (operations: Array<{
8
+ sql: string;
9
+ params: any[];
10
+ }>) => Promise<void>;
11
+ createBulkInserter: (table: string, columns: string[], options?: {
12
+ onConflict?: 'ignore' | 'replace';
13
+ batchSize?: number;
14
+ }) => Promise<BulkInserter>;
5
15
  };
6
16
  }
17
+ export type SetupHandler = (ctx: SetupContext) => Promise<void>;
18
+ export declare function defineSetup(handler: SetupHandler): {
19
+ __type: "setup";
20
+ handler: SetupHandler;
21
+ };
22
+ /**
23
+ * Run all setup scripts in the given directory on server boot.
24
+ *
25
+ * Each script should export a default handler function (or `{ handler }`) that
26
+ * receives a {@link SetupContext} with database access. Scripts run in sorted
27
+ * filename order — prefix with numbers (e.g. `01-create-tables.ts`) to control
28
+ * execution order. Files starting with `_` are ignored.
29
+ *
30
+ * @param setupDir - Absolute path to the `setup/` directory
31
+ */
7
32
  export declare function initSetup(setupDir: string): Promise<void>;
33
+ /** Run a single setup handler with a SetupContext. */
34
+ export declare function runSetupHandler(name: string, handler: SetupHandler): Promise<void>;
8
35
  //# sourceMappingURL=setup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACtD,CAAA;CACF;AAiBD,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAEvD,8DAA8D;AAC9D,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACnD,QAAQ,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,GAAG,EAAE,CAAA;SAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QAC9E,kBAAkB,EAAE,CAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,CAAC,EAAE;YAAE,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,KAChE,OAAO,CAAC,YAAY,CAAC,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAE/D,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY;;;EAEhD;AAkBD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D;AAED,sDAAsD;AACtD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxF"}
package/dist/setup.js CHANGED
@@ -6,10 +6,38 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ /**
10
+ * Setup scripts that run once on server boot for initializing custom tables,
11
+ * views, or other database state.
12
+ *
13
+ * Place scripts in the `setup/` directory. Each module default-exports a handler
14
+ * function (or `{ handler }`) that receives a {@link SetupContext} with database
15
+ * access. Scripts run in sorted filename order — prefix with numbers to control
16
+ * execution order. Files starting with `_` are ignored.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * // setup/01-leaderboard.ts
21
+ * import type { SetupContext } from '@hatk/hatk/setup'
22
+ *
23
+ * export default async function (ctx: SetupContext) {
24
+ * await ctx.db.run(`
25
+ * CREATE TABLE IF NOT EXISTS leaderboard (
26
+ * did TEXT PRIMARY KEY,
27
+ * score INTEGER DEFAULT 0
28
+ * )
29
+ * `)
30
+ * }
31
+ * ```
32
+ */
9
33
  import { resolve, relative } from 'node:path';
10
34
  import { readdirSync, statSync } from 'node:fs';
11
35
  import { log } from "./logger.js";
12
- import { querySQL, runSQL } from "./db.js";
36
+ import { querySQL, runSQL, runBatch, createBulkInserterSQL } from "./database/db.js";
37
+ export function defineSetup(handler) {
38
+ return { __type: 'setup', handler };
39
+ }
40
+ /** Recursively collect .ts/.js files in a directory, skipping files prefixed with `_`. */
13
41
  function walkDir(dir) {
14
42
  const results = [];
15
43
  try {
@@ -26,23 +54,42 @@ function walkDir(dir) {
26
54
  catch { }
27
55
  return results.sort();
28
56
  }
57
+ /**
58
+ * Run all setup scripts in the given directory on server boot.
59
+ *
60
+ * Each script should export a default handler function (or `{ handler }`) that
61
+ * receives a {@link SetupContext} with database access. Scripts run in sorted
62
+ * filename order — prefix with numbers (e.g. `01-create-tables.ts`) to control
63
+ * execution order. Files starting with `_` are ignored.
64
+ *
65
+ * @param setupDir - Absolute path to the `setup/` directory
66
+ */
29
67
  export async function initSetup(setupDir) {
30
68
  const files = walkDir(setupDir);
31
69
  if (files.length === 0)
32
70
  return;
33
71
  for (const scriptPath of files) {
34
72
  const name = relative(setupDir, scriptPath).replace(/\.(ts|js)$/, '');
35
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
73
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
36
74
  const handler = mod.default?.handler || mod.default;
37
75
  if (typeof handler !== 'function') {
38
76
  console.warn(`[setup] ${name}: no handler function found, skipping`);
39
77
  continue;
40
78
  }
41
79
  const ctx = {
42
- db: { query: querySQL, run: runSQL },
80
+ db: { query: querySQL, run: runSQL, runBatch, createBulkInserter: createBulkInserterSQL },
43
81
  };
44
82
  log(`[setup] running: ${name}`);
45
83
  await handler(ctx);
46
84
  log(`[setup] done: ${name}`);
47
85
  }
48
86
  }
87
+ /** Run a single setup handler with a SetupContext. */
88
+ export async function runSetupHandler(name, handler) {
89
+ const ctx = {
90
+ db: { query: querySQL, run: runSQL, runBatch, createBulkInserter: createBulkInserterSQL },
91
+ };
92
+ log(`[setup] running: ${name}`);
93
+ await handler(ctx);
94
+ log(`[setup] done: ${name}`);
95
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Serving a blob that lives in a permissioned space.
3
+ *
4
+ * Every other image hatk hands out has a public URL — a CDN path, or the
5
+ * repo's own `com.atproto.sync.getBlob`. A space's blob has neither, by
6
+ * design: `com.atproto.space.getBlob` serves it only to a credential holder,
7
+ * and only for a space the blob is actually referenced from. Giving one a
8
+ * public URL would turn that URL into the capability the credential exists to
9
+ * replace.
10
+ *
11
+ * So the bytes come through here, fetched on the viewer's own behalf with the
12
+ * viewer's own credential, and go no further: `private, no-store` keeps them
13
+ * out of every shared cache between this server and that browser.
14
+ */
15
+ import type { OAuthConfig } from '../config.ts';
16
+ /**
17
+ * The URL this appview serves a space blob at, for whoever is asking.
18
+ *
19
+ * Built here rather than by the app, because the route is hatk's: a blob in a
20
+ * space has no public address by design, and this is the one address it does
21
+ * have — answered per viewer, with that viewer's credential, uncacheable.
22
+ */
23
+ export declare function spaceBlobUrl(space: string, repo: string, cid: string): string;
24
+ /**
25
+ * The CID a blob reference names, in whichever spelling the read used.
26
+ *
27
+ * A repo read gives the lexicon's `{ ref: { $link } }`; a space read comes
28
+ * back through a lex client that decodes the same field into a CID object
29
+ * whose `/` is the string. One blob, two spellings, and no caller should have
30
+ * to know which it got.
31
+ */
32
+ export declare function blobCid(blob: unknown): string | undefined;
33
+ export interface SpaceBlobRequest {
34
+ space: string;
35
+ /** The repo holding the blob — the writer's, not the authority's. */
36
+ repo: string;
37
+ cid: string;
38
+ }
39
+ export declare function parseSpaceBlobRequest(params: URLSearchParams): SpaceBlobRequest | null;
40
+ /**
41
+ * Fetch a space blob as this viewer, or answer why not.
42
+ *
43
+ * A viewer who cannot read the space gets the same 404 as a blob that is not
44
+ * there. The distinction between "you may not" and "it does not exist" is
45
+ * itself information about a private space, and the space host draws the line
46
+ * the same way when it reports a non-member's repo as simply absent.
47
+ */
48
+ export declare function serveSpaceBlob(oauth: OAuthConfig | null, viewer: {
49
+ did: string;
50
+ } | null, request: SpaceBlobRequest): Promise<Response>;
51
+ //# sourceMappingURL=blob.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob.d.ts","sourceRoot":"","sources":["../../src/spaces/blob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAe/C;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAG7E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAMzD;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAOtF;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,GAAG,IAAI,EACzB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EAC9B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAsCnB"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Serving a blob that lives in a permissioned space.
3
+ *
4
+ * Every other image hatk hands out has a public URL — a CDN path, or the
5
+ * repo's own `com.atproto.sync.getBlob`. A space's blob has neither, by
6
+ * design: `com.atproto.space.getBlob` serves it only to a credential holder,
7
+ * and only for a space the blob is actually referenced from. Giving one a
8
+ * public URL would turn that URL into the capability the credential exists to
9
+ * replace.
10
+ *
11
+ * So the bytes come through here, fetched on the viewer's own behalf with the
12
+ * viewer's own credential, and go no further: `private, no-store` keeps them
13
+ * out of every shared cache between this server and that browser.
14
+ */
15
+ import { emit } from "../logger.js";
16
+ import { repoEndpoint } from "./identity.js";
17
+ import { parseSpaceRef } from "./uri.js";
18
+ import { viewerCredential } from "./viewer.js";
19
+ /**
20
+ * What we are willing to hand back, whatever the repo claims it is.
21
+ *
22
+ * The record naming this blob was written by an account hatk does not control,
23
+ * and so was its mime type. Echoing that unchecked would let a writer choose
24
+ * what a browser executes in this origin.
25
+ */
26
+ const SERVABLE = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif']);
27
+ /**
28
+ * The URL this appview serves a space blob at, for whoever is asking.
29
+ *
30
+ * Built here rather than by the app, because the route is hatk's: a blob in a
31
+ * space has no public address by design, and this is the one address it does
32
+ * have — answered per viewer, with that viewer's credential, uncacheable.
33
+ */
34
+ export function spaceBlobUrl(space, repo, cid) {
35
+ const params = new URLSearchParams({ space, repo, cid });
36
+ return `/space-blob?${params}`;
37
+ }
38
+ /**
39
+ * The CID a blob reference names, in whichever spelling the read used.
40
+ *
41
+ * A repo read gives the lexicon's `{ ref: { $link } }`; a space read comes
42
+ * back through a lex client that decodes the same field into a CID object
43
+ * whose `/` is the string. One blob, two spellings, and no caller should have
44
+ * to know which it got.
45
+ */
46
+ export function blobCid(blob) {
47
+ const ref = blob?.ref;
48
+ if (!ref || typeof ref !== 'object')
49
+ return undefined;
50
+ if (typeof ref.$link === 'string')
51
+ return ref.$link;
52
+ if (typeof ref['/'] === 'string')
53
+ return ref['/'];
54
+ return undefined;
55
+ }
56
+ export function parseSpaceBlobRequest(params) {
57
+ const space = params.get('space');
58
+ const repo = params.get('repo') ?? params.get('did');
59
+ const cid = params.get('cid');
60
+ if (!space || !repo || !cid)
61
+ return null;
62
+ if (!parseSpaceRef(space) || !repo.startsWith('did:'))
63
+ return null;
64
+ return { space, repo, cid };
65
+ }
66
+ /**
67
+ * Fetch a space blob as this viewer, or answer why not.
68
+ *
69
+ * A viewer who cannot read the space gets the same 404 as a blob that is not
70
+ * there. The distinction between "you may not" and "it does not exist" is
71
+ * itself information about a private space, and the space host draws the line
72
+ * the same way when it reports a non-member's repo as simply absent.
73
+ */
74
+ export async function serveSpaceBlob(oauth, viewer, request) {
75
+ if (!viewer)
76
+ return new Response('Unauthorized', { status: 401 });
77
+ const credential = await viewerCredential(oauth, viewer, request.space);
78
+ if (!credential)
79
+ return new Response('Not found', { status: 404 });
80
+ let upstream;
81
+ try {
82
+ const url = new URL(`${await repoEndpoint(request.repo)}/xrpc/com.atproto.space.getBlob`);
83
+ url.searchParams.set('space', request.space);
84
+ // `repo`, not `did`: a space read names the repo holding the record the
85
+ // same way everywhere. A server hosting one account falls back to it,
86
+ // which is what makes `did` ever appear to work.
87
+ url.searchParams.set('repo', request.repo);
88
+ url.searchParams.set('cid', request.cid);
89
+ upstream = await credential.fetch(url);
90
+ }
91
+ catch (err) {
92
+ emit('spaces', 'blob_error', { space: request.space, repo: request.repo, error: err.message });
93
+ return new Response('Blob unavailable', { status: 502 });
94
+ }
95
+ if (!upstream.ok)
96
+ return new Response('Not found', { status: upstream.status === 401 ? 404 : upstream.status });
97
+ const claimed = (upstream.headers.get('content-type') ?? '').split(';')[0].trim();
98
+ const contentType = SERVABLE.has(claimed) ? claimed : 'application/octet-stream';
99
+ return new Response(upstream.body, {
100
+ status: 200,
101
+ headers: {
102
+ 'content-type': contentType,
103
+ // Not a public cache: this response is one viewer's, and the next
104
+ // request for the same URL may be somebody with no right to it.
105
+ 'cache-control': 'private, no-store',
106
+ 'content-disposition': 'inline',
107
+ 'content-security-policy': "default-src 'none'; sandbox",
108
+ 'x-content-type-options': 'nosniff',
109
+ },
110
+ });
111
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Getting into a space to read it.
3
+ *
4
+ * hatk is not a member of anything. It has no DID that any community has
5
+ * admitted, and asking every authority to admit an indexer would make indexing
6
+ * a per-community act of trust before a single record could be read.
7
+ *
8
+ * So it reads as somebody who is already inside: a viewer whose OAuth session
9
+ * it already holds, whose PDS will mint a delegation token for the space, and
10
+ * whom the authority already lets in. The exchange is the same one the
11
+ * community's own web app makes from the browser —
12
+ *
13
+ * the reader's PDS signs a delegation token (60s, single-use, addressed to
14
+ * the authority's space host)
15
+ * the authority checks it, checks the reader against the space's policy, and
16
+ * returns a credential bound to the key that signed our DPoP proof (2h)
17
+ *
18
+ * — and the credential is then presented to every writer's host in the space.
19
+ * It carries no audience, which is why it must be bound to a key rather than
20
+ * held as a bearer token: a host handed a bearer credential to serve its own
21
+ * repo could replay it against every other host in the space.
22
+ *
23
+ * The consequence to be honest about: a space is only readable while somebody
24
+ * who can read it has a live session here. That is the same bound the data has
25
+ * anyway — nobody outside the space's audience was ever entitled to this — but
26
+ * it does mean indexing lapses when the last member logs out.
27
+ */
28
+ import type { OAuthConfig } from '../config.ts';
29
+ export declare class SpaceCredentialError extends Error {
30
+ readonly status: number;
31
+ readonly code: string;
32
+ constructor(status: number, code: string, message: string);
33
+ }
34
+ /** A refusal about who is asking, rather than about the space or the network. */
35
+ export declare function isNotAuthorized(err: unknown): boolean;
36
+ export declare function isSpaceGone(err: unknown): boolean;
37
+ export interface SpaceCredential {
38
+ /** The space this credential reads, and the only one it is accepted for. */
39
+ space: string;
40
+ /** The account whose delegation bought it. */
41
+ readerDid: string;
42
+ expiresAt: number;
43
+ /** A fetch that presents the credential with a fresh DPoP proof per request. */
44
+ fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
45
+ }
46
+ /** Mint a credential for `space` using `readerDid`'s stored session. */
47
+ export declare function mintSpaceCredential(oauthConfig: OAuthConfig, space: string, readerDid: string): Promise<SpaceCredential>;
48
+ /**
49
+ * A credential for `space`, minted with whichever candidate session works.
50
+ *
51
+ * Candidates are tried in order and the first that succeeds is cached. A
52
+ * refusal about the reader — they were ejected, or never belonged — moves on to
53
+ * the next candidate; a refusal about the space itself stops the whole attempt,
54
+ * since no other reader would fare better.
55
+ */
56
+ export declare function getSpaceCredential(oauthConfig: OAuthConfig, space: string, candidates: string[], opts?: {
57
+ refresh?: boolean;
58
+ }): Promise<SpaceCredential>;
59
+ export declare function forgetSpaceCredential(space: string): void;
60
+ /** For tests: drop every cached credential and the process binding key. */
61
+ export declare function resetSpaceCredentials(): void;
62
+ //# sourceMappingURL=credential.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential.d.ts","sourceRoot":"","sources":["../../src/spaces/credential.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAsB/C,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;gBADZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAIlB;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAErD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEjD;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAA;IACb,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CACtE;AAiDD,wEAAwE;AACxE,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAiC1B;AAUD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAC/B,OAAO,CAAC,eAAe,CAAC,CAwB1B;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Getting into a space to read it.
3
+ *
4
+ * hatk is not a member of anything. It has no DID that any community has
5
+ * admitted, and asking every authority to admit an indexer would make indexing
6
+ * a per-community act of trust before a single record could be read.
7
+ *
8
+ * So it reads as somebody who is already inside: a viewer whose OAuth session
9
+ * it already holds, whose PDS will mint a delegation token for the space, and
10
+ * whom the authority already lets in. The exchange is the same one the
11
+ * community's own web app makes from the browser —
12
+ *
13
+ * the reader's PDS signs a delegation token (60s, single-use, addressed to
14
+ * the authority's space host)
15
+ * the authority checks it, checks the reader against the space's policy, and
16
+ * returns a credential bound to the key that signed our DPoP proof (2h)
17
+ *
18
+ * — and the credential is then presented to every writer's host in the space.
19
+ * It carries no audience, which is why it must be bound to a key rather than
20
+ * held as a bearer token: a host handed a bearer credential to serve its own
21
+ * repo could replay it against every other host in the space.
22
+ *
23
+ * The consequence to be honest about: a space is only readable while somebody
24
+ * who can read it has a live session here. That is the same bound the data has
25
+ * anyway — nobody outside the space's audience was ever entitled to this — but
26
+ * it does mean indexing lapses when the last member logs out.
27
+ */
28
+ import { computeJwkThumbprint, generateKeyPair } from "../oauth/crypto.js";
29
+ import { createDpopProof } from "../oauth/dpop.js";
30
+ import { pdsXrpc } from "../pds-proxy.js";
31
+ import { emit } from "../logger.js";
32
+ import { spaceHostEndpoint } from "./identity.js";
33
+ import { parseSpaceRef } from "./uri.js";
34
+ /** Re-mint this long before the credential's own expiry rather than racing it. */
35
+ const RENEW_LEAD_MS = 5 * 60 * 1000;
36
+ /**
37
+ * The key credentials are bound to, generated once per process and never
38
+ * persisted. It is registered nowhere, so a restart costs one round trip to
39
+ * mint fresh credentials and nothing else.
40
+ */
41
+ let bindingKey = null;
42
+ function getBindingKey() {
43
+ bindingKey ??= generateKeyPair();
44
+ return bindingKey;
45
+ }
46
+ export class SpaceCredentialError extends Error {
47
+ status;
48
+ code;
49
+ constructor(status, code, message) {
50
+ super(message);
51
+ this.status = status;
52
+ this.code = code;
53
+ }
54
+ }
55
+ /** A refusal about who is asking, rather than about the space or the network. */
56
+ export function isNotAuthorized(err) {
57
+ return err instanceof SpaceCredentialError && (err.code === 'UserNotAuthorized' || err.code === 'NotAuthorized');
58
+ }
59
+ export function isSpaceGone(err) {
60
+ return err instanceof SpaceCredentialError && (err.code === 'SpaceDeleted' || err.code === 'SpaceNotFound');
61
+ }
62
+ function expiryOf(jwt) {
63
+ try {
64
+ const payload = JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString());
65
+ return typeof payload.exp === 'number' ? payload.exp * 1000 : Date.now() + 60 * 60 * 1000;
66
+ }
67
+ catch {
68
+ // An unreadable expiry is not a reason to refuse a credential the authority
69
+ // just issued; treat it as short-lived and re-mint sooner.
70
+ return Date.now() + 10 * 60 * 1000;
71
+ }
72
+ }
73
+ /**
74
+ * Exchange a delegation token for a credential at the authority.
75
+ *
76
+ * The proof sent here carries no `ath`: a delegation token is an authorization
77
+ * grant rather than an access token, so there is nothing to hash yet, and a
78
+ * server that checks will refuse a proof that includes one. `dpopJkt` is sent
79
+ * alongside because implementations differ on which they read — the reference
80
+ * reads the proof, others read the field — and they agree here by construction.
81
+ */
82
+ async function exchange(authorityHost, space, delegationToken) {
83
+ const { privateJwk, publicJwk } = await getBindingKey();
84
+ const url = `${authorityHost}/xrpc/com.atproto.space.getSpaceCredential`;
85
+ const proof = await createDpopProof(privateJwk, publicJwk, 'POST', url);
86
+ const dpopJkt = await computeJwkThumbprint(publicJwk);
87
+ const res = await fetch(url, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ Accept: 'application/json',
92
+ Authorization: `Bearer ${delegationToken}`,
93
+ DPoP: proof,
94
+ },
95
+ body: JSON.stringify({ space, dpopJkt }),
96
+ });
97
+ const body = (await res.json().catch(() => ({})));
98
+ if (!res.ok || !body.credential) {
99
+ throw new SpaceCredentialError(res.status, body.error ?? (res.status >= 500 ? 'UpstreamFailure' : 'InvalidRequest'), body.message ?? `getSpaceCredential refused (${res.status})`);
100
+ }
101
+ return body.credential;
102
+ }
103
+ /** Mint a credential for `space` using `readerDid`'s stored session. */
104
+ export async function mintSpaceCredential(oauthConfig, space, readerDid) {
105
+ const ref = parseSpaceRef(space);
106
+ if (!ref)
107
+ throw new SpaceCredentialError(400, 'InvalidRequest', `Not a space ref: ${space}`);
108
+ // The reader's own PDS signs this, under the whole-space read grant their
109
+ // session carries. A session without that scope is refused here, before the
110
+ // authority is ever contacted.
111
+ let token;
112
+ try {
113
+ const out = await pdsXrpc(oauthConfig, { did: readerDid }, 'com.atproto.space.getDelegationToken', {
114
+ params: { space },
115
+ });
116
+ if (typeof out.token !== 'string')
117
+ throw new Error('PDS returned no delegation token');
118
+ token = out.token;
119
+ }
120
+ catch (err) {
121
+ throw new SpaceCredentialError(err?.status ?? 502, 'DelegationFailed', err?.message ?? 'delegation failed');
122
+ }
123
+ const authorityHost = await spaceHostEndpoint(ref.authority);
124
+ const credential = await exchange(authorityHost, space, token);
125
+ const { privateJwk, publicJwk } = await getBindingKey();
126
+ const credentialFetch = async (input, init) => {
127
+ const url = typeof input === 'string' ? input : input.toString();
128
+ const method = (init?.method ?? 'GET').toUpperCase();
129
+ const proof = await createDpopProof(privateJwk, publicJwk, method, url, credential);
130
+ const headers = new Headers(init?.headers);
131
+ headers.set('Authorization', `DPoP ${credential}`);
132
+ headers.set('DPoP', proof);
133
+ return fetch(url, { ...init, method, headers });
134
+ };
135
+ return { space, readerDid, expiresAt: expiryOf(credential), fetch: credentialFetch };
136
+ }
137
+ // --- Cache ---
138
+ const cache = new Map();
139
+ function fresh(cred) {
140
+ return !!cred && cred.expiresAt - RENEW_LEAD_MS > Date.now();
141
+ }
142
+ /**
143
+ * A credential for `space`, minted with whichever candidate session works.
144
+ *
145
+ * Candidates are tried in order and the first that succeeds is cached. A
146
+ * refusal about the reader — they were ejected, or never belonged — moves on to
147
+ * the next candidate; a refusal about the space itself stops the whole attempt,
148
+ * since no other reader would fare better.
149
+ */
150
+ export async function getSpaceCredential(oauthConfig, space, candidates, opts = {}) {
151
+ if (!opts.refresh) {
152
+ const cached = cache.get(space);
153
+ if (fresh(cached))
154
+ return cached;
155
+ }
156
+ let lastError;
157
+ for (const readerDid of candidates) {
158
+ try {
159
+ const credential = await mintSpaceCredential(oauthConfig, space, readerDid);
160
+ cache.set(space, credential);
161
+ return credential;
162
+ }
163
+ catch (err) {
164
+ if (isSpaceGone(err))
165
+ throw err;
166
+ lastError = err;
167
+ emit('spaces', 'credential_refused', {
168
+ space,
169
+ reader_did: readerDid,
170
+ error: err instanceof Error ? err.message : String(err),
171
+ });
172
+ }
173
+ }
174
+ cache.delete(space);
175
+ throw lastError ?? new SpaceCredentialError(403, 'NoReader', `No session on this instance can read ${space}`);
176
+ }
177
+ export function forgetSpaceCredential(space) {
178
+ cache.delete(space);
179
+ }
180
+ /** For tests: drop every cached credential and the process binding key. */
181
+ export function resetSpaceCredentials() {
182
+ cache.clear();
183
+ bindingKey = null;
184
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Keeping the index level with a permissioned space.
3
+ *
4
+ * A space never reaches a firehose. There is no stream to tail and no relay
5
+ * that has seen it, so the only way to know a space has moved is to ask the
6
+ * authority who has written into it and then ask each of those writers' hosts
7
+ * what changed. Two loops do that:
8
+ *
9
+ * reconcile the authority's `listRepos` gives the writer set with each
10
+ * writer's current rev. Anything whose rev has moved is synced;
11
+ * anything the authority no longer names is dropped.
12
+ * sync one writer's repo, forward from the rev we last read, through
13
+ * `listRepoOps` — or wholesale through `listRecords` when there
14
+ * is no rev to start from, or when the op log has been compacted
15
+ * past it.
16
+ *
17
+ * Write notices exist too and are far cheaper, but they are best-effort by
18
+ * design: the reference host forwards them once from an in-memory queue and
19
+ * logs the failure. So reconcile is the thing that makes sync correct, and a
20
+ * notice only makes it prompt.
21
+ *
22
+ * What is deliberately not done here is verification. The alpha signs a commit
23
+ * over the repo's LtHash state, and a syncer can check it. hatk does not check
24
+ * the equivalent on the firehose path either — it reads records out of relayed
25
+ * CAR blocks and trusts them — so checking here would claim a guarantee the
26
+ * rest of the index does not make. Adding it means depending on `@atproto/space`
27
+ * for LtHash and the commit verifier, and is worth doing to both paths at once
28
+ * rather than to this one alone.
29
+ */
30
+ import type { OAuthConfig } from '../config.ts';
31
+ import { type SpaceCredential } from './credential.ts';
32
+ import { type SpaceWatch } from './store.ts';
33
+ export interface SpaceEngineOptions {
34
+ oauth: OAuthConfig;
35
+ /** Space type NSIDs this instance will follow. Anything else is refused. */
36
+ types: Set<string>;
37
+ /** Collections hatk indexes at all; a space collection outside this is skipped. */
38
+ collections: Set<string>;
39
+ /**
40
+ * This instance's service identifier — `did:web:...#atproto_space_syncer`.
41
+ * Absent when the instance receives no notices, in which case the sweep is
42
+ * the only thing that notices a write.
43
+ */
44
+ serviceId?: string;
45
+ }
46
+ export declare function configureSpaceEngine(opts: SpaceEngineOptions): void;
47
+ /**
48
+ * The collections a space of this type holds, narrowed to the ones hatk has a
49
+ * table for.
50
+ *
51
+ * A space type's lexicon declares its collections, which is what makes a space
52
+ * readable without the app being told what is in it — but a space may name
53
+ * collections this instance does not index, and those are simply not its
54
+ * business.
55
+ */
56
+ export declare function collectionsForSpaceType(spaceType: string): string[];
57
+ /** Bring one writer's repo in a space up to date, however that has to happen. */
58
+ export declare function syncSpaceRepo(watch: SpaceWatch, writer: string, credential: SpaceCredential): Promise<void>;
59
+ /**
60
+ * Compare the authority's writer set against what the index holds, and close
61
+ * the difference.
62
+ *
63
+ * The writer set is what the authority has been told, not what is true: a
64
+ * repo's own host is the source of truth, and after a community migrates hosts
65
+ * the set is empty until each writer writes again. So a writer the authority
66
+ * stops naming has their rows dropped, but the rev comparison is only ever a
67
+ * reason to read — never a reason to believe a repo has not moved.
68
+ */
69
+ export declare function reconcileSpace(watch: SpaceWatch): Promise<void>;
70
+ /** Follow a space from now on, and read it once. */
71
+ export declare function watchSpace(space: string): Promise<SpaceWatch>;
72
+ /** Stop following a space and drop everything indexed from it. */
73
+ export declare function unwatchSpace(space: string): Promise<void>;
74
+ /**
75
+ * Act on a write notice: read the one repo it names.
76
+ *
77
+ * Only for a space already followed. A notice about anything else is not an
78
+ * instruction to start following it — that decision belongs to config or to the
79
+ * app, never to whoever can reach this endpoint.
80
+ *
81
+ * The notice's `rev` is deliberately not trusted as the new position: it is
82
+ * read from the repo's own host, which is the source of truth, by the ordinary
83
+ * sync path. The notice only says "look again".
84
+ */
85
+ export declare function handleWriteNotice(notice: {
86
+ space: string;
87
+ repo: string;
88
+ }): Promise<boolean>;
89
+ /**
90
+ * Reconcile every followed space.
91
+ *
92
+ * Failures are per space and recorded rather than thrown: one community's host
93
+ * being unreachable is not a reason to stop reading the others, and a space
94
+ * whose last reader logged out should go quiet rather than take the sweep down.
95
+ */
96
+ export declare function reconcileAll(): Promise<void>;
97
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/spaces/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAQ/C,OAAO,EAA0D,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAE9G,OAAO,EAUL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAA;AAMnB,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,WAAW,CAAA;IAClB,4EAA4E;IAC5E,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAClB,mFAAmF;IACnF,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAaD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAEnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAQnE;AAoUD,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjH;AAkED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCrE;AAID,oDAAoD;AACpD,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAUnE;AAED,kEAAkE;AAClE,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAKjG;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAclD"}