@hatk/hatk 0.0.1-alpha.23 → 0.0.1-alpha.25

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 (63) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +94 -0
  4. package/dist/backfill.d.ts.map +1 -1
  5. package/dist/backfill.js +12 -0
  6. package/dist/cli.js +186 -66
  7. package/dist/config.d.ts +1 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/database/db.d.ts.map +1 -1
  11. package/dist/database/db.js +5 -1
  12. package/dist/dev-entry.d.ts +8 -0
  13. package/dist/dev-entry.d.ts.map +1 -0
  14. package/dist/dev-entry.js +109 -0
  15. package/dist/feeds.d.ts +4 -0
  16. package/dist/feeds.d.ts.map +1 -1
  17. package/dist/feeds.js +41 -2
  18. package/dist/hooks.d.ts +7 -0
  19. package/dist/hooks.d.ts.map +1 -1
  20. package/dist/hooks.js +11 -1
  21. package/dist/labels.d.ts +14 -0
  22. package/dist/labels.d.ts.map +1 -1
  23. package/dist/labels.js +13 -1
  24. package/dist/main.js +49 -17
  25. package/dist/oauth/server.d.ts +2 -0
  26. package/dist/oauth/server.d.ts.map +1 -1
  27. package/dist/oauth/server.js +91 -1
  28. package/dist/oauth/session.d.ts +9 -0
  29. package/dist/oauth/session.d.ts.map +1 -0
  30. package/dist/oauth/session.js +65 -0
  31. package/dist/opengraph.d.ts +10 -0
  32. package/dist/opengraph.d.ts.map +1 -1
  33. package/dist/opengraph.js +102 -4
  34. package/dist/pds-proxy.d.ts +39 -0
  35. package/dist/pds-proxy.d.ts.map +1 -0
  36. package/dist/pds-proxy.js +173 -0
  37. package/dist/renderer.d.ts +27 -0
  38. package/dist/renderer.d.ts.map +1 -0
  39. package/dist/renderer.js +46 -0
  40. package/dist/response.d.ts +16 -0
  41. package/dist/response.d.ts.map +1 -0
  42. package/dist/response.js +69 -0
  43. package/dist/scanner.d.ts +21 -0
  44. package/dist/scanner.d.ts.map +1 -0
  45. package/dist/scanner.js +88 -0
  46. package/dist/server-init.d.ts +8 -0
  47. package/dist/server-init.d.ts.map +1 -0
  48. package/dist/server-init.js +59 -0
  49. package/dist/server.d.ts +26 -3
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +473 -616
  52. package/dist/setup.d.ts +7 -0
  53. package/dist/setup.d.ts.map +1 -1
  54. package/dist/setup.js +13 -1
  55. package/dist/test.d.ts.map +1 -1
  56. package/dist/test.js +12 -22
  57. package/dist/vite-plugin.d.ts +1 -1
  58. package/dist/vite-plugin.d.ts.map +1 -1
  59. package/dist/vite-plugin.js +245 -75
  60. package/dist/xrpc.d.ts +13 -0
  61. package/dist/xrpc.d.ts.map +1 -1
  62. package/dist/xrpc.js +87 -1
  63. package/package.json +8 -5
