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

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 +137 -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 +156 -3
  60. package/dist/indexer.d.ts.map +1 -1
  61. package/dist/indexer.js +539 -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 +1021 -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/feeds.js CHANGED
@@ -9,8 +9,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { resolve } from 'node:path';
10
10
  import { readdirSync } from 'node:fs';
11
11
  import { log } from "./logger.js";
12
- import { querySQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids } from "./db.js";
13
- import { resolveRecords, buildHydrateContext } from "./hydrate.js";
12
+ import { packCursor, unpackCursor, isTakendownDid, filterTakendownDids, resolveHandleToDid, } from "./database/db.js";
13
+ import { resolveRecords, buildBaseContext } from "./hydrate.js";
14
+ import { spaceFilterSql } from "./spaces/visibility.js";
15
+ import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
14
16
  export function createPaginate(deps) {
15
17
  return async (sql, opts) => {
16
18
  const { db, cursor, limit, packCursor: pack, unpackCursor: unpack } = deps;
@@ -25,6 +27,16 @@ export function createPaginate(deps) {
25
27
  const op = order === 'ASC' ? '>' : '<';
26
28
  let paramIdx = userParams.length + 1;
27
29
  const sqlParams = [...userParams];
30
+ // The permissioned-space gate, applied to whatever table the ordering
31
+ // names — the same table `cid` is read from. Feed SQL is hand-written and
32
+ // nothing can inject a predicate into a string somebody else wrote, so
33
+ // building it here is the only way every feed gets it without every feed
34
+ // author remembering to. Outside a viewer's scope it is `space IS NULL`
35
+ // and binds nothing, which is what it compiles to on an instance that
36
+ // indexes no space.
37
+ const gate = spaceFilterSql(prefix.replace(/\.$/, ''), paramIdx);
38
+ sqlParams.push(...gate.params);
39
+ paramIdx = gate.nextIdx;
28
40
  // Build cursor condition
29
41
  let cursorCondition = '';
30
42
  if (cursor) {
@@ -35,15 +47,12 @@ export function createPaginate(deps) {
35
47
  paramIdx += 3;
36
48
  }
37
49
  }
38
- // Detect existing WHERE and append cursor condition
50
+ // Detect existing WHERE and append the gate and the cursor condition
39
51
  let fullSql = sql;
40
- if (cursorCondition) {
41
- if (/\bWHERE\b/i.test(sql)) {
42
- fullSql += ` AND ${cursorCondition}`;
43
- }
44
- else {
45
- fullSql += ` WHERE ${cursorCondition}`;
46
- }
52
+ const appended = [gate.sql, ...(cursorCondition ? [cursorCondition] : [])];
53
+ if (appended.length) {
54
+ const joiner = /\bWHERE\b/i.test(sql) ? 'AND' : 'WHERE';
55
+ fullSql += ` ${joiner} ${appended.join(' AND ')}`;
47
56
  }
48
57
  fullSql += ` ORDER BY ${orderBy} ${order}, ${cidCol} ${order} LIMIT $${paramIdx}`;
49
58
  sqlParams.push(limit + 1);
@@ -59,7 +68,47 @@ export function createPaginate(deps) {
59
68
  };
60
69
  }
61
70
  export function defineFeed(opts) {
62
- return { ...opts, generate: (ctx) => opts.generate({ ...ctx, ok: (v) => v }) };
71
+ return { __type: 'feed', ...opts, generate: (ctx) => opts.generate({ ...ctx, ok: (v) => v }) };
72
+ }
73
+ /** Register a single feed from a scanned server/ module. */
74
+ export function registerFeed(name, generator) {
75
+ const handler = {
76
+ name,
77
+ label: generator.label || name,
78
+ collection: generator.collection,
79
+ view: generator.view,
80
+ generate: async (params, cursor, limit, viewer) => {
81
+ const paginateDeps = {
82
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
83
+ cursor,
84
+ limit,
85
+ packCursor,
86
+ unpackCursor,
87
+ };
88
+ const ctx = {
89
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
90
+ params,
91
+ cursor,
92
+ limit,
93
+ viewer,
94
+ packCursor,
95
+ unpackCursor,
96
+ isTakendown: isTakendownDid,
97
+ filterTakendownDids,
98
+ paginate: createPaginate(paginateDeps),
99
+ spaceFilter: spaceFilterSql,
100
+ };
101
+ const result = await generator.generate(ctx);
102
+ if (Array.isArray(result)) {
103
+ return { uris: result.map((r) => r.uri || r) };
104
+ }
105
+ return { uris: result.uris, cursor: result.cursor };
106
+ },
107
+ };
108
+ if (typeof generator.hydrate === 'function') {
109
+ handler.hydrate = generator.hydrate;
110
+ }
111
+ feeds.set(name, handler);
63
112
  }
64
113
  const feeds = new Map();
65
114
  export async function initFeeds(feedsDir) {
@@ -75,7 +124,7 @@ export async function initFeeds(feedsDir) {
75
124
  for (const file of files) {
76
125
  const name = file.replace(/\.(ts|js)$/, '');
77
126
  const scriptPath = resolve(feedsDir, file);
78
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
127
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
79
128
  const generator = mod.default;
80
129
  const handler = {
81
130
  name,
@@ -84,14 +133,14 @@ export async function initFeeds(feedsDir) {
84
133
  view: generator.view,
85
134
  generate: async (params, cursor, limit, viewer) => {
86
135
  const paginateDeps = {
87
- db: { query: querySQL },
136
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
88
137
  cursor,
89
138
  limit,
90
139
  packCursor,
91
140
  unpackCursor,
92
141
  };
93
142
  const ctx = {
94
- db: { query: querySQL },
143
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
95
144
  params,
96
145
  cursor,
97
146
  limit,
@@ -101,6 +150,7 @@ export async function initFeeds(feedsDir) {
101
150
  isTakendown: isTakendownDid,
102
151
  filterTakendownDids,
103
152
  paginate: createPaginate(paginateDeps),
153
+ spaceFilter: spaceFilterSql,
104
154
  };
105
155
  const result = await generator.generate(ctx);
106
156
  if (Array.isArray(result)) {
@@ -127,11 +177,17 @@ export async function executeFeed(name, params, cursor, limit, viewer) {
127
177
  const handler = feeds.get(name);
128
178
  if (!handler)
129
179
  return null;
180
+ // Resolve handle-based actor param to DID
181
+ if (params.actor && !params.actor.startsWith('did:')) {
182
+ const resolved = await resolveHandleToDid(params.actor);
183
+ if (resolved)
184
+ params.actor = resolved;
185
+ }
130
186
  const result = await handler.generate(params, cursor, limit, viewer || null);
131
187
  if (handler.hydrate) {
132
188
  const items = await resolveRecords(result.uris);
133
- const ctx = buildHydrateContext(items, viewer || null);
134
- const hydrated = await handler.hydrate(ctx);
189
+ const ctx = buildBaseContext(viewer || null);
190
+ const hydrated = await handler.hydrate(ctx, items);
135
191
  return { items: hydrated, cursor: result.cursor };
136
192
  }
137
193
  return { uris: result.uris, cursor: result.cursor };
@@ -0,0 +1,102 @@
1
+ import type { OAuthConfig } from './config.ts';
2
+ import { type BaseContext } from './hydrate.ts';
3
+ import { type PushInterface } from './push.ts';
4
+ /** Context passed to the on-login hook after a successful OAuth login. */
5
+ export type OnLoginCtx = Omit<BaseContext, 'db'> & {
6
+ /** DID of the user who just logged in. */
7
+ did: string;
8
+ /** Database access with both read and write. */
9
+ db: {
10
+ query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
11
+ run: (sql: string, params?: unknown[]) => Promise<void>;
12
+ /**
13
+ * Raw SQL with the permissioned-space guard off. A hook is not serving a
14
+ * viewer, and discovering what a space contains — which spaces a
15
+ * community lists, say — is exactly a read over space rows that no scope
16
+ * is open for. See `spaces/guard.ts`.
17
+ */
18
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
19
+ };
20
+ /** Trigger a backfill for a DID and wait for it to complete. */
21
+ ensureRepo: (did: string) => Promise<void>;
22
+ /** Write a record to the user's PDS and index locally. */
23
+ createRecord: (collection: string, record: Record<string, unknown>, opts?: {
24
+ rkey?: string;
25
+ }) => Promise<{
26
+ uri?: string;
27
+ cid?: string;
28
+ }>;
29
+ /** Create or update a record on the user's PDS and index locally. */
30
+ putRecord: (collection: string, rkey: string, record: Record<string, unknown>) => Promise<{
31
+ uri?: string;
32
+ cid?: string;
33
+ }>;
34
+ /** Delete a record from the user's PDS and local index. */
35
+ deleteRecord: (collection: string, rkey: string) => Promise<void>;
36
+ };
37
+ /** Context passed to on-commit hooks after a record is indexed. */
38
+ export type OnCommitCtx = {
39
+ /** Whether the record was created or deleted. */
40
+ action: 'create' | 'delete';
41
+ /** The collection NSID that matched. */
42
+ collection: string;
43
+ /** The record value (null for deletes). */
44
+ record: Record<string, any> | null;
45
+ /** DID of the committing actor. */
46
+ repo: string;
47
+ /** AT URI of the record. */
48
+ uri: string;
49
+ /** Database access (read and write). */
50
+ db: {
51
+ query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
52
+ run: (sql: string, params?: unknown[]) => Promise<void>;
53
+ /** Raw SQL with the permissioned-space guard off. See `spaces/guard.ts`. */
54
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
55
+ };
56
+ /** Typed record lookup (same as BaseContext). */
57
+ lookup: BaseContext['lookup'];
58
+ /** Push notification delivery. */
59
+ push: PushInterface;
60
+ };
61
+ export declare function defineHook(event: 'on-login', handler: (ctx: OnLoginCtx) => Promise<void>): {
62
+ __type: 'hook';
63
+ event: 'on-login';
64
+ handler: (ctx: OnLoginCtx) => Promise<void>;
65
+ };
66
+ export declare function defineHook(event: 'on-commit', options: {
67
+ collections: string[];
68
+ }, handler: (ctx: OnCommitCtx) => Promise<void>): {
69
+ __type: 'hook';
70
+ event: 'on-commit';
71
+ collections: string[];
72
+ handler: (ctx: OnCommitCtx) => Promise<void>;
73
+ };
74
+ /**
75
+ * Discover and load the on-login hook from the project's `hooks/` directory.
76
+ * Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
77
+ *
78
+ * The documented contract is `export default defineHook('on-login', handler)`,
79
+ * which is an object carrying the handler — the same shape the `server/`
80
+ * scanner recognizes by its `__type` tag. A bare async function is still
81
+ * accepted, because that is what the legacy `hooks/` directory used before
82
+ * `defineHook` existed. Anything else is reported rather than installed: a
83
+ * non-callable hook would otherwise fail silently on every login, since
84
+ * `fireOnLoginHook` swallows what the hook throws.
85
+ */
86
+ export declare function loadOnLoginHook(hooksDir: string): Promise<void>;
87
+ /** Register a hook from a scanned server/ module. */
88
+ export declare function registerHook(event: string, handler: Function, options?: any): void;
89
+ /** Fire the on-login hook if loaded. Errors are logged but never block login. */
90
+ export declare function fireOnLoginHook(did: string, oauthConfig: OAuthConfig | null): Promise<void>;
91
+ /**
92
+ * Fire on-commit hooks for a batch of indexed records.
93
+ * Runs async and non-blocking — errors are logged but never throw.
94
+ */
95
+ export declare function fireOnCommitHooks(items: Array<{
96
+ action: 'create' | 'delete';
97
+ collection: string;
98
+ uri: string;
99
+ authorDid: string;
100
+ record: Record<string, any> | null;
101
+ }>): void;
102
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAK9C,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAA;AACjE,OAAO,EAAqC,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AAEjF,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG;IACjD,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,gDAAgD;IAChD,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9D,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACvD;;;;;WAKG;QACH,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KACpE,CAAA;IACD,gEAAgE;IAChE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,0DAA0D;IAC1D,YAAY,EAAE,CACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,KACrB,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,qEAAqE;IACrE,SAAS,EAAE,CACT,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,2DAA2D;IAC3D,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAClE,CAAA;AAED,mEAAmE;AACnE,MAAM,MAAM,WAAW,GAAG;IACxB,iDAAiD;IACjD,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,wCAAwC;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAClC,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,wCAAwC;IACxC,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9D,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;QACvD,4EAA4E;QAC5E,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KACpE,CAAA;IACD,iDAAiD;IACjD,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IAC7B,kCAAkC;IAClC,IAAI,EAAE,aAAa,CAAA;CACpB,CAAA;AAQD,wBAAgB,UAAU,CACxB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAC1C;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAA;AACrF,wBAAgB,UAAU,CACxB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,EAClC,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAC3C;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAA;AAkB9G;;;;;;;;;;;GAWG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBrE;AAgBD,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,IAAI,CASlF;AAED,iFAAiF;AACjF,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BjG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,KAAK,CAAC;IACX,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;CACnC,CAAC,GACD,IAAI,CA6BN"}
package/dist/hooks.js ADDED
@@ -0,0 +1,186 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ /**
10
+ * Lifecycle hooks that run in response to server events.
11
+ *
12
+ * Place hook modules in the `hooks/` directory. Currently supported hooks:
13
+ *
14
+ * - `on-login.ts` — called after each successful OAuth login
15
+ * - `on-commit-*.ts` — called after records are indexed from the firehose
16
+ *
17
+ * Each hook default-exports the result of `defineHook()`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // hooks/on-login.ts
22
+ * import { defineHook } from '$hatk'
23
+ *
24
+ * export default defineHook("on-login", async (ctx) => {
25
+ * await ctx.ensureRepo(ctx.did)
26
+ * })
27
+ * ```
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // hooks/on-commit-favorite.ts
32
+ * import { defineHook } from '$hatk'
33
+ *
34
+ * export default defineHook("on-commit", { collections: ["social.grain.favorite"] },
35
+ * async ({ action, collection, record, repo, uri, db, lookup, push }) => {
36
+ * if (action !== "create") return
37
+ * // send push notification, etc.
38
+ * }
39
+ * )
40
+ * ```
41
+ */
42
+ import { existsSync } from 'node:fs';
43
+ import { resolve } from 'node:path';
44
+ import { pdsCreateRecord, pdsPutRecord, pdsDeleteRecord } from "./pds-proxy.js";
45
+ import { log, emit } from "./logger.js";
46
+ import { setRepoStatus, runSQL } from "./database/db.js";
47
+ import { triggerAutoBackfill, awaitBackfill } from "./indexer.js";
48
+ import { buildBaseContext } from "./hydrate.js";
49
+ import { buildPushInterface, isPushEnabled } from "./push.js";
50
+ export function defineHook(event, ...args) {
51
+ if (event === 'on-login') {
52
+ return { __type: 'hook', event, handler: args[0] };
53
+ }
54
+ if (event === 'on-commit') {
55
+ const options = args[0];
56
+ const handler = args[1];
57
+ return { __type: 'hook', event, collections: options.collections, handler };
58
+ }
59
+ throw new Error(`Unknown hook event: ${event}`);
60
+ }
61
+ let onLoginHook = null;
62
+ const onCommitHooks = [];
63
+ /**
64
+ * Discover and load the on-login hook from the project's `hooks/` directory.
65
+ * Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
66
+ *
67
+ * The documented contract is `export default defineHook('on-login', handler)`,
68
+ * which is an object carrying the handler — the same shape the `server/`
69
+ * scanner recognizes by its `__type` tag. A bare async function is still
70
+ * accepted, because that is what the legacy `hooks/` directory used before
71
+ * `defineHook` existed. Anything else is reported rather than installed: a
72
+ * non-callable hook would otherwise fail silently on every login, since
73
+ * `fireOnLoginHook` swallows what the hook throws.
74
+ */
75
+ export async function loadOnLoginHook(hooksDir) {
76
+ const tsPath = resolve(hooksDir, 'on-login.ts');
77
+ const jsPath = resolve(hooksDir, 'on-login.js');
78
+ const path = existsSync(tsPath) ? tsPath : existsSync(jsPath) ? jsPath : null;
79
+ if (!path)
80
+ return;
81
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${path}?t=${Date.now()}`));
82
+ const handler = resolveHookHandler(mod.default);
83
+ if (!handler) {
84
+ emit('hooks', 'on_login_load_error', {
85
+ path,
86
+ error: 'default export is not a function or a defineHook("on-login", ...) result',
87
+ });
88
+ return;
89
+ }
90
+ onLoginHook = handler;
91
+ log('[hooks] on-login hook loaded');
92
+ }
93
+ /** Unwrap a hook module's default export into the callable handler, if it has one. */
94
+ function resolveHookHandler(exported) {
95
+ if (typeof exported === 'function')
96
+ return exported;
97
+ const handler = exported?.handler;
98
+ return typeof handler === 'function' ? handler : null;
99
+ }
100
+ /** Mark a DID as pending, trigger auto-backfill, and wait for completion. */
101
+ async function ensureRepo(did) {
102
+ await setRepoStatus(did, 'pending');
103
+ triggerAutoBackfill(did);
104
+ await awaitBackfill(did);
105
+ }
106
+ /** Register a hook from a scanned server/ module. */
107
+ export function registerHook(event, handler, options) {
108
+ if (event === 'on-login') {
109
+ onLoginHook = handler;
110
+ log('[hooks] on-login hook registered');
111
+ }
112
+ else if (event === 'on-commit') {
113
+ const collections = new Set(options?.collections || []);
114
+ onCommitHooks.push({ collections, handler: handler });
115
+ log(`[hooks] on-commit hook registered (collections: ${[...collections].join(', ')})`);
116
+ }
117
+ }
118
+ /** Fire the on-login hook if loaded. Errors are logged but never block login. */
119
+ export async function fireOnLoginHook(did, oauthConfig) {
120
+ if (!onLoginHook)
121
+ return;
122
+ try {
123
+ const base = buildBaseContext({ did });
124
+ const viewer = { did };
125
+ const hookPromise = onLoginHook({
126
+ ...base,
127
+ did,
128
+ db: { query: base.db.query, run: runSQL, unfiltered: base.db.unfiltered },
129
+ ensureRepo,
130
+ createRecord: async (collection, record, opts) => {
131
+ if (!oauthConfig)
132
+ throw new Error('No OAuth config — cannot write to PDS');
133
+ return pdsCreateRecord(oauthConfig, viewer, { collection, record, rkey: opts?.rkey });
134
+ },
135
+ putRecord: async (collection, rkey, record) => {
136
+ if (!oauthConfig)
137
+ throw new Error('No OAuth config — cannot write to PDS');
138
+ return pdsPutRecord(oauthConfig, viewer, { collection, rkey, record });
139
+ },
140
+ deleteRecord: async (collection, rkey) => {
141
+ if (!oauthConfig)
142
+ throw new Error('No OAuth config — cannot write to PDS');
143
+ await pdsDeleteRecord(oauthConfig, viewer, { collection, rkey });
144
+ },
145
+ });
146
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('on-login hook timed out after 30s')), 30_000));
147
+ await Promise.race([hookPromise, timeout]);
148
+ }
149
+ catch (err) {
150
+ emit('hooks', 'on_login_error', { did, error: err.message });
151
+ }
152
+ }
153
+ /**
154
+ * Fire on-commit hooks for a batch of indexed records.
155
+ * Runs async and non-blocking — errors are logged but never throw.
156
+ */
157
+ export function fireOnCommitHooks(items) {
158
+ if (onCommitHooks.length === 0)
159
+ return;
160
+ const base = buildBaseContext(null);
161
+ const push = isPushEnabled() ? buildPushInterface() : { send: async () => { } };
162
+ for (const item of items) {
163
+ for (const hook of onCommitHooks) {
164
+ if (hook.collections.size > 0 && !hook.collections.has(item.collection))
165
+ continue;
166
+ hook
167
+ .handler({
168
+ action: item.action,
169
+ collection: item.collection,
170
+ record: item.record,
171
+ repo: item.authorDid,
172
+ uri: item.uri,
173
+ db: { query: base.db.query, run: runSQL, unfiltered: base.db.unfiltered },
174
+ lookup: base.lookup,
175
+ push,
176
+ })
177
+ .catch((err) => {
178
+ emit('hooks', 'on_commit_error', {
179
+ collection: item.collection,
180
+ uri: item.uri,
181
+ error: err.message,
182
+ });
183
+ });
184
+ }
185
+ }
186
+ }
package/dist/hydrate.d.ts CHANGED
@@ -1,23 +1,66 @@
1
1
  import type { Row } from './lex-types.ts';
2
2
  export type { Row };
3
- export interface HydrateContext<T = unknown> {
4
- items: Row<T>[];
3
+ export interface BaseContext {
5
4
  viewer: {
6
5
  did: string;
6
+ handle?: string;
7
7
  } | null;
8
8
  db: {
9
9
  query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
10
+ /**
11
+ * Raw SQL with the permissioned-space guard off. Named so it can be found
12
+ * with grep: for results never served to a viewer, such as an admin
13
+ * rollup. See `spaces/guard.ts`.
14
+ */
15
+ unfiltered: (sql: string, params?: unknown[]) => Promise<unknown[]>;
10
16
  };
11
17
  getRecords: <R = unknown>(collection: string, uris: string[]) => Promise<Map<string, Row<R>>>;
12
18
  lookup: <R = unknown>(collection: string, field: string, values: string[]) => Promise<Map<string, Row<R>>>;
13
19
  count: (collection: string, field: string, values: string[]) => Promise<Map<string, number>>;
14
20
  labels: (uris: string[]) => Promise<Map<string, unknown[]>>;
15
- blobUrl: (did: string, ref: unknown, preset?: 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize') => string | undefined;
21
+ blobUrl: (did: string, ref: unknown, preset?: string) => string | undefined;
22
+ /**
23
+ * The permissioned-space gate, for hand-written SQL.
24
+ *
25
+ * Every helper on this context applies it already. Raw SQL through
26
+ * `ctx.db.query` cannot — nothing can inject a predicate into a string
27
+ * somebody else wrote — so a query that selects from a collection a space
28
+ * writes into has to apply it itself:
29
+ *
30
+ * ```ts
31
+ * const gate = ctx.spaceFilter('t', 2)
32
+ * ctx.db.query(
33
+ * `SELECT t.* FROM "app.example.post" t WHERE t.did = $1 AND ${gate.sql}`,
34
+ * ['did:plc:someone', ...gate.params],
35
+ * )
36
+ * ```
37
+ *
38
+ * Outside a viewer's scope this is `t.space IS NULL` and binds nothing, so on
39
+ * an instance that indexes no space it costs one predicate on an indexed
40
+ * column and changes no result.
41
+ */
42
+ spaceFilter: (alias: string, startIdx: number) => {
43
+ sql: string;
44
+ params: string[];
45
+ nextIdx: number;
46
+ };
47
+ /**
48
+ * Every record of a collection in a space the viewer may read. Empty for a
49
+ * space outside their scope; see `requireSpace` for the explicit refusal.
50
+ */
51
+ spaceRecords: <R = unknown>(collection: string, space: string) => Promise<Row<R>[]>;
52
+ /** Every record where `field` is one of `values` — the many-rows counterpart of `lookup`. */
53
+ records: <R = unknown>(collection: string, field: string, values: string[]) => Promise<Row<R>[]>;
54
+ /** Throw `NotAuthorized` unless the viewer may read this space. */
55
+ requireSpace: (space: string) => void;
56
+ /** The URL this appview serves a space blob at, for the viewer asking. */
57
+ spaceBlobUrl: (space: string, repo: string, cid: string) => string;
16
58
  }
17
59
  /** Fetch records for URIs, reshape them, and filter out taken-down DIDs. */
18
60
  export declare function resolveRecords(uris: string[]): Promise<Row<unknown>[]>;
19
- /** Build a HydrateContext for a feed's hydrate function. */
20
- export declare function buildHydrateContext(items: Row<unknown>[], viewer: {
61
+ /** Build a BaseContext for hydration. */
62
+ export declare function buildBaseContext(viewer: {
21
63
  did: string;
22
- } | null): HydrateContext;
64
+ handle?: string;
65
+ } | null): BaseContext;
23
66
  //# sourceMappingURL=hydrate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEzC,YAAY,EAAE,GAAG,EAAE,CAAA;AAInB,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,OAAO;IACzC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IACf,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC9B,EAAE,EAAE;QAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KAAE,CAAA;IACtE,UAAU,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1G,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,OAAO,EACZ,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,gBAAgB,GAAG,eAAe,KAC9D,MAAM,GAAG,SAAS,CAAA;CACxB;AAID,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAoC5E;AAID,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,cAAc,CA4BzG"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAA;AAEzC,YAAY,EAAE,GAAG,EAAE,CAAA;AAInB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAC/C,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QAC9D;;;;WAIG;QACH,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;KACpE,CAAA;IACD,UAAU,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1G,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5F,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IAC3D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IAC3E;;;;;;;;;;;;;;;;;;;OAmBG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACpG;;;OAGG;IACH,YAAY,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACnF,6FAA6F;IAC7F,OAAO,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAChG,mEAAmE;IACnE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,0EAA0E;IAC1E,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;CACnE;AAID,4EAA4E;AAC5E,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAsC5E;AAID,yCAAyC;AACzC,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,GAAG,WAAW,CAuB7F"}
package/dist/hydrate.js CHANGED
@@ -1,15 +1,23 @@
1
- import { getRecordsByUris, countByFieldBatch, lookupByFieldBatch, querySQL, reshapeRow, queryLabelsForUris, filterTakendownDids, } from "./db.js";
1
+ import { getRecordsMap, countByFieldBatch, lookupByFieldBatch, queryLabelsForUris, filterTakendownDids, getRecordsByUris, reshapeRow, } from "./database/db.js";
2
2
  import { blobUrl } from "./xrpc.js";
3
+ import { collectionFromRecordUri } from "./spaces/uri.js";
4
+ import { spaceFilterSql } from "./spaces/visibility.js";
5
+ import { guardedQuerySQL, unfilteredQuerySQL } from "./spaces/guard.js";
6
+ import { records, requireSpace, spaceRecords } from "./spaces/records.js";
7
+ import { spaceBlobUrl } from "./spaces/blob.js";
3
8
  // --- Record Resolution ---
4
9
  /** Fetch records for URIs, reshape them, and filter out taken-down DIDs. */
5
10
  export async function resolveRecords(uris) {
6
11
  if (uris.length === 0)
7
12
  return [];
8
- // Group URIs by collection for batch fetching
13
+ // Group URIs by collection for batch fetching. A space record's collection
14
+ // sits in a different segment than a repo record's, so the position is read
15
+ // by a parser that knows both shapes rather than inline.
9
16
  const byCollection = new Map();
10
17
  for (const uri of uris) {
11
- const parts = uri.replace('at://', '').split('/');
12
- const col = parts[1];
18
+ const col = collectionFromRecordUri(uri);
19
+ if (!col)
20
+ continue;
13
21
  if (!byCollection.has(col))
14
22
  byCollection.set(col, []);
15
23
  byCollection.get(col).push(uri);
@@ -39,24 +47,12 @@ export async function resolveRecords(uris) {
39
47
  .filter((r) => r != null);
40
48
  }
41
49
  // --- Context Builder ---
42
- /** Build a HydrateContext for a feed's hydrate function. */
43
- export function buildHydrateContext(items, viewer) {
50
+ /** Build a BaseContext for hydration. */
51
+ export function buildBaseContext(viewer) {
44
52
  return {
45
- items,
46
53
  viewer,
47
- db: { query: querySQL },
48
- getRecords: async (collection, uris) => {
49
- if (uris.length === 0)
50
- return new Map();
51
- const records = await getRecordsByUris(collection, uris);
52
- const map = new Map();
53
- for (const r of records) {
54
- const shaped = reshapeRow(r, r?.__childData, r?.__unionData);
55
- if (shaped)
56
- map.set(shaped.uri, shaped);
57
- }
58
- return map;
59
- },
54
+ db: { query: guardedQuerySQL, unfiltered: unfilteredQuerySQL },
55
+ getRecords: getRecordsMap,
60
56
  lookup: async (collection, field, values) => {
61
57
  if (values.length === 0)
62
58
  return new Map();
@@ -71,5 +67,10 @@ export function buildHydrateContext(items, viewer) {
71
67
  },
72
68
  labels: queryLabelsForUris,
73
69
  blobUrl,
70
+ spaceFilter: spaceFilterSql,
71
+ spaceRecords,
72
+ records,
73
+ requireSpace,
74
+ spaceBlobUrl,
74
75
  };
75
76
  }