@hatk/hatk 0.0.1-alpha.8 → 0.0.1-alpha.81

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 (175) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +111 -0
  4. package/dist/backfill.d.ts +29 -2
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +136 -51
  7. package/dist/car.d.ts +42 -10
  8. package/dist/car.d.ts.map +1 -1
  9. package/dist/car.js +154 -14
  10. package/dist/cli.js +243 -1043
  11. package/dist/config.d.ts +82 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +44 -9
  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} +57 -6
  30. package/dist/database/db.d.ts.map +1 -0
  31. package/dist/{db.js → database/db.js} +755 -552
  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} +116 -32
  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} +14 -3
  45. package/dist/database/schema.d.ts.map +1 -0
  46. package/dist/{schema.js → database/schema.js} +81 -41
  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 +125 -0
  50. package/dist/feeds.d.ts +12 -8
  51. package/dist/feeds.d.ts.map +1 -1
  52. package/dist/feeds.js +51 -6
  53. package/dist/hooks.d.ts +85 -0
  54. package/dist/hooks.d.ts.map +1 -0
  55. package/dist/hooks.js +163 -0
  56. package/dist/hydrate.d.ts +7 -6
  57. package/dist/hydrate.d.ts.map +1 -1
  58. package/dist/hydrate.js +4 -16
  59. package/dist/indexer.d.ts +133 -3
  60. package/dist/indexer.d.ts.map +1 -1
  61. package/dist/indexer.js +406 -66
  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/lexicon-resolve.d.ts.map +1 -1
  69. package/dist/lexicon-resolve.js +27 -112
  70. package/dist/lexicons/com/atproto/label/defs.json +75 -0
  71. package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
  72. package/dist/lexicons/com/atproto/repo/strongRef.json +21 -0
  73. package/dist/lexicons/dev/hatk/applyWrites.json +87 -0
  74. package/dist/lexicons/dev/hatk/createRecord.json +40 -0
  75. package/dist/lexicons/dev/hatk/createReport.json +48 -0
  76. package/dist/lexicons/dev/hatk/deleteRecord.json +25 -0
  77. package/dist/lexicons/dev/hatk/describeCollections.json +41 -0
  78. package/dist/lexicons/dev/hatk/describeFeeds.json +29 -0
  79. package/dist/lexicons/dev/hatk/describeLabels.json +45 -0
  80. package/dist/lexicons/dev/hatk/getFeed.json +30 -0
  81. package/dist/lexicons/dev/hatk/getPreferences.json +19 -0
  82. package/dist/lexicons/dev/hatk/getRecord.json +26 -0
  83. package/dist/lexicons/dev/hatk/getRecords.json +32 -0
  84. package/dist/lexicons/dev/hatk/putPreference.json +28 -0
  85. package/dist/lexicons/dev/hatk/putRecord.json +41 -0
  86. package/dist/lexicons/dev/hatk/searchRecords.json +32 -0
  87. package/dist/lexicons/dev/hatk/uploadBlob.json +23 -0
  88. package/dist/logger.d.ts +29 -0
  89. package/dist/logger.d.ts.map +1 -1
  90. package/dist/logger.js +29 -0
  91. package/dist/main.js +169 -74
  92. package/dist/mst.d.ts +18 -1
  93. package/dist/mst.d.ts.map +1 -1
  94. package/dist/mst.js +19 -8
  95. package/dist/oauth/db.d.ts +3 -1
  96. package/dist/oauth/db.d.ts.map +1 -1
  97. package/dist/oauth/db.js +56 -19
  98. package/dist/oauth/discovery.d.ts.map +1 -1
  99. package/dist/oauth/discovery.js +13 -1
  100. package/dist/oauth/server.d.ts +92 -1
  101. package/dist/oauth/server.d.ts.map +1 -1
  102. package/dist/oauth/server.js +352 -37
  103. package/dist/oauth/service-describe.d.ts +31 -0
  104. package/dist/oauth/service-describe.d.ts.map +1 -0
  105. package/dist/oauth/service-describe.js +61 -0
  106. package/dist/oauth/session.d.ts +32 -0
  107. package/dist/oauth/session.d.ts.map +1 -0
  108. package/dist/oauth/session.js +131 -0
  109. package/dist/opengraph.d.ts +10 -0
  110. package/dist/opengraph.d.ts.map +1 -1
  111. package/dist/opengraph.js +80 -40
  112. package/dist/pds-proxy.d.ts +82 -0
  113. package/dist/pds-proxy.d.ts.map +1 -0
  114. package/dist/pds-proxy.js +356 -0
  115. package/dist/private-collections.d.ts +8 -0
  116. package/dist/private-collections.d.ts.map +1 -0
  117. package/dist/private-collections.js +22 -0
  118. package/dist/push.d.ts +54 -0
  119. package/dist/push.d.ts.map +1 -0
  120. package/dist/push.js +367 -0
  121. package/dist/renderer.d.ts +27 -0
  122. package/dist/renderer.d.ts.map +1 -0
  123. package/dist/renderer.js +46 -0
  124. package/dist/resolve-hatk.d.ts +6 -0
  125. package/dist/resolve-hatk.d.ts.map +1 -0
  126. package/dist/resolve-hatk.js +20 -0
  127. package/dist/response.d.ts +16 -0
  128. package/dist/response.d.ts.map +1 -0
  129. package/dist/response.js +69 -0
  130. package/dist/scanner.d.ts +21 -0
  131. package/dist/scanner.d.ts.map +1 -0
  132. package/dist/scanner.js +88 -0
  133. package/dist/seed.d.ts +19 -0
  134. package/dist/seed.d.ts.map +1 -1
  135. package/dist/seed.js +43 -4
  136. package/dist/server-init.d.ts +8 -0
  137. package/dist/server-init.d.ts.map +1 -0
  138. package/dist/server-init.js +62 -0
  139. package/dist/server.d.ts +26 -3
  140. package/dist/server.d.ts.map +1 -1
  141. package/dist/server.js +858 -638
  142. package/dist/setup.d.ts +28 -1
  143. package/dist/setup.d.ts.map +1 -1
  144. package/dist/setup.js +50 -3
  145. package/dist/templates/feed.tpl +14 -0
  146. package/dist/templates/hook.tpl +5 -0
  147. package/dist/templates/label.tpl +15 -0
  148. package/dist/templates/og.tpl +17 -0
  149. package/dist/templates/seed.tpl +11 -0
  150. package/dist/templates/setup.tpl +5 -0
  151. package/dist/templates/test-feed.tpl +19 -0
  152. package/dist/templates/test-xrpc.tpl +19 -0
  153. package/dist/templates/xrpc.tpl +41 -0
  154. package/dist/test.d.ts +1 -1
  155. package/dist/test.d.ts.map +1 -1
  156. package/dist/test.js +41 -32
  157. package/dist/views.js +1 -1
  158. package/dist/vite-plugin.d.ts +1 -1
  159. package/dist/vite-plugin.d.ts.map +1 -1
  160. package/dist/vite-plugin.js +254 -66
  161. package/dist/xrpc.d.ts +84 -11
  162. package/dist/xrpc.d.ts.map +1 -1
  163. package/dist/xrpc.js +211 -41
  164. package/package.json +17 -9
  165. package/public/admin-auth.js +271 -306
  166. package/public/admin.html +177 -61
  167. package/dist/db.d.ts.map +0 -1
  168. package/dist/fts.d.ts.map +0 -1
  169. package/dist/oauth/hooks.d.ts +0 -10
  170. package/dist/oauth/hooks.d.ts.map +0 -1
  171. package/dist/oauth/hooks.js +0 -40
  172. package/dist/schema.d.ts.map +0 -1
  173. package/dist/test-browser.d.ts +0 -14
  174. package/dist/test-browser.d.ts.map +0 -1
  175. package/dist/test-browser.js +0 -26