@@ -0,0 +1,65 @@
1
+ // SSR session cookie — signed HttpOnly cookie for server-side viewer resolution.
2
+ // Separate from OAuth protocol flows but uses the same server keypair.
3
+ import { base64UrlEncode, base64UrlDecode } from "./crypto.js";
4
+ let _privateJwk;
5
+ let _cookieName = '__hatk_session';
6
+ const MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
7
+ export function getSessionCookieName() {
8
+ return _cookieName;
9
+ }
10
+ export function initSession(privateJwk, cookieName) {
11
+ _privateJwk = privateJwk;
12
+ if (cookieName)
13
+ _cookieName = cookieName;
14
+ }
15
+ async function hmacKey(usage) {
16
+ return crypto.subtle.importKey('raw', new TextEncoder().encode(JSON.stringify(_privateJwk, Object.keys(_privateJwk).sort())), { name: 'HMAC', hash: 'SHA-256' }, false, [usage]);
17
+ }
18
+ export async function createSessionCookie(did) {
19
+ const timestamp = Math.floor(Date.now() / 1000);
20
+ const payload = `${did}.${timestamp}`;
21
+ const key = await hmacKey('sign');
22
+ const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
23
+ return `${payload}.${base64UrlEncode(new Uint8Array(sig))}`;
24
+ }
25
+ export function sessionCookieHeader(value, secure) {
26
+ const parts = [
27
+ `${_cookieName}=${value}`,
28
+ 'HttpOnly',
29
+ 'SameSite=Lax',
30
+ 'Path=/',
31
+ `Max-Age=${MAX_AGE}`,
32
+ ];
33
+ if (secure)
34
+ parts.push('Secure');
35
+ return parts.join('; ');
36
+ }
37
+ export function clearSessionCookieHeader() {
38
+ return `${_cookieName}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0`;
39
+ }
40
+ export async function parseSessionCookie(request) {
41
+ const cookieHeader = request.headers.get('cookie');
42
+ if (!cookieHeader)
43
+ return null;
44
+ const match = cookieHeader.split(';').map(c => c.trim()).find(c => c.startsWith(`${_cookieName}=`));
45
+ if (!match)
46
+ return null;
47
+ const value = match.slice(_cookieName.length + 1);
48
+ const parts = value.split('.');
49
+ // Format: did:plc:xxx.timestamp.signature — DID contains dots so take last 2 parts
50
+ if (parts.length < 3)
51
+ return null;
52
+ const signature = parts.pop();
53
+ const timestamp = parts.pop();
54
+ const did = parts.join('.');
55
+ const ts = Number(timestamp);
56
+ if (isNaN(ts) || (Date.now() / 1000 - ts) > MAX_AGE)
57
+ return null;
58
+ const payload = `${did}.${timestamp}`;
59
+ const key = await hmacKey('verify');
60
+ const sigBytes = base64UrlDecode(signature);
61
+ const valid = await crypto.subtle.verify('HMAC', key, sigBytes, new TextEncoder().encode(payload));
62
+ if (!valid)
63
+ return null;
64
+ return { did };
65
+ }
@@ -28,7 +28,17 @@ export interface OpengraphResult {
28
28
  description?: string;
29
29
  };
30
30
  }
31
+ export declare function defineOG(path: string, generate: (ctx: OpengraphContext) => Promise<OpengraphResult>): {
32
+ __type: "og";
33
+ path: string;
34
+ generate: (ctx: OpengraphContext) => Promise<OpengraphResult>;
35
+ };
31
36
  export declare function initOpengraph(ogDir: string): Promise<void>;
37
+ /** Register a single OG handler from a scanned server/ module. */
38
+ export declare function registerOgHandler(ogMod: {
39
+ path: string;
40
+ generate: (ctx: OpengraphContext) => Promise<OpengraphResult>;
41
+ }): void;
32
42
  export declare function handleOpengraphRequest(pathname: string): Promise<Buffer | null>;
33
43
  export declare function buildOgMeta(pathname: string, origin: string): string | null;
34
44
  //# sourceMappingURL=opengraph.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"opengraph.d.ts","sourceRoot":"","sources":["../src/opengraph.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC3B,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAAA;QAC3C,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KACnB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACpD;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,UAAU,CAAA;IACnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,CAAA;IAC5D,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAChD;AAkCD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoGhE;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8BrF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyC3E"}
