@hatk/hatk 0.0.1-alpha.5 → 0.0.1-alpha.50

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 (165) hide show
  1. package/dist/adapter.d.ts +19 -0
  2. package/dist/adapter.d.ts.map +1 -0
  3. package/dist/adapter.js +107 -0
  4. package/dist/backfill.d.ts +60 -1
  5. package/dist/backfill.d.ts.map +1 -1
  6. package/dist/backfill.js +167 -33
  7. package/dist/car.d.ts +59 -1
  8. package/dist/car.d.ts.map +1 -1
  9. package/dist/car.js +179 -7
  10. package/dist/cbor.d.ts +37 -0
  11. package/dist/cbor.d.ts.map +1 -1
  12. package/dist/cbor.js +36 -3
  13. package/dist/cid.d.ts +37 -0
  14. package/dist/cid.d.ts.map +1 -1
  15. package/dist/cid.js +38 -3
  16. package/dist/cli.js +243 -996
  17. package/dist/config.d.ts +12 -1
  18. package/dist/config.d.ts.map +1 -1
  19. package/dist/config.js +36 -9
  20. package/dist/database/adapter-factory.d.ts +6 -0
  21. package/dist/database/adapter-factory.d.ts.map +1 -0
  22. package/dist/database/adapter-factory.js +20 -0
  23. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  24. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  25. package/dist/database/adapters/duckdb-search.js +27 -0
  26. package/dist/database/adapters/duckdb.d.ts +25 -0
  27. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  28. package/dist/database/adapters/duckdb.js +161 -0
  29. package/dist/database/adapters/sqlite-search.d.ts +23 -0
  30. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  31. package/dist/database/adapters/sqlite-search.js +74 -0
  32. package/dist/database/adapters/sqlite.d.ts +18 -0
  33. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  34. package/dist/database/adapters/sqlite.js +88 -0
  35. package/dist/{db.d.ts → database/db.d.ts} +56 -6
  36. package/dist/database/db.d.ts.map +1 -0
  37. package/dist/{db.js → database/db.js} +719 -549
  38. package/dist/database/dialect.d.ts +45 -0
  39. package/dist/database/dialect.d.ts.map +1 -0
  40. package/dist/database/dialect.js +72 -0
  41. package/dist/{fts.d.ts → database/fts.d.ts} +7 -0
  42. package/dist/database/fts.d.ts.map +1 -0
  43. package/dist/{fts.js → database/fts.js} +116 -32
  44. package/dist/database/index.d.ts +7 -0
  45. package/dist/database/index.d.ts.map +1 -0
  46. package/dist/database/index.js +6 -0
  47. package/dist/database/ports.d.ts +50 -0
  48. package/dist/database/ports.d.ts.map +1 -0
  49. package/dist/database/ports.js +1 -0
  50. package/dist/{schema.d.ts → database/schema.d.ts} +14 -3
  51. package/dist/database/schema.d.ts.map +1 -0
  52. package/dist/{schema.js → database/schema.js} +81 -41
  53. package/dist/dev-entry.d.ts +8 -0
  54. package/dist/dev-entry.d.ts.map +1 -0
  55. package/dist/dev-entry.js +111 -0
  56. package/dist/feeds.d.ts +12 -8
  57. package/dist/feeds.d.ts.map +1 -1
  58. package/dist/feeds.js +45 -6
  59. package/dist/hooks.d.ts +43 -0
  60. package/dist/hooks.d.ts.map +1 -0
  61. package/dist/hooks.js +102 -0
  62. package/dist/hydrate.d.ts +6 -5
  63. package/dist/hydrate.d.ts.map +1 -1
  64. package/dist/hydrate.js +4 -16
  65. package/dist/indexer.d.ts +22 -0
  66. package/dist/indexer.d.ts.map +1 -1
  67. package/dist/indexer.js +80 -8
  68. package/dist/labels.d.ts +36 -0
  69. package/dist/labels.d.ts.map +1 -1
  70. package/dist/labels.js +71 -6
  71. package/dist/lexicon-resolve.d.ts.map +1 -1
  72. package/dist/lexicon-resolve.js +27 -112
  73. package/dist/lexicons/com/atproto/label/defs.json +75 -0
  74. package/dist/lexicons/com/atproto/moderation/defs.json +30 -0
  75. package/dist/lexicons/com/atproto/repo/strongRef.json +24 -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 +126 -67
  94. package/dist/mst.d.ts +18 -1
  95. package/dist/mst.d.ts.map +1 -1
  96. package/dist/mst.js +19 -8
  97. package/dist/oauth/db.d.ts +3 -1
  98. package/dist/oauth/db.d.ts.map +1 -1
  99. package/dist/oauth/db.js +48 -19
  100. package/dist/oauth/server.d.ts +24 -0
  101. package/dist/oauth/server.d.ts.map +1 -1
  102. package/dist/oauth/server.js +198 -22
  103. package/dist/oauth/session.d.ts +11 -0
  104. package/dist/oauth/session.d.ts.map +1 -0
  105. package/dist/oauth/session.js +65 -0
  106. package/dist/opengraph.d.ts +10 -0
  107. package/dist/opengraph.d.ts.map +1 -1
  108. package/dist/opengraph.js +73 -39
  109. package/dist/pds-proxy.d.ts +42 -0
  110. package/dist/pds-proxy.d.ts.map +1 -0
  111. package/dist/pds-proxy.js +207 -0
  112. package/dist/renderer.d.ts +27 -0
  113. package/dist/renderer.d.ts.map +1 -0
  114. package/dist/renderer.js +46 -0
  115. package/dist/resolve-hatk.d.ts +6 -0
  116. package/dist/resolve-hatk.d.ts.map +1 -0
  117. package/dist/resolve-hatk.js +20 -0
  118. package/dist/response.d.ts +16 -0
  119. package/dist/response.d.ts.map +1 -0
  120. package/dist/response.js +69 -0
  121. package/dist/scanner.d.ts +21 -0
  122. package/dist/scanner.d.ts.map +1 -0
  123. package/dist/scanner.js +88 -0
  124. package/dist/seed.d.ts +19 -0
  125. package/dist/seed.d.ts.map +1 -1
  126. package/dist/seed.js +43 -4
  127. package/dist/server-init.d.ts +8 -0
  128. package/dist/server-init.d.ts.map +1 -0
  129. package/dist/server-init.js +62 -0
  130. package/dist/server.d.ts +26 -3
  131. package/dist/server.d.ts.map +1 -1
  132. package/dist/server.js +601 -635
  133. package/dist/setup.d.ts +28 -1
  134. package/dist/setup.d.ts.map +1 -1
  135. package/dist/setup.js +50 -3
  136. package/dist/templates/feed.tpl +14 -0
  137. package/dist/templates/hook.tpl +5 -0
  138. package/dist/templates/label.tpl +15 -0
  139. package/dist/templates/og.tpl +17 -0
  140. package/dist/templates/seed.tpl +11 -0
  141. package/dist/templates/setup.tpl +5 -0
  142. package/dist/templates/test-feed.tpl +19 -0
  143. package/dist/templates/test-xrpc.tpl +19 -0
  144. package/dist/templates/xrpc.tpl +41 -0
  145. package/dist/test.d.ts +1 -1
  146. package/dist/test.d.ts.map +1 -1
  147. package/dist/test.js +38 -32
  148. package/dist/views.js +1 -1
  149. package/dist/vite-plugin.d.ts +1 -1
  150. package/dist/vite-plugin.d.ts.map +1 -1
  151. package/dist/vite-plugin.js +254 -66
  152. package/dist/xrpc.d.ts +60 -10
  153. package/dist/xrpc.d.ts.map +1 -1
  154. package/dist/xrpc.js +155 -39
  155. package/package.json +15 -7
  156. package/public/admin.html +133 -54
  157. package/dist/db.d.ts.map +0 -1
  158. package/dist/fts.d.ts.map +0 -1
  159. package/dist/oauth/hooks.d.ts +0 -10
  160. package/dist/oauth/hooks.d.ts.map +0 -1
  161. package/dist/oauth/hooks.js +0 -40
  162. package/dist/schema.d.ts.map +0 -1
  163. package/dist/test-browser.d.ts +0 -14
  164. package/dist/test-browser.d.ts.map +0 -1
  165. package/dist/test-browser.js +0 -26