@@ -0,0 +1,356 @@
1
+ // Shared PDS proxy functions — used by both HTTP route handlers and XRPC handlers.
2
+ import { getSession, getServerKey, deleteSession } from "./oauth/db.js";
3
+ import { createDpopProof } from "./oauth/dpop.js";
4
+ import { refreshPdsSession } from "./oauth/server.js";
5
+ import { validateRecord } from '@bigmoves/lexicon';
6
+ import { getLexiconArray } from "./database/schema.js";
7
+ import { insertRecord, deleteRecord as dbDeleteRecord } from "./database/db.js";
8
+ import { emit } from "./logger.js";
9
+ import { runLabelRules } from "./labels.js";
10
+ export class ProxyError extends Error {
11
+ status;
12
+ constructor(status, message) {
13
+ super(message);
14
+ this.status = status;
15
+ }
16
+ }
17
+ export class ScopeMissingProxyError extends ProxyError {
18
+ constructor() {
19
+ super(401, 'ScopeMissingError');
20
+ }
21
+ }
22
+ /** Shared retry logic: DPoP nonce handling + token refresh. */
23
+ async function withDpopRetry(oauthConfig, session, doFetch) {
24
+ let accessToken = session.access_token;
25
+ let result = await doFetch(accessToken);
26
+ if (result.ok)
27
+ return result;
28
+ let nonce;
29
+ // Step 1: handle DPoP nonce requirement
30
+ if (isNonceChallenge(result)) {
31
+ nonce = result.headers.get('DPoP-Nonce') || undefined;
32
+ if (nonce) {
33
+ result = await doFetch(accessToken, nonce);
34
+ if (result.ok)
35
+ return result;
36
+ }
37
+ }
38
+ // Step 2: handle insufficient scope — clear session so user re-authenticates with updated scopes
39
+ if (result.body.error === 'ScopeMissingError') {
40
+ await deleteSession(session.did);
41
+ throw new ScopeMissingProxyError();
42
+ }
43
+ // Step 3: handle expired PDS token — refresh and retry
44
+ if (isTokenError(result.body.error)) {
45
+ const refreshed = await refreshPdsSession(oauthConfig, session);
46
+ if (refreshed) {
47
+ accessToken = refreshed.accessToken;
48
+ result = await doFetch(accessToken, nonce);
49
+ if (result.ok)
50
+ return result;
51
+ if (isNonceChallenge(result)) {
52
+ nonce = result.headers.get('DPoP-Nonce') || undefined;
53
+ if (nonce)
54
+ result = await doFetch(accessToken, nonce);
55
+ }
56
+ // A token minted seconds ago cannot be expired, so a second refusal is
57
+ // about the grant rather than the token: the session was authorized
58
+ // before the app asked for the scope this call needs, and no number of
59
+ // refreshes will add it. Read as a scope problem it becomes the prompt to
60
+ // authorize again; read as expiry it was an opaque 500.
61
+ //
62
+ // Not every PDS names this case. The reference answers ScopeMissingError,
63
+ // handled above; pds.js answers InvalidToken for a scope its permissioned
64
+ // space endpoints refuse, which is indistinguishable from expiry until
65
+ // the refresh rules expiry out.
66
+ if (!result.ok && isTokenError(result.body.error)) {
67
+ await deleteSession(session.did);
68
+ throw new ScopeMissingProxyError();
69
+ }
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ /**
75
+ * Is this refusal a DPoP nonce challenge — a request to retry, not a dead token?
76
+ *
77
+ * A nonce challenge is the one 401 that costs nothing to satisfy: the server
78
+ * hands back the nonce it wants in the same response. Miss it and the failure
79
+ * falls through to the token-error branch below, which refreshes, replays the
80
+ * identical proof, is refused identically, and drops the user's session — a
81
+ * re-authorization to fix a header.
82
+ *
83
+ * So it is read the way RFC 9449 §8 writes it, from
84
+ * `WWW-Authenticate: DPoP error="use_dpop_nonce"`, and only then from the body.
85
+ * The body is not always available to read: zds sends that header but an
86
+ * `InvalidToken` body, because its XRPC error handler overwrites the
87
+ * `UseDpopNonce` one on the way out.
88
+ *
89
+ * The `DPoP-Nonce` header alone is not the signal. A server may attach one to
90
+ * every 401 it sends, including 401s about a genuinely invalid token, and
91
+ * treating those as challenges would replay every dead token once for nothing.
92
+ */
93
+ function isNonceChallenge(result) {
94
+ const error = result.body.error;
95
+ if (error === 'use_dpop_nonce' || error === 'UseDpopNonce')
96
+ return true;
97
+ return result.status === 401 && (result.headers.get('WWW-Authenticate') ?? '').includes('use_dpop_nonce');
98
+ }
99
+ /**
100
+ * The PDS returns 'InvalidToken' or 'ExpiredToken' (AT Proto PascalCase
101
+ * convention) while the OAuth spec uses 'invalid_token' (RFC 6750 snake_case).
102
+ */
103
+ function isTokenError(error) {
104
+ return error === 'invalid_token' || error === 'InvalidToken' || error === 'ExpiredToken';
105
+ }
106
+ async function proxyToPds(oauthConfig, session, method, pdsUrl, body) {
107
+ const serverKey = await getServerKey('appview-oauth-key');
108
+ const privateJwk = JSON.parse(serverKey.privateKey);
109
+ const publicJwk = JSON.parse(serverKey.publicKey);
110
+ return withDpopRetry(oauthConfig, session, async (token, nonce) => {
111
+ const proof = await createDpopProof(privateJwk, publicJwk, method, pdsUrl, token, nonce);
112
+ // A GET carries no body, and sending a JSON content-type without one makes
113
+ // some servers try to parse it. Queries reach here through pdsXrpc.
114
+ const hasBody = method !== 'GET' && body !== undefined;
115
+ const res = await fetch(pdsUrl, {
116
+ method,
117
+ headers: {
118
+ ...(hasBody ? { 'Content-Type': 'application/json' } : {}),
119
+ Authorization: `DPoP ${token}`,
120
+ DPoP: proof,
121
+ },
122
+ body: hasBody ? JSON.stringify(body) : undefined,
123
+ });
124
+ const resBody = await res.json().catch(() => ({}));
125
+ return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
126
+ });
127
+ }
128
+ /** Proxy a raw binary request to the user's PDS with DPoP + nonce retry + token refresh. */
129
+ async function proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType) {
130
+ const serverKey = await getServerKey('appview-oauth-key');
131
+ const privateJwk = JSON.parse(serverKey.privateKey);
132
+ const publicJwk = JSON.parse(serverKey.publicKey);
133
+ return withDpopRetry(oauthConfig, session, async (token, nonce) => {
134
+ const proof = await createDpopProof(privateJwk, publicJwk, 'POST', pdsUrl, token, nonce);
135
+ const res = await fetch(pdsUrl, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'Content-Type': contentType,
139
+ 'Content-Length': String(body.length),
140
+ Authorization: `DPoP ${token}`,
141
+ DPoP: proof,
142
+ },
143
+ body: Buffer.from(body),
144
+ });
145
+ const resBody = await res.json().catch(() => ({}));
146
+ return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
147
+ });
148
+ }
149
+ /**
150
+ * Call any XRPC method on the user's PDS with their session.
151
+ *
152
+ * The escape hatch beside the typed record helpers above, for methods hatk has
153
+ * no opinion about — `com.atproto.space.*` and the rest of proposal 0016 being
154
+ * the case it was added for. Nothing is validated or indexed: the response is
155
+ * whatever the PDS sent, and it is the caller's job to know its shape.
156
+ *
157
+ * The session's granted scopes still apply. A method the user never authorized
158
+ * comes back as a 403 from the PDS, not from here.
159
+ */
160
+ export async function pdsXrpc(oauthConfig, viewer, nsid, options = {}) {
161
+ const session = await getSession(viewer.did);
162
+ if (!session)
163
+ throw new ProxyError(401, 'No PDS session for user');
164
+ const method = options.method ?? 'GET';
165
+ const url = new URL(`${session.pds_endpoint}/xrpc/${nsid}`);
166
+ for (const [key, value] of Object.entries(options.params ?? {})) {
167
+ if (value === undefined)
168
+ continue;
169
+ if (Array.isArray(value)) {
170
+ for (const v of value)
171
+ url.searchParams.append(key, String(v));
172
+ }
173
+ else {
174
+ url.searchParams.append(key, String(value));
175
+ }
176
+ }
177
+ const res = await proxyToPds(oauthConfig, session, method, url.toString(), options.body);
178
+ if (!res.ok) {
179
+ throw new ProxyError(res.status, String(res.body.error || `${nsid} failed`));
180
+ }
181
+ return res.body;
182
+ }
183
+ export async function pdsCreateRecord(oauthConfig, viewer, input) {
184
+ const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
185
+ if (validationError) {
186
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
187
+ }
188
+ const session = await getSession(viewer.did);
189
+ if (!session)
190
+ throw new ProxyError(401, 'No PDS session for user');
191
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.createRecord`;
192
+ const pdsBody = {
193
+ repo: viewer.did,
194
+ collection: input.collection,
195
+ rkey: input.rkey,
196
+ record: input.record,
197
+ };
198
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
199
+ if (!pdsRes.ok)
200
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
201
+ try {
202
+ await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
203
+ await runLabelRules({
204
+ uri: String(pdsRes.body.uri),
205
+ cid: String(pdsRes.body.cid),
206
+ did: viewer.did,
207
+ collection: input.collection,
208
+ value: input.record,
209
+ });
210
+ }
211
+ catch (err) {
212
+ emit('pds-proxy', 'local_index_error', {
213
+ op: 'createRecord',
214
+ error: err instanceof Error ? err.message : String(err),
215
+ });
216
+ }
217
+ return pdsRes.body;
218
+ }
219
+ export async function pdsDeleteRecord(oauthConfig, viewer, input) {
220
+ const session = await getSession(viewer.did);
221
+ if (!session)
222
+ throw new ProxyError(401, 'No PDS session for user');
223
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.deleteRecord`;
224
+ const pdsBody = {
225
+ repo: viewer.did,
226
+ collection: input.collection,
227
+ rkey: input.rkey,
228
+ };
229
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
230
+ if (!pdsRes.ok)
231
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS delete failed'));
232
+ try {
233
+ const uri = `at://${viewer.did}/${input.collection}/${input.rkey}`;
234
+ await dbDeleteRecord(input.collection, uri);
235
+ }
236
+ catch (err) {
237
+ emit('pds-proxy', 'local_index_error', {
238
+ op: 'deleteRecord',
239
+ error: err instanceof Error ? err.message : String(err),
240
+ });
241
+ }
242
+ return pdsRes.body;
243
+ }
244
+ export async function pdsPutRecord(oauthConfig, viewer, input) {
245
+ const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
246
+ if (validationError) {
247
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
248
+ }
249
+ const session = await getSession(viewer.did);
250
+ if (!session)
251
+ throw new ProxyError(401, 'No PDS session for user');
252
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.putRecord`;
253
+ const pdsBody = {
254
+ repo: viewer.did,
255
+ collection: input.collection,
256
+ rkey: input.rkey,
257
+ record: input.record,
258
+ };
259
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
260
+ if (!pdsRes.ok)
261
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
262
+ try {
263
+ await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
264
+ await runLabelRules({
265
+ uri: String(pdsRes.body.uri),
266
+ cid: String(pdsRes.body.cid),
267
+ did: viewer.did,
268
+ collection: input.collection,
269
+ value: input.record,
270
+ });
271
+ }
272
+ catch (err) {
273
+ emit('pds-proxy', 'local_index_error', { op: 'putRecord', error: err instanceof Error ? err.message : String(err) });
274
+ }
275
+ return pdsRes.body;
276
+ }
277
+ /** Map dev.hatk.applyWrites#* types to com.atproto.repo.applyWrites#* for PDS. */
278
+ function toPdsWriteType($type) {
279
+ return $type.replace('dev.hatk.applyWrites#', 'com.atproto.repo.applyWrites#');
280
+ }
281
+ function isCreateOrUpdate($type) {
282
+ const mapped = toPdsWriteType($type);
283
+ return mapped === 'com.atproto.repo.applyWrites#create' || mapped === 'com.atproto.repo.applyWrites#update';
284
+ }
285
+ export async function pdsApplyWrites(oauthConfig, viewer, input) {
286
+ // Validate all create/update records before sending
287
+ for (const write of input.writes) {
288
+ if (isCreateOrUpdate(write.$type) && write.value) {
289
+ const validationError = validateRecord(getLexiconArray(), write.collection, write.value);
290
+ if (validationError) {
291
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
292
+ }
293
+ }
294
+ }
295
+ const session = await getSession(viewer.did);
296
+ if (!session)
297
+ throw new ProxyError(401, 'No PDS session for user');
298
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.applyWrites`;
299
+ const pdsBody = {
300
+ repo: viewer.did,
301
+ writes: input.writes.map((w) => ({ ...w, $type: toPdsWriteType(w.$type) })),
302
+ };
303
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
304
+ if (!pdsRes.ok)
305
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS applyWrites failed'));
306
+ // Index results locally
307
+ const results = pdsRes.body.results ?? [];
308
+ for (let i = 0; i < input.writes.length; i++) {
309
+ const write = input.writes[i];
310
+ const result = results[i];
311
+ try {
312
+ const mapped = toPdsWriteType(write.$type);
313
+ if (mapped === 'com.atproto.repo.applyWrites#create' && result?.uri && result?.cid && write.value) {
314
+ await insertRecord(write.collection, result.uri, result.cid, viewer.did, write.value);
315
+ await runLabelRules({
316
+ uri: result.uri,
317
+ cid: result.cid,
318
+ did: viewer.did,
319
+ collection: write.collection,
320
+ value: write.value,
321
+ });
322
+ }
323
+ else if (mapped === 'com.atproto.repo.applyWrites#update' && result?.uri && result?.cid && write.value) {
324
+ await insertRecord(write.collection, result.uri, result.cid, viewer.did, write.value);
325
+ await runLabelRules({
326
+ uri: result.uri,
327
+ cid: result.cid,
328
+ did: viewer.did,
329
+ collection: write.collection,
330
+ value: write.value,
331
+ });
332
+ }
333
+ else if (mapped === 'com.atproto.repo.applyWrites#delete' && write.rkey) {
334
+ const uri = `at://${viewer.did}/${write.collection}/${write.rkey}`;
335
+ await dbDeleteRecord(write.collection, uri);
336
+ }
337
+ }
338
+ catch (err) {
339
+ emit('pds-proxy', 'local_index_error', {
340
+ op: 'applyWrites',
341
+ error: err instanceof Error ? err.message : String(err),
342
+ });
343
+ }
344
+ }
345
+ return pdsRes.body;
346
+ }
347
+ export async function pdsUploadBlob(oauthConfig, viewer, body, contentType) {
348
+ const session = await getSession(viewer.did);
349
+ if (!session)
350
+ throw new ProxyError(401, 'No PDS session for user');
351
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.uploadBlob`;
352
+ const pdsRes = await proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType);
353
+ if (!pdsRes.ok)
354
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS upload failed'));
355
+ return pdsRes.body;
356
+ }
@@ -0,0 +1,8 @@
1
+ export declare function setPrivateCollections(list: string[]): void;
2
+ export declare function isPrivateCollection(nsid: string | null | undefined): boolean;
3
+ /**
4
+ * The collection segment of an AT-URI: at://{did}/{collection}/{rkey}.
5
+ * Splitting yields ['at:', '', did, collection, rkey].
6
+ */
7
+ export declare function collectionFromUri(uri: string): string | undefined;
8
+ //# sourceMappingURL=private-collections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-collections.d.ts","sourceRoot":"","sources":["../src/private-collections.ts"],"names":[],"mappings":"AASA,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAE1D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjE"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Collections that are indexed and queryable in-process but must never be
3
+ * served by the built-in dev.hatk.* record endpoints.
4
+ *
5
+ * Held module-level, mirroring the schema registry, so the guard can be applied
6
+ * inside handlers without threading config through every signature.
7
+ */
8
+ let privateCollections = new Set();
9
+ export function setPrivateCollections(list) {
10
+ privateCollections = new Set(list);
11
+ }
12
+ export function isPrivateCollection(nsid) {
13
+ return nsid != null && privateCollections.has(nsid);
14
+ }
15
+ /**
16
+ * The collection segment of an AT-URI: at://{did}/{collection}/{rkey}.
17
+ * Splitting yields ['at:', '', did, collection, rkey].
18
+ */
19
+ export function collectionFromUri(uri) {
20
+ const parts = uri.split('/');
21
+ return parts.length > 4 ? parts[3] : undefined;
22
+ }
package/dist/push.d.ts ADDED
@@ -0,0 +1,54 @@
1
+ export interface ApnsConfig {
2
+ keyFile: string;
3
+ keyId: string;
4
+ teamId: string;
5
+ bundleId: string;
6
+ production?: boolean;
7
+ }
8
+ /**
9
+ * Firebase Cloud Messaging, for Android devices.
10
+ *
11
+ * `keyFile` is a Google service-account JSON with the Firebase Messaging role,
12
+ * resolved like the APNs key: relative to the config file. The project id is
13
+ * read from that file unless overridden here.
14
+ */
15
+ export interface FcmConfig {
16
+ keyFile: string;
17
+ projectId?: string;
18
+ }
19
+ export interface PushConfig {
20
+ apns?: ApnsConfig;
21
+ fcm?: FcmConfig;
22
+ }
23
+ export interface PushPayload {
24
+ did: string;
25
+ title: string;
26
+ body: string;
27
+ data?: Record<string, string>;
28
+ collapseId?: string;
29
+ badge?: number;
30
+ }
31
+ export interface PushInterface {
32
+ send: (payload: PushPayload) => Promise<void>;
33
+ }
34
+ /**
35
+ * Initialize push with config. Must be called before send().
36
+ *
37
+ * Each transport is loaded on its own: a missing or malformed credential
38
+ * disables that platform and leaves the other one sending, rather than taking
39
+ * push down entirely.
40
+ */
41
+ export declare function initPush(config: PushConfig, configDir: string): void;
42
+ /** Check if push is configured and at least one transport is usable. */
43
+ export declare function isPushEnabled(): boolean;
44
+ /** Which platforms this process can actually deliver to, for startup logging. */
45
+ export declare function enabledPushTransports(): string[];
46
+ /** Build the push interface injected into hook contexts. */
47
+ export declare function buildPushInterface(): PushInterface;
48
+ /** Register a push token for a DID. Upserts on conflict. */
49
+ export declare function registerToken(did: string, token: string, platform: string): Promise<void>;
50
+ /** Remove a push token. */
51
+ export declare function removeToken(token: string): Promise<void>;
52
+ /** Unregister a specific token for a DID. */
53
+ export declare function unregisterToken(did: string, token: string): Promise<void>;
54
+ //# sourceMappingURL=push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../src/push.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,GAAG,CAAC,EAAE,SAAS,CAAA;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC9C;AAsBD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CA4CpE;AAED,wEAAwE;AACxE,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,CAKhD;AAED,4DAA4D;AAC5D,wBAAgB,kBAAkB,IAAI,aAAa,CAElD;AAmSD,4DAA4D;AAC5D,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/F;AAED,2BAA2B;AAC3B,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED,6CAA6C;AAC7C,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/E"}