1
+ {"version":3,"file":"opengraph.d.ts","sourceRoot":"","sources":["../src/opengraph.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAE5C,4CAA4C;AAC5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC3B,QAAQ,CAAC,EAAE,CAAC,UAAU,GAAG,MAAM,CAAC,EAAE,GAAG,MAAM,CAAA;QAC3C,GAAG,CAAC,EAAE,MAAM,CAAA;QACZ,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KACnB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CACpD;AAED,qDAAqD;AACrD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,UAAU,CAAA;IACnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAA;KAAE,CAAA;IAC5D,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAChD;AAED,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC;;;oBAA7C,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC;EAG9D;AAkCD,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoGhE;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;CAAE,GAAG,IAAI,CAgF9H;AAED,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA+BrF;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyC3E"}
package/dist/opengraph.js CHANGED
@@ -9,11 +9,25 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  import { resolve } from 'node:path';
10
10
  import { readFileSync, readdirSync } from 'node:fs';
11
11
  import { log } from "./logger.js";
12
- import satori from 'satori';
13
- import { Resvg } from '@resvg/resvg-js';
12
+ // Lazy-imported to avoid CJS require() issues in Vite's module runner
13
+ let _satori = null;
14
+ let _Resvg = null;
15
+ async function getSatori() {
16
+ if (!_satori)
17
+ _satori = (await import('satori')).default;
18
+ return _satori;
19
+ }
20
+ async function getResvg() {
21
+ if (!_Resvg)
22
+ _Resvg = (await import('@resvg/resvg-js')).Resvg;
23
+ return _Resvg;
24
+ }
14
25
  import { querySQL, runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, lookupByFieldBatch, countByFieldBatch, queryLabelsForUris, } from "./database/db.js";
15
26
  import { resolveRecords } from "./hydrate.js";
16
27
  import { blobUrl } from "./xrpc.js";
28
+ export function defineOG(path, generate) {
29
+ return { __type: 'og', path, generate };
30
+ }
17
31
  const handlers = [];
18
32
  const pageRoutes = [];
19
33
  let defaultFont = null;
@@ -51,7 +65,7 @@ export async function initOpengraph(ogDir) {
51
65
  for (const file of files) {
52
66
  const name = file.replace(/\.(ts|js)$/, '');
53
67
  const scriptPath = resolve(ogDir, file);
54
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
68
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
55
69
  const handler = mod.default;
56
70
  if (!handler.path) {
57
71
  console.warn(`[opengraph] ${file} missing 'path' export, skipping`);
@@ -117,7 +131,7 @@ export async function initOpengraph(ogDir) {
117
131
  ...result.options,
118
132
  fonts: [...(defaultFont ? [defaultFont] : []), ...(result.options?.fonts || [])],
119
133
  };
120
- const svg = await satori(element, options);
134
+ const svg = await (await getSatori())(element, options);
121
135
  return { svg, meta: result.meta };
122
136
  },
123
137
  });
@@ -129,6 +143,89 @@ export async function initOpengraph(ogDir) {
129
143
  }
130
144
  }
131
145
  }