package/dist/opengraph.js CHANGED
@@ -9,11 +9,23 @@ 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';
14
- import { querySQL, runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, lookupByFieldBatch, countByFieldBatch, queryLabelsForUris, } from "./db.js";
15
- import { resolveRecords } from "./hydrate.js";
16
- import { blobUrl } from "./xrpc.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
+ }
25
+ import { buildXrpcContext } from "./xrpc.js";
26
+ export function defineOG(path, generate) {
27
+ return { __type: 'og', path, generate };
28
+ }
17
29
  const handlers = [];
18
30
  const pageRoutes = [];
19
31
  let defaultFont = null;
@@ -51,7 +63,7 @@ export async function initOpengraph(ogDir) {
51
63
  for (const file of files) {
52
64
  const name = file.replace(/\.(ts|js)$/, '');
53
65
  const scriptPath = resolve(ogDir, file);
54
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
66
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
55
67
  const handler = mod.default;
56
68
  if (!handler.path) {
57
69
  console.warn(`[opengraph] ${file} missing 'path' export, skipping`);
@@ -64,38 +76,7 @@ export async function initOpengraph(ogDir) {
64
76
  pattern,
65
77
  paramNames,
66
78
  execute: async (params) => {
67
- const ctx = {
68
- db: { query: querySQL, run: runSQL },
69
- params,
70
- input: {},
71
- limit: 1,
72
- viewer: null,
73
- packCursor,
74
- unpackCursor,
75
- isTakendown: isTakendownDid,
76
- filterTakendownDids,
77
- search: searchRecords,
78
- resolve: resolveRecords,
79
- lookup: async (collection, field, values) => {
80
- if (values.length === 0)
81
- return new Map();
82
- const unique = [...new Set(values.filter(Boolean))];
83
- return lookupByFieldBatch(collection, field, unique);
84
- },
85
- count: async (collection, field, values) => {
86
- if (values.length === 0)
87
- return new Map();
88
- const unique = [...new Set(values.filter(Boolean))];
89
- return countByFieldBatch(collection, field, unique);
90
- },
91
- exists: async (collection, filters) => {
92
- const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
93
- const uri = await findUriByFields(collection, conditions);
94
- return uri !== null;
95
- },
96
- labels: queryLabelsForUris,
97
- blobUrl,
98
- };
79
+ const ctx = buildXrpcContext(params, undefined, 1, null);
99
80
  ctx.fetchImage = async (url) => {
100
81
  try {
101
82
  const resp = await fetch(url, { redirect: 'follow' });
@@ -117,7 +98,7 @@ export async function initOpengraph(ogDir) {
117
98
  ...result.options,
118
99
  fonts: [...(defaultFont ? [defaultFont] : []), ...(result.options?.fonts || [])],
119
100
  };
120
- const svg = await satori(element, options);
101
+ const svg = await (await getSatori())(element, options);
121
102
  return { svg, meta: result.meta };
122
103
  },
123
104
  });
@@ -129,6 +110,58 @@ export async function initOpengraph(ogDir) {
129
110
  }
130
111
  }
131
112
  }
113
+ /** Register a single OG handler from a scanned server/ module. */
114
+ export function registerOgHandler(ogMod) {
115
+ const { pattern, paramNames } = compilePath(ogMod.path);
116
+ const name = ogMod.path.replace(/^\//, '').replace(/\//g, '-').replace(/:/g, '');
117
+ // Load default font if not already loaded
118
+ if (!defaultFont) {
119
+ try {
120
+ const fontPath = resolve(import.meta.dirname, '..', 'fonts', 'Inter-Regular.woff');
121
+ const fontData = readFileSync(fontPath);
122
+ defaultFont = { name: 'Inter', data: fontData.buffer, weight: 400, style: 'normal' };
123
+ }
124
+ catch { }
125
+ }
126
+ handlers.push({
127
+ name,
128
+ path: ogMod.path,
129
+ pattern,
130
+ paramNames,
131
+ execute: async (params) => {
132
+ const ctx = buildXrpcContext(params, undefined, 1, null);
133
+ ctx.fetchImage = async (url) => {
134
+ try {
135
+ const resp = await fetch(url, { redirect: 'follow' });
136
+ if (!resp.ok)
137
+ return null;
138
+ const buf = Buffer.from(await resp.arrayBuffer());
139
+ const contentType = resp.headers.get('content-type') || 'image/jpeg';
140
+ return `data:${contentType};base64,${buf.toString('base64')}`;
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ };
146
+ const result = await ogMod.generate(ctx);
147
+ const element = result.element;
148
+ const options = {
149
+ width: 1200,
150
+ height: 630,
151
+ ...result.options,
152
+ fonts: [...(defaultFont ? [defaultFont] : []), ...(result.options?.fonts || [])],
153
+ };
154
+ const svg = await (await getSatori())(element, options);
155
+ return { svg, meta: result.meta };
156
+ },
157
+ });
158
+ const pagePath = ogMod.path.replace(/^\/og/, '');
159
+ if (pagePath !== ogMod.path) {
160
+ const compiled = compilePath(pagePath);
161
+ pageRoutes.push({ ogPath: ogMod.path, pattern: compiled.pattern, paramNames: compiled.paramNames, name });
162
+ }
163
+ log(`[opengraph] registered: ${name} → ${ogMod.path}`);
164
+ }
132
165
  export async function handleOpengraphRequest(pathname) {
133
166
  const cached = cache.get(pathname);
134
167
  if (cached && cached.expires > Date.now())
@@ -143,6 +176,7 @@ export async function handleOpengraphRequest(pathname) {
143
176
  });
144
177
  try {
145
178
  const { svg, meta } = await handler.execute(params);
179
+ const Resvg = await getResvg();
146
180
  const png = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }).render().asPng();
147
181
  if (cache.size >= CACHE_MAX) {
148
182
  const oldest = cache.keys().next().value;
@@ -0,0 +1,42 @@
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 class ScopeMissingProxyError extends ProxyError {
7
+ constructor();
8
+ }
9
+ export declare function pdsCreateRecord(oauthConfig: OAuthConfig, viewer: {
10
+ did: string;
11
+ }, input: {
12
+ collection: string;
13
+ repo?: string;
14
+ rkey?: string;
15
+ record: Record<string, unknown>;
16
+ }): Promise<{
17
+ uri?: string;
18
+ cid?: string;
19
+ }>;
20
+ export declare function pdsDeleteRecord(oauthConfig: OAuthConfig, viewer: {
21
+ did: string;
22
+ }, input: {
23
+ collection: string;
24
+ rkey: string;
25
+ }): Promise<Record<string, unknown>>;
26
+ export declare function pdsPutRecord(oauthConfig: OAuthConfig, viewer: {
27
+ did: string;
28
+ }, input: {
29
+ collection: string;
30
+ rkey: string;
31
+ record: Record<string, unknown>;
32
+ repo?: string;
33
+ }): Promise<{
34
+ uri?: string;
35
+ cid?: string;
36
+ }>;
37
+ export declare function pdsUploadBlob(oauthConfig: OAuthConfig, viewer: {
38
+ did: string;
39
+ }, body: Uint8Array, contentType: string): Promise<{
40
+ blob: unknown;
41
+ }>;
42
+ //# 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;AAU9C,qBAAa,UAAW,SAAQ,KAAK;IAE1B,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAIlB;AAED,qBAAa,sBAAuB,SAAQ,UAAU;;CAIrD;AAuHD,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,CAwCzC;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,CAyBlC;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,CAqCzC;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,207 @@
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 (result.body.error === 'use_dpop_nonce') {
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
+ // The PDS returns 'InvalidToken' or 'ExpiredToken' (AT Proto PascalCase convention)
45
+ // while the OAuth spec uses 'invalid_token' (RFC 6750 snake_case)
46
+ const err = result.body.error;
47
+ if (err === 'invalid_token' || err === 'InvalidToken' || err === 'ExpiredToken') {
48
+ const refreshed = await refreshPdsSession(oauthConfig, session);
49
+ if (refreshed) {
50
+ accessToken = refreshed.accessToken;
51
+ result = await doFetch(accessToken, nonce);
52
+ if (result.ok)
53
+ return result;
54
+ if (result.body.error === 'use_dpop_nonce') {
55
+ nonce = result.headers.get('DPoP-Nonce') || undefined;
56
+ if (nonce)
57
+ result = await doFetch(accessToken, nonce);
58
+ }
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ async function proxyToPds(oauthConfig, session, method, pdsUrl, body) {
64
+ const serverKey = await getServerKey('appview-oauth-key');
65
+ const privateJwk = JSON.parse(serverKey.privateKey);
66
+ const publicJwk = JSON.parse(serverKey.publicKey);
67
+ return withDpopRetry(oauthConfig, session, async (token, nonce) => {
68
+ const proof = await createDpopProof(privateJwk, publicJwk, method, pdsUrl, token, nonce);
69
+ const res = await fetch(pdsUrl, {
70
+ method,
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ Authorization: `DPoP ${token}`,
74
+ DPoP: proof,
75
+ },
76
+ body: JSON.stringify(body),
77
+ });
78
+ const resBody = await res.json().catch(() => ({}));
79
+ return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
80
+ });
81
+ }
82
+ /** Proxy a raw binary request to the user's PDS with DPoP + nonce retry + token refresh. */
83
+ async function proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType) {
84
+ const serverKey = await getServerKey('appview-oauth-key');
85
+ const privateJwk = JSON.parse(serverKey.privateKey);
86
+ const publicJwk = JSON.parse(serverKey.publicKey);
87
+ return withDpopRetry(oauthConfig, session, async (token, nonce) => {
88
+ const proof = await createDpopProof(privateJwk, publicJwk, 'POST', pdsUrl, token, nonce);
89
+ const res = await fetch(pdsUrl, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': contentType,
93
+ 'Content-Length': String(body.length),
94
+ Authorization: `DPoP ${token}`,
95
+ DPoP: proof,
96
+ },
97
+ body: Buffer.from(body),
98
+ });
99
+ const resBody = await res.json().catch(() => ({}));
100
+ return { ok: res.ok, status: res.status, body: resBody, headers: res.headers };
101
+ });
102
+ }
103
+ // --- High-level proxy functions ---
104
+ export async function pdsCreateRecord(oauthConfig, viewer, input) {
105
+ const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
106
+ if (validationError) {
107
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
108
+ }
109
+ const session = await getSession(viewer.did);
110
+ if (!session)
111
+ throw new ProxyError(401, 'No PDS session for user');
112
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.createRecord`;
113
+ const pdsBody = {
114
+ repo: viewer.did,
115
+ collection: input.collection,
116
+ rkey: input.rkey,
117
+ record: input.record,
118
+ };
119
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
120
+ if (!pdsRes.ok)
121
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
122
+ try {
123
+ await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
124
+ await runLabelRules({
125
+ uri: String(pdsRes.body.uri),
126
+ cid: String(pdsRes.body.cid),
127
+ did: viewer.did,
128
+ collection: input.collection,
129
+ value: input.record,
130
+ });
131
+ }
132
+ catch (err) {
133
+ emit('pds-proxy', 'local_index_error', {
134
+ op: 'createRecord',
135
+ error: err instanceof Error ? err.message : String(err),
136
+ });
137
+ }
138
+ return pdsRes.body;
139
+ }
140
+ export async function pdsDeleteRecord(oauthConfig, viewer, input) {
141
+ const session = await getSession(viewer.did);
142
+ if (!session)
143
+ throw new ProxyError(401, 'No PDS session for user');
144
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.deleteRecord`;
145
+ const pdsBody = {
146
+ repo: viewer.did,
147
+ collection: input.collection,
148
+ rkey: input.rkey,
149
+ };
150
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
151
+ if (!pdsRes.ok)
152
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS delete failed'));
153
+ try {
154
+ const uri = `at://${viewer.did}/${input.collection}/${input.rkey}`;
155
+ await dbDeleteRecord(input.collection, uri);
156
+ }
157
+ catch (err) {
158
+ emit('pds-proxy', 'local_index_error', {
159
+ op: 'deleteRecord',
160
+ error: err instanceof Error ? err.message : String(err),
161
+ });
162
+ }
163
+ return pdsRes.body;
164
+ }
165
+ export async function pdsPutRecord(oauthConfig, viewer, input) {
166
+ const validationError = validateRecord(getLexiconArray(), input.collection, input.record);
167
+ if (validationError) {
168
+ throw new ProxyError(400, `InvalidRecord: ${validationError.path ? validationError.path + ': ' : ''}${validationError.message}`);
169
+ }
170
+ const session = await getSession(viewer.did);
171
+ if (!session)
172
+ throw new ProxyError(401, 'No PDS session for user');
173
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.putRecord`;
174
+ const pdsBody = {
175
+ repo: viewer.did,
176
+ collection: input.collection,
177
+ rkey: input.rkey,
178
+ record: input.record,
179
+ };
180
+ const pdsRes = await proxyToPds(oauthConfig, session, 'POST', pdsUrl, pdsBody);
181
+ if (!pdsRes.ok)
182
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS write failed'));
183
+ try {
184
+ await insertRecord(input.collection, String(pdsRes.body.uri), String(pdsRes.body.cid), viewer.did, input.record);
185
+ await runLabelRules({
186
+ uri: String(pdsRes.body.uri),
187
+ cid: String(pdsRes.body.cid),
188
+ did: viewer.did,
189
+ collection: input.collection,
190
+ value: input.record,
191
+ });
192
+ }
193
+ catch (err) {
194
+ emit('pds-proxy', 'local_index_error', { op: 'putRecord', error: err instanceof Error ? err.message : String(err) });
195
+ }
196
+ return pdsRes.body;
197
+ }
198
+ export async function pdsUploadBlob(oauthConfig, viewer, body, contentType) {
199
+ const session = await getSession(viewer.did);
200
+ if (!session)
201
+ throw new ProxyError(401, 'No PDS session for user');
202
+ const pdsUrl = `${session.pds_endpoint}/xrpc/com.atproto.repo.uploadBlob`;
203
+ const pdsRes = await proxyToPdsRaw(oauthConfig, session, pdsUrl, body, contentType);
204
+ if (!pdsRes.ok)
205
+ throw new ProxyError(pdsRes.status, String(pdsRes.body.error || 'PDS upload failed'));
206
+ return pdsRes.body;
207
+ }
@@ -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,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAsBnH"}
@@ -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,6 @@
1
+ /**
2
+ * Register a Node.js module resolve hook so dynamic import() of server files
3
+ * can resolve the $hatk alias to the generated entry points.
4
+ */
5
+ export declare function registerHatkResolveHook(): void;
6
+ //# sourceMappingURL=resolve-hatk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-hatk.d.ts","sourceRoot":"","sources":["../src/resolve-hatk.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C"}
@@ -0,0 +1,20 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { registerHooks } from 'node:module';
4
+ /**
5
+ * Register a Node.js module resolve hook so dynamic import() of server files
6
+ * can resolve the $hatk alias to the generated entry points.
7
+ */
8
+ export function registerHatkResolveHook() {
9
+ const hatkUrl = pathToFileURL(resolve('hatk.generated.ts')).href;
10
+ const hatkClientUrl = pathToFileURL(resolve('hatk.generated.client.ts')).href;
11
+ registerHooks({
12
+ resolve(specifier, context, nextResolve) {
13
+ if (specifier === '$hatk/client')
14
+ return { url: hatkClientUrl, shortCircuit: true };
15
+ if (specifier === '$hatk')
16
+ return { url: hatkUrl, shortCircuit: true };
17
+ return nextResolve(specifier, context);
18
+ },
19
+ });
20
+ }
@@ -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"}