146
+ /** Register a single OG handler from a scanned server/ module. */
147
+ export function registerOgHandler(ogMod) {
148
+ const { pattern, paramNames } = compilePath(ogMod.path);
149
+ const name = ogMod.path.replace(/^\//, '').replace(/\//g, '-').replace(/:/g, '');
150
+ // Load default font if not already loaded
151
+ if (!defaultFont) {
152
+ try {
153
+ const fontPath = resolve(import.meta.dirname, '..', 'fonts', 'Inter-Regular.woff');
154
+ const fontData = readFileSync(fontPath);
155
+ defaultFont = { name: 'Inter', data: fontData.buffer, weight: 400, style: 'normal' };
156
+ }
157
+ catch { }
158
+ }
159
+ handlers.push({
160
+ name,
161
+ path: ogMod.path,
162
+ pattern,
163
+ paramNames,
164
+ execute: async (params) => {
165
+ const ctx = {
166
+ db: { query: querySQL, run: runSQL },
167
+ params,
168
+ input: {},
169
+ limit: 1,
170
+ viewer: null,
171
+ packCursor,
172
+ unpackCursor,
173
+ isTakendown: isTakendownDid,
174
+ filterTakendownDids,
175
+ search: searchRecords,
176
+ resolve: resolveRecords,
177
+ lookup: async (collection, field, values) => {
178
+ if (values.length === 0)
179
+ return new Map();
180
+ const unique = [...new Set(values.filter(Boolean))];
181
+ return lookupByFieldBatch(collection, field, unique);
182
+ },
183
+ count: async (collection, field, values) => {
184
+ if (values.length === 0)
185
+ return new Map();
186
+ const unique = [...new Set(values.filter(Boolean))];
187
+ return countByFieldBatch(collection, field, unique);
188
+ },
189
+ exists: async (collection, filters) => {
190
+ const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
191
+ const uri = await findUriByFields(collection, conditions);
192
+ return uri !== null;
193
+ },
194
+ labels: queryLabelsForUris,
195
+ blobUrl,
196
+ };
197
+ ctx.fetchImage = async (url) => {
198
+ try {
199
+ const resp = await fetch(url, { redirect: 'follow' });
200
+ if (!resp.ok)
201
+ return null;
202
+ const buf = Buffer.from(await resp.arrayBuffer());
203
+ const contentType = resp.headers.get('content-type') || 'image/jpeg';
204
+ return `data:${contentType};base64,${buf.toString('base64')}`;
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ };
210
+ const result = await ogMod.generate(ctx);
211
+ const element = result.element;
212
+ const options = {
213
+ width: 1200,
214
+ height: 630,
215
+ ...result.options,
216
+ fonts: [...(defaultFont ? [defaultFont] : []), ...(result.options?.fonts || [])],
217
+ };
218
+ const svg = await (await getSatori())(element, options);
219
+ return { svg, meta: result.meta };
220
+ },
221
+ });
222
+ const pagePath = ogMod.path.replace(/^\/og/, '');
223
+ if (pagePath !== ogMod.path) {
224
+ const compiled = compilePath(pagePath);
225
+ pageRoutes.push({ ogPath: ogMod.path, pattern: compiled.pattern, paramNames: compiled.paramNames, name });
226
+ }
227
+ log(`[opengraph] registered: ${name} → ${ogMod.path}`);
228
+ }
132
229
  export async function handleOpengraphRequest(pathname) {
133
230
  const cached = cache.get(pathname);
134
231
  if (cached && cached.expires > Date.now())
@@ -143,6 +240,7 @@ export async function handleOpengraphRequest(pathname) {
143
240
  });
144
241
  try {
145
242
  const { svg, meta } = await handler.execute(params);
243
+ const Resvg = await getResvg();
146
244
  const png = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }).render().asPng();
147
245
  if (cache.size >= CACHE_MAX) {
148
246
  const oldest = cache.keys().next().value;
@@ -0,0 +1,39 @@
1
+ import type { OAuthConfig } from './config.ts';
2
+ export declare class ProxyError extends Error {
3
+ status: number;
4
+ constructor(status: number, message: string);
5
+ }
6
+ export declare function pdsCreateRecord(oauthConfig: OAuthConfig, viewer: {
7
+ did: string;
8
+ }, input: {
9
+ collection: string;
10
+ repo?: string;
11
+ rkey?: string;
12
+ record: Record<string, unknown>;
13
+ }): Promise<{
14
+ uri?: string;
15
+ cid?: string;
16
+ }>;
17
+ export declare function pdsDeleteRecord(oauthConfig: OAuthConfig, viewer: {
18
+ did: string;
19
+ }, input: {
20
+ collection: string;
21
+ rkey: string;
22
+ }): Promise<Record<string, unknown>>;
23
+ export declare function pdsPutRecord(oauthConfig: OAuthConfig, viewer: {
24
+ did: string;
25
+ }, input: {
26
+ collection: string;
27
+ rkey: string;
28
+ record: Record<string, unknown>;
29
+ repo?: string;
30
+ }): Promise<{
31
+ uri?: string;
32
+ cid?: string;
33
+ }>;
34
+ export declare function pdsUploadBlob(oauthConfig: OAuthConfig, viewer: {
35
+ did: string;
36
+ }, body: Uint8Array, contentType: string): Promise<{
37
+ blob: unknown;
38
+ }>;
39
+ //# sourceMappingURL=pds-proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pds-proxy.d.ts","sourceRoot":"","sources":["../src/pds-proxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAS9C,qBAAa,UAAW,SAAQ,KAAK;IAChB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAGnD;AA8GD,wBAAsB,eAAe,CACnC,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACvB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC3F,OAAO,CAAC;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2BzC;AAED,wBAAsB,eAAe,CACnC,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACvB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsBlC;AAED,wBAAsB,YAAY,CAChC,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACvB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1F,OAAO,CAAC;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2BzC;AAED,wBAAsB,aAAa,CACjC,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACvB,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAS5B"}
@@ -0,0 +1,173 @@
1
+ // Shared PDS proxy functions — used by both HTTP route handlers and XRPC handlers.
2
+ import { getSession, getServerKey } 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
+ export class ProxyError extends Error {
10
+ status;
11
+ constructor(status, message) {
12
+ super(message);
13
+ this.status = status;
14
+ }
15
+ }
16
+ /** Shared retry logic: DPoP nonce handling + token refresh. */
17
+ async function withDpopRetry(oauthConfig, session, doFetch) {
18
+ let accessToken = session.access_token;
19
+ let result = await doFetch(accessToken);
20
+ if (result.ok)
21
+ return result;
22
+ let nonce;
23
+ // Step 1: handle DPoP nonce requirement
24
+ if (result.body.error === 'use_dpop_nonce') {
25
+ nonce = result.headers.get('DPoP-Nonce') || undefined;
26
+ if (nonce) {
27
+ result = await doFetch(accessToken, nonce);
28
+ if (result.ok)
29
+ return result;
30
+ }
31
+ }
32
+ // Step 2: handle expired PDS token — refresh and retry
33
+ if (result.body.error === 'invalid_token') {
34
+ const refreshed = await refreshPdsSession(oauthConfig, session);
35
+ if (refreshed) {
36
+ accessToken = refreshed.accessToken;
37
+ result = await doFetch(accessToken, nonce);
38
+ if (result.ok)
39
+ return result;
40
+ if (result.body.error === 'use_dpop_nonce') {
41
+ nonce = result.headers.get('DPoP-Nonce') || undefined;
42
+ if (nonce)
43
+ result = await doFetch(accessToken, nonce);
44
+ }
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ async function proxyToPds(oauthConfig, session, method, pdsUrl, body) {
50
+ const serverKey = await getServerKey('appview-oauth-key');
51
+ const privateJwk = JSON.parse(serverKey.privateKey);
52
+ const publicJwk = JSON.parse(serverKey.publicKey);
53
+ return withDpopRetry(oauthConfig, session, async (token, nonce) => {
54
+ const proof = await createDpopProof(privateJwk, publicJwk, method, pdsUrl, token, nonce);
55
+ const res = await fetch(pdsUrl, {
56
+ method,
57
+ headers: {
58
+ 'Content-Type': 'application/json',
59
+ Authorization: `DPoP ${token}`,
60
+ DPoP: proof,
61
+ },
62
+ body: JSON.stringify(body),
63
+ });
64
+ const resBody = await res.json().catch(() => ({}));
65
+ return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
66
+ });
67
+ }
68
+ /** Proxy a raw binary request to the user's PDS with DPoP + nonce retry + token refresh. */
69
+ async function proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType) {
70
+ const serverKey = await getServerKey('appview-oauth-key');
71
+ const privateJwk = JSON.parse(serverKey.privateKey);
72
+ const publicJwk = JSON.parse(serverKey.publicKey);
73
+ return withDpopRetry(oauthConfig, session, async (token, nonce) => {
74
+ const proof = await createDpopProof(privateJwk, publicJwk, 'POST', pdsUrl, token, nonce);
75
+ const res = await fetch(pdsUrl, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': contentType,
79
+ 'Content-Length': String(body.length),
80
+ Authorization: `DPoP ${token}`,
81
+ DPoP: proof,
82
+ },
83
+ body: Buffer.from(body),
84
+ });
85
+ const resBody = await res.json().catch(() => ({}));
86
+ return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
87
+ });
88
+ }
89
+ // --- High-level proxy functions ---
90
+ export async function pdsCreateRecord(oauthConfig, viewer, input) {
91
+ const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
92
+ if (validationError) {
93
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
94
+ }
95
+ const session = await getSession(viewer.did);
96
+ if (!session)
97
+ throw new ProxyError(401, 'No PDS session for user');
98
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.createRecord`;
99
+ const pdsBody = {
100
+ repo: viewer.did,
101
+ collection: input.collection,
102
+ rkey: input.rkey,
103
+ record: input.record,
104
+ };
105
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
106
+ if (!pdsRes.ok)
107
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
108
+ try {
109
+ await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
110
+ }
111
+ catch (err) {
112
+ emit('pds-proxy', 'local_index_error', { op: 'createRecord', error: err instanceof Error ? err.message : String(err) });
113
+ }
114
+ return pdsRes.body;
115
+ }
116
+ export async function pdsDeleteRecord(oauthConfig, viewer, input) {
117
+ const session = await getSession(viewer.did);
118
+ if (!session)
119
+ throw new ProxyError(401, 'No PDS session for user');
120
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.deleteRecord`;
121
+ const pdsBody = {
122
+ repo: viewer.did,
123
+ collection: input.collection,
124
+ rkey: input.rkey,
125
+ };
126
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
127
+ if (!pdsRes.ok)
128
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS delete failed'));
129
+ try {
130
+ const uri = `at://${viewer.did}/${input.collection}/${input.rkey}`;
131
+ await dbDeleteRecord(input.collection, uri);
132
+ }
133
+ catch (err) {
134
+ emit('pds-proxy', 'local_index_error', { op: 'deleteRecord', error: err instanceof Error ? err.message : String(err) });
135
+ }
136
+ return pdsRes.body;
137
+ }
138
+ export async function pdsPutRecord(oauthConfig, viewer, input) {
139
+ const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
140
+ if (validationError) {
141
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
142
+ }
143
+ const session = await getSession(viewer.did);
144
+ if (!session)
145
+ throw new ProxyError(401, 'No PDS session for user');
146
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.putRecord`;
147
+ const pdsBody = {
148
+ repo: viewer.did,
149
+ collection: input.collection,
150
+ rkey: input.rkey,
151
+ record: input.record,
152
+ };
153
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
154
+ if (!pdsRes.ok)
155
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
156
+ try {
157
+ await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
158
+ }
159
+ catch (err) {
160
+ emit('pds-proxy', 'local_index_error', { op: 'putRecord', error: err instanceof Error ? err.message : String(err) });
161
+ }
162
+ return pdsRes.body;
163
+ }
164
+ export async function pdsUploadBlob(oauthConfig, viewer, body, contentType) {
165
+ const session = await getSession(viewer.did);
166
+ if (!session)
167
+ throw new ProxyError(401, 'No PDS session for user');
168
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.uploadBlob`;
169
+ const pdsRes = await proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType);
170
+ if (!pdsRes.ok)
171
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS upload failed'));
172
+ return pdsRes.body;
173
+ }
@@ -0,0 +1,27 @@
1
+ export interface SSRManifest {
2
+ getPreloadTags(url: string): string;
3
+ }
4
+ export interface RenderResult {
5
+ html: string;
6
+ head?: string;
7
+ }
8
+ export type RendererHandler = (request: Request, manifest: SSRManifest) => Promise<RenderResult>;
9
+ export declare function defineRenderer(handler: RendererHandler): {
10
+ __type: "renderer";
11
+ handler: RendererHandler;
12
+ };
13
+ export declare function registerRenderer(handler: RendererHandler): void;
14
+ export declare function setSSRManifest(manifest: SSRManifest): void;
15
+ export declare function getRenderer(): RendererHandler | null;
16
+ export declare function getSSRManifest(): SSRManifest | null;
17
+ /**
18
+ * Render an HTML page by calling the user's renderer and assembling the result
19
+ * into the index.html template.
20
+ *
21
+ * @param template - The index.html content (with <!--ssr-outlet--> placeholder)
22
+ * @param request - The incoming Request
23
+ * @param ogMeta - Optional OG meta tags to inject
24
+ * @returns Assembled HTML string, or null if no renderer is registered
25
+ */
26
+ export declare function renderPage(template: string, request: Request, ogMeta?: string | null): Promise<string | null>;
27
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;AAKhG,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe;;;EAEtD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAG/D;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAE1D;AAED,wBAAgB,WAAW,IAAI,eAAe,GAAG,IAAI,CAEpD;AAED,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAEnD;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsBxB"}
@@ -0,0 +1,46 @@
1
+ import { log } from "./logger.js";
2
+ let renderer = null;
3
+ let ssrManifest = null;
4
+ export function defineRenderer(handler) {
5
+ return { __type: 'renderer', handler };
6
+ }
7
+ export function registerRenderer(handler) {
8
+ renderer = handler;
9
+ log('[renderer] SSR renderer registered');
10
+ }
11
+ export function setSSRManifest(manifest) {
12
+ ssrManifest = manifest;
13
+ }
14
+ export function getRenderer() {
15
+ return renderer;
16
+ }
17
+ export function getSSRManifest() {
18
+ return ssrManifest;
19
+ }
20
+ /**
21
+ * Render an HTML page by calling the user's renderer and assembling the result
22
+ * into the index.html template.
23
+ *
24
+ * @param template - The index.html content (with <!--ssr-outlet--> placeholder)
25
+ * @param request - The incoming Request
26
+ * @param ogMeta - Optional OG meta tags to inject
27
+ * @returns Assembled HTML string, or null if no renderer is registered
28
+ */
29
+ export async function renderPage(template, request, ogMeta) {
30
+ if (!renderer)
31
+ return null;
32
+ const manifest = ssrManifest || { getPreloadTags: () => '' };
33
+ const result = await renderer(request, manifest);
34
+ let html = template;
35
+ // Inject SSR head tags (preloads, styles)
36
+ if (result.head) {
37
+ html = html.replace('</head>', `${result.head}\n</head>`);
38
+ }
39
+ // Inject OG meta tags
40
+ if (ogMeta) {
41
+ html = html.replace('</head>', `${ogMeta}\n</head>`);
42
+ }
43
+ // Inject rendered HTML into the outlet
44
+ html = html.replace('<!--ssr-outlet-->', result.html);
45
+ return html;
46
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Create a JSON Response with optional gzip compression.
3
+ * Mirrors the old jsonResponse/sendJson behavior.
4
+ */
5
+ export declare function json(data: unknown, status?: number, acceptEncoding?: string | null): Response;
6
+ /** Create a JSON error Response. */
7
+ export declare function jsonError(status: number, message: string, acceptEncoding?: string | null): Response;
8
+ /** CORS preflight Response. */
9
+ export declare function cors(): Response;
10
+ /** Add CORS headers to an existing Response. */
11
+ export declare function withCors(response: Response): Response;
12
+ /** Create a static file Response with correct MIME type. */
13
+ export declare function file(content: Buffer | Uint8Array, contentType: string, cacheControl?: string): Response;
14
+ /** 404 Not Found. */
15
+ export declare function notFound(): Response;
16
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/response.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,SAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAuB1F;AAED,oCAAoC;AACpC,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,QAAQ,CAEnG;AAED,+BAA+B;AAC/B,wBAAgB,IAAI,IAAI,QAAQ,CAS/B;AAED,gDAAgD;AAChD,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAUrD;AAED,4DAA4D;AAC5D,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,CAQvG;AAED,qBAAqB;AACrB,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC"}
@@ -0,0 +1,69 @@
1
+ import { gzipSync } from 'node:zlib';
2
+ import { normalizeValue } from "./database/db.js";
3
+ /**
4
+ * Create a JSON Response with optional gzip compression.
5
+ * Mirrors the old jsonResponse/sendJson behavior.
6
+ */
7
+ export function json(data, status = 200, acceptEncoding) {
8
+ const body = Buffer.from(JSON.stringify(data, (_, v) => normalizeValue(v)));
9
+ if (body.length > 1024 && acceptEncoding && /\bgzip\b/.test(acceptEncoding)) {
10
+ const compressed = gzipSync(body);
11
+ return new Response(compressed, {
12
+ status,
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ 'Content-Encoding': 'gzip',
16
+ 'Vary': 'Accept-Encoding',
17
+ ...(status === 200 ? { 'Cache-Control': 'no-store' } : {}),
18
+ },
19
+ });
20
+ }
21
+ return new Response(body, {
22
+ status,
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ ...(status === 200 ? { 'Cache-Control': 'no-store' } : {}),
26
+ },
27
+ });
28
+ }
29
+ /** Create a JSON error Response. */
30
+ export function jsonError(status, message, acceptEncoding) {
31
+ return json({ error: message }, status, acceptEncoding);
32
+ }
33
+ /** CORS preflight Response. */
34
+ export function cors() {
35
+ return new Response(null, {
36
+ status: 200,
37
+ headers: {
38
+ 'Access-Control-Allow-Origin': '*',
39
+ 'Access-Control-Allow-Headers': '*',
40
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
41
+ },
42
+ });
43
+ }
44
+ /** Add CORS headers to an existing Response. */
45
+ export function withCors(response) {
46
+ const headers = new Headers(response.headers);
47
+ headers.set('Access-Control-Allow-Origin', '*');
48
+ headers.set('Access-Control-Allow-Headers', '*');
49
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
50
+ return new Response(response.body, {
51
+ status: response.status,
52
+ statusText: response.statusText,
53
+ headers,
54
+ });
55
+ }
56
+ /** Create a static file Response with correct MIME type. */
57
+ export function file(content, contentType, cacheControl) {
58
+ return new Response(Buffer.from(content), {
59
+ status: 200,
60
+ headers: {
61
+ 'Content-Type': contentType,
62
+ ...(cacheControl ? { 'Cache-Control': cacheControl } : {}),
63
+ },
64
+ });
65
+ }
66
+ /** 404 Not Found. */
67
+ export function notFound() {
68
+ return new Response('Not Found', { status: 404 });
69
+ }
@@ -0,0 +1,21 @@
1
+ export interface ScannedModule {
2
+ path: string;
3
+ name: string;
4
+ mod: any;
5
+ }
6
+ export interface ScanResult {
7
+ feeds: ScannedModule[];
8
+ queries: ScannedModule[];
9
+ procedures: ScannedModule[];
10
+ hooks: ScannedModule[];
11
+ setup: ScannedModule[];
12
+ labels: ScannedModule[];
13
+ og: ScannedModule[];
14
+ renderer: ScannedModule | null;
15
+ }
16
+ /**
17
+ * Scan a directory for hatk server modules.
18
+ * Each file's default export is inspected for a `__type` tag.
19
+ */
20
+ export declare function scanServerDir(serverDir: string): Promise<ScanResult>;
21
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,GAAG,CAAA;CACT;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,UAAU,EAAE,aAAa,EAAE,CAAA;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,MAAM,EAAE,aAAa,EAAE,CAAA;IACvB,EAAE,EAAE,aAAa,EAAE,CAAA;IACnB,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;CAC/B;AAmBD;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA2D1E"}