@hatk/hatk 0.0.1-alpha.4 → 0.0.1-alpha.40

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 (150) 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 +417 -133
  17. package/dist/cloudflare/container.d.ts +73 -0
  18. package/dist/cloudflare/container.d.ts.map +1 -0
  19. package/dist/cloudflare/container.js +232 -0
  20. package/dist/cloudflare/hooks.d.ts +33 -0
  21. package/dist/cloudflare/hooks.d.ts.map +1 -0
  22. package/dist/cloudflare/hooks.js +40 -0
  23. package/dist/cloudflare/init.d.ts +27 -0
  24. package/dist/cloudflare/init.d.ts.map +1 -0
  25. package/dist/cloudflare/init.js +103 -0
  26. package/dist/cloudflare/worker.d.ts +27 -0
  27. package/dist/cloudflare/worker.d.ts.map +1 -0
  28. package/dist/cloudflare/worker.js +54 -0
  29. package/dist/config.d.ts +12 -1
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +36 -9
  32. package/dist/database/adapter-factory.d.ts +6 -0
  33. package/dist/database/adapter-factory.d.ts.map +1 -0
  34. package/dist/database/adapter-factory.js +20 -0
  35. package/dist/database/adapters/d1.d.ts +56 -0
  36. package/dist/database/adapters/d1.d.ts.map +1 -0
  37. package/dist/database/adapters/d1.js +108 -0
  38. package/dist/database/adapters/duckdb-search.d.ts +12 -0
  39. package/dist/database/adapters/duckdb-search.d.ts.map +1 -0
  40. package/dist/database/adapters/duckdb-search.js +27 -0
  41. package/dist/database/adapters/duckdb.d.ts +25 -0
  42. package/dist/database/adapters/duckdb.d.ts.map +1 -0
  43. package/dist/database/adapters/duckdb.js +161 -0
  44. package/dist/database/adapters/sqlite-search.d.ts +23 -0
  45. package/dist/database/adapters/sqlite-search.d.ts.map +1 -0
  46. package/dist/database/adapters/sqlite-search.js +74 -0
  47. package/dist/database/adapters/sqlite.d.ts +18 -0
  48. package/dist/database/adapters/sqlite.d.ts.map +1 -0
  49. package/dist/database/adapters/sqlite.js +87 -0
  50. package/dist/database/db.d.ts +159 -0
  51. package/dist/database/db.d.ts.map +1 -0
  52. package/dist/database/db.js +1445 -0
  53. package/dist/database/dialect.d.ts +45 -0
  54. package/dist/database/dialect.d.ts.map +1 -0
  55. package/dist/database/dialect.js +72 -0
  56. package/dist/database/fts.d.ts +27 -0
  57. package/dist/database/fts.d.ts.map +1 -0
  58. package/dist/database/fts.js +846 -0
  59. package/dist/database/index.d.ts +7 -0
  60. package/dist/database/index.d.ts.map +1 -0
  61. package/dist/database/index.js +6 -0
  62. package/dist/database/ports.d.ts +50 -0
  63. package/dist/database/ports.d.ts.map +1 -0
  64. package/dist/database/ports.js +1 -0
  65. package/dist/database/schema.d.ts +61 -0
  66. package/dist/database/schema.d.ts.map +1 -0
  67. package/dist/database/schema.js +394 -0
  68. package/dist/db.d.ts +1 -1
  69. package/dist/db.d.ts.map +1 -1
  70. package/dist/db.js +4 -38
  71. package/dist/dev-entry.d.ts +8 -0
  72. package/dist/dev-entry.d.ts.map +1 -0
  73. package/dist/dev-entry.js +110 -0
  74. package/dist/feeds.d.ts +12 -8
  75. package/dist/feeds.d.ts.map +1 -1
  76. package/dist/feeds.js +45 -6
  77. package/dist/fts.d.ts.map +1 -1
  78. package/dist/fts.js +5 -0
  79. package/dist/hooks.d.ts +22 -0
  80. package/dist/hooks.d.ts.map +1 -0
  81. package/dist/hooks.js +75 -0
  82. package/dist/hydrate.d.ts +6 -5
  83. package/dist/hydrate.d.ts.map +1 -1
  84. package/dist/hydrate.js +4 -16
  85. package/dist/indexer.d.ts +20 -0
  86. package/dist/indexer.d.ts.map +1 -1
  87. package/dist/indexer.js +53 -7
  88. package/dist/labels.d.ts +34 -0
  89. package/dist/labels.d.ts.map +1 -1
  90. package/dist/labels.js +66 -6
  91. package/dist/logger.d.ts +29 -0
  92. package/dist/logger.d.ts.map +1 -1
  93. package/dist/logger.js +29 -0
  94. package/dist/main.js +134 -67
  95. package/dist/mst.d.ts +18 -1
  96. package/dist/mst.d.ts.map +1 -1
  97. package/dist/mst.js +19 -8
  98. package/dist/oauth/db.d.ts.map +1 -1
  99. package/dist/oauth/db.js +43 -17
  100. package/dist/oauth/server.d.ts +2 -0
  101. package/dist/oauth/server.d.ts.map +1 -1
  102. package/dist/oauth/server.js +102 -7
  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 +189 -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/schema.d.ts +8 -0
  125. package/dist/schema.d.ts.map +1 -1
  126. package/dist/schema.js +29 -0
  127. package/dist/seed.d.ts +19 -0
  128. package/dist/seed.d.ts.map +1 -1
  129. package/dist/seed.js +43 -4
  130. package/dist/server-init.d.ts +8 -0
  131. package/dist/server-init.d.ts.map +1 -0
  132. package/dist/server-init.js +61 -0
  133. package/dist/server.d.ts +26 -3
  134. package/dist/server.d.ts.map +1 -1
  135. package/dist/server.js +528 -635
  136. package/dist/setup.d.ts +28 -1
  137. package/dist/setup.d.ts.map +1 -1
  138. package/dist/setup.js +50 -3
  139. package/dist/test.d.ts +1 -1
  140. package/dist/test.d.ts.map +1 -1
  141. package/dist/test.js +38 -32
  142. package/dist/views.js +1 -1
  143. package/dist/vite-plugin.d.ts +1 -1
  144. package/dist/vite-plugin.d.ts.map +1 -1
  145. package/dist/vite-plugin.js +254 -66
  146. package/dist/xrpc.d.ts +46 -10
  147. package/dist/xrpc.d.ts.map +1 -1
  148. package/dist/xrpc.js +128 -39
  149. package/package.json +13 -6
  150. package/public/admin.html +0 -54
package/dist/xrpc.d.ts CHANGED
@@ -1,26 +1,35 @@
1
+ import type { BaseContext } from './hydrate.ts';
1
2
  import type { Row, FlatRow } from './lex-types.ts';
2
3
  export type { Row, FlatRow };
4
+ /** Thrown from XRPC handlers to return a 400 response with an error message. */
3
5
  export declare class InvalidRequestError extends Error {
4
6
  status: number;
5
7
  errorName?: string;
6
8
  constructor(message: string, errorName?: string);
7
9
  }
10
+ /** Thrown from XRPC handlers to return a 404 response. */
8
11
  export declare class NotFoundError extends InvalidRequestError {
9
12
  status: number;
10
13
  constructor(message?: string);
11
14
  }
12
- export interface XrpcContext<P = Record<string, string>, Records extends Record<string, any> = Record<string, any>, I = unknown> {
15
+ /**
16
+ * Context passed to every XRPC handler. Provides database access, pagination
17
+ * helpers, viewer auth, record resolution, full-text search, label queries,
18
+ * and blob URL generation.
19
+ *
20
+ * @typeParam P - Query parameter types (derived from lexicon)
21
+ * @typeParam Records - Map of collection NSID → record type (from generated types)
22
+ * @typeParam I - Input body type for procedure calls
23
+ */
24
+ export interface XrpcContext<P = Record<string, string>, Records extends Record<string, any> = Record<string, any>, I = unknown> extends BaseContext {
13
25
  db: {
14
- query: (sql: string, params?: any[]) => Promise<any[]>;
15
- run: (sql: string, ...params: any[]) => Promise<void>;
26
+ query: (sql: string, params?: unknown[]) => Promise<unknown[]>;
27
+ run: (sql: string, params?: unknown[]) => Promise<void>;
16
28
  };
17
29
  params: P;
18
30
  input: I;
19
31
  cursor?: string;
20
32
  limit: number;
21
- viewer: {
22
- did: string;
23
- } | null;
24
33
  packCursor: (primary: string | number, cid: string) => string;
25
34
  unpackCursor: (cursor: string) => {
26
35
  primary: string;
@@ -37,17 +46,44 @@ export interface XrpcContext<P = Record<string, string>, Records extends Record<
37
46
  cursor?: string;
38
47
  }>;
39
48
  resolve: <R = unknown>(uris: string[]) => Promise<Row<R>[]>;
40
- lookup: <R = any>(collection: string, field: string, values: string[]) => Promise<Map<string, Row<R>>>;
41
- count: (collection: string, field: string, values: string[]) => Promise<Map<string, number>>;
42
49
  exists: (collection: string, filters: Record<string, string>) => Promise<boolean>;
43
- labels: (uris: string[]) => Promise<Map<string, any[]>>;
44
- blobUrl: (did: string, ref: unknown, preset?: 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize') => string | undefined;
45
50
  }
51
+ /** Set the relay URL used for blob URL generation. Called once during boot. */
46
52
  export declare function configureRelay(relay: string): void;
53
+ /**
54
+ * Generate a CDN URL for a blob ref. Uses the PDS directly in local dev,
55
+ * or the Bluesky CDN (`cdn.bsky.app`) in production.
56
+ */
47
57
  export declare function blobUrl(did: string, ref: unknown, preset?: 'avatar' | 'banner' | 'feed_thumbnail' | 'feed_fullsize'): string | undefined;
58
+ /** Build a full XrpcContext from request parameters. Reuses buildBaseContext for shared fields. */
59
+ export declare function buildXrpcContext(params: Record<string, string>, cursor: string | undefined, limit: number, viewer: {
60
+ did: string;
61
+ handle?: string;
62
+ } | null, input?: unknown): XrpcContext;
63
+ /**
64
+ * Discover and load XRPC handler modules from the `xrpc/` directory.
65
+ * Directory nesting maps to NSID segments. Parameters are validated and
66
+ * coerced against the matching lexicon definition.
67
+ */
48
68
  export declare function initXrpc(xrpcDir: string): Promise<void>;
69
+ /** Register a single XRPC handler from a scanned server/ module. */
70
+ export declare function registerXrpcHandler(nsid: string, handlerModule: {
71
+ handler: (ctx: any) => Promise<any>;
72
+ }): void;
73
+ /** Execute a registered XRPC handler by name. Returns null if no handler matches. */
49
74
  export declare function executeXrpc(name: string, params: Record<string, string>, cursor: string | undefined, limit: number, viewer?: {
50
75
  did: string;
51
76
  } | null, input?: unknown): Promise<any | null>;
77
+ /** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
78
+ export declare function callXrpc(nsid: string, params?: Record<string, any>, input?: unknown): Promise<any>;
79
+ /**
80
+ * Register a core XRPC handler directly (no XrpcContext wrapping).
81
+ * Used for built-in dev.hatk.* handlers that manage their own dependencies.
82
+ */
83
+ export declare function registerCoreXrpcHandler(nsid: string, fn: (params: Record<string, string>, cursor: string | undefined, limit: number, viewer: {
84
+ did: string;
85
+ handle?: string;
86
+ } | null, input?: unknown) => Promise<any>): void;
87
+ /** Return all registered XRPC method names. */
52
88
  export declare function listXrpc(): string[];
53
89
  //# sourceMappingURL=xrpc.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"xrpc.d.ts","sourceRoot":"","sources":["../src/xrpc.ts"],"names":[],"mappings":"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"}
package/dist/xrpc.js CHANGED
@@ -6,12 +6,33 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
+ /**
10
+ * XRPC method handler system for serving AT Protocol endpoints.
11
+ *
12
+ * Place handler modules in the `xrpc/` directory, nested by NSID segments
13
+ * (e.g. `xrpc/app/bsky/feed/getAuthorFeed.ts` → `app.bsky.feed.getAuthorFeed`).
14
+ * Each module default-exports a `{ handler }` function that receives an
15
+ * {@link XrpcContext} with database access, query params, pagination, and
16
+ * viewer auth.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * // xrpc/xyz/statusphere/getStatuses.ts
21
+ * import { defineXrpc } from '../../hatk.generated.ts'
22
+ *
23
+ * export default defineXrpc('xyz.statusphere.getStatuses', async (ctx) => {
24
+ * const rows = await ctx.db.query('SELECT * FROM statusphere_status LIMIT ?', [ctx.limit])
25
+ * return { statuses: rows }
26
+ * })
27
+ * ```
28
+ */
9
29
  import { resolve, relative } from 'node:path';
10
30
  import { readdirSync, statSync } from 'node:fs';
11
- import { log } from "./logger.js";
12
- import { querySQL, runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, lookupByFieldBatch, countByFieldBatch, queryLabelsForUris, } from "./db.js";
13
- import { resolveRecords } from "./hydrate.js";
14
- import { getLexicon } from "./schema.js";
31
+ import { log, emit, timer } from "./logger.js";
32
+ import { querySQL, runSQL, packCursor, unpackCursor, isTakendownDid, filterTakendownDids, searchRecords, findUriByFields, } from "./database/db.js";
33
+ import { resolveRecords, buildBaseContext } from "./hydrate.js";
34
+ import { getLexicon } from "./database/schema.js";
35
+ /** Thrown from XRPC handlers to return a 400 response with an error message. */
15
36
  export class InvalidRequestError extends Error {
16
37
  status = 400;
17
38
  errorName;
@@ -20,6 +41,7 @@ export class InvalidRequestError extends Error {
20
41
  this.errorName = errorName;
21
42
  }
22
43
  }
44
+ /** Thrown from XRPC handlers to return a 404 response. */
23
45
  export class NotFoundError extends InvalidRequestError {
24
46
  status = 404;
25
47
  constructor(message = 'Not found') {
@@ -27,9 +49,14 @@ export class NotFoundError extends InvalidRequestError {
27
49
  }
28
50
  }
29
51
  let _relayUrl = '';
52
+ /** Set the relay URL used for blob URL generation. Called once during boot. */
30
53
  export function configureRelay(relay) {
31
54
  _relayUrl = relay;
32
55
  }
56
+ /**
57
+ * Generate a CDN URL for a blob ref. Uses the PDS directly in local dev,
58
+ * or the Bluesky CDN (`cdn.bsky.app`) in production.
59
+ */
33
60
  export function blobUrl(did, ref, preset = 'avatar') {
34
61
  if (!ref)
35
62
  return undefined;
@@ -41,7 +68,31 @@ export function blobUrl(did, ref, preset = 'avatar') {
41
68
  }
42
69
  return `https://cdn.bsky.app/img/${preset}/plain/${did}/${p.ref.$link}@jpeg`;
43
70
  }
71
+ /** Build a full XrpcContext from request parameters. Reuses buildBaseContext for shared fields. */
72
+ export function buildXrpcContext(params, cursor, limit, viewer, input) {
73
+ const base = buildBaseContext(viewer);
74
+ return {
75
+ ...base,
76
+ db: { query: querySQL, run: runSQL },
77
+ params,
78
+ input: input || {},
79
+ cursor,
80
+ limit,
81
+ packCursor,
82
+ unpackCursor,
83
+ isTakendown: isTakendownDid,
84
+ filterTakendownDids,
85
+ search: searchRecords,
86
+ resolve: resolveRecords,
87
+ exists: async (collection, filters) => {
88
+ const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
89
+ const uri = await findUriByFields(collection, conditions);
90
+ return uri !== null;
91
+ },
92
+ };
93
+ }
44
94
  const handlers = new Map();
95
+ /** Recursively collect .ts/.js files in a directory, skipping files prefixed with `_`. */
45
96
  function walkDir(dir) {
46
97
  const results = [];
47
98
  try {
@@ -58,6 +109,11 @@ function walkDir(dir) {
58
109
  catch { }
59
110
  return results.sort();
60
111
  }
112
+ /**
113
+ * Discover and load XRPC handler modules from the `xrpc/` directory.
114
+ * Directory nesting maps to NSID segments. Parameters are validated and
115
+ * coerced against the matching lexicon definition.
116
+ */
61
117
  export async function initXrpc(xrpcDir) {
62
118
  const files = walkDir(xrpcDir);
63
119
  if (files.length === 0)
@@ -65,7 +121,7 @@ export async function initXrpc(xrpcDir) {
65
121
  for (const scriptPath of files) {
66
122
  const rel = relative(xrpcDir, scriptPath).replace(/\.(ts|js)$/, '');
67
123
  const name = rel.replace(/[\\/]/g, '.');
68
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
124
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
69
125
  const handler = mod.default;
70
126
  // Extract param schema from lexicon for validation and defaults
71
127
  const lexicon = getLexicon(name);
@@ -89,51 +145,84 @@ export async function initXrpc(xrpcDir) {
89
145
  throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
90
146
  }
91
147
  }
92
- const ctx = {
93
- db: { query: querySQL, run: runSQL },
94
- params,
95
- input: input || {},
96
- cursor,
97
- limit,
98
- viewer,
99
- packCursor,
100
- unpackCursor,
101
- isTakendown: isTakendownDid,
102
- filterTakendownDids,
103
- search: searchRecords,
104
- resolve: resolveRecords,
105
- lookup: async (collection, field, values) => {
106
- if (values.length === 0)
107
- return new Map();
108
- const unique = [...new Set(values.filter(Boolean))];
109
- return lookupByFieldBatch(collection, field, unique);
110
- },
111
- count: async (collection, field, values) => {
112
- if (values.length === 0)
113
- return new Map();
114
- const unique = [...new Set(values.filter(Boolean))];
115
- return countByFieldBatch(collection, field, unique);
116
- },
117
- exists: async (collection, filters) => {
118
- const conditions = Object.entries(filters).map(([field, value]) => ({ field, value }));
119
- const uri = await findUriByFields(collection, conditions);
120
- return uri !== null;
121
- },
122
- labels: queryLabelsForUris,
123
- blobUrl,
124
- };
148
+ const ctx = buildXrpcContext(params, cursor, limit, viewer, input);
125
149
  return handler.handler(ctx);
126
150
  },
127
151
  });
128
152
  log(`[xrpc] discovered: ${name}`);
129
153
  }
130
154
  }
155
+ /** Register a single XRPC handler from a scanned server/ module. */
156
+ export function registerXrpcHandler(nsid, handlerModule) {
157
+ const lexicon = getLexicon(nsid);
158
+ const paramsDef = lexicon?.defs?.main?.parameters;
159
+ const requiredParams = paramsDef?.required || [];
160
+ const paramProperties = paramsDef?.properties || {};
161
+ handlers.set(nsid, {
162
+ name: nsid,
163
+ execute: async (params, cursor, limit, viewer, input) => {
164
+ for (const [key, def] of Object.entries(paramProperties)) {
165
+ if (params[key] == null && def.default != null) {
166
+ params[key] = String(def.default);
167
+ }
168
+ if (params[key] != null && def.type === 'integer') {
169
+ params[key] = Number(params[key]);
170
+ }
171
+ }
172
+ for (const param of requiredParams) {
173
+ if (!params[param]) {
174
+ throw new InvalidRequestError(`Missing required parameter: ${param}`, 'InvalidRequest');
175
+ }
176
+ }
177
+ const ctx = buildXrpcContext(params, cursor, limit, viewer, input);
178
+ return handlerModule.handler(ctx);
179
+ },
180
+ });
181
+ }
182
+ /** Execute a registered XRPC handler by name. Returns null if no handler matches. */
131
183
  export async function executeXrpc(name, params, cursor, limit, viewer, input) {
132
184
  const handler = handlers.get(name);
133
185
  if (!handler)
134
186
  return null;
135
- return handler.execute(params, cursor, limit, viewer || null, input);
187
+ const elapsed = timer();
188
+ try {
189
+ const result = await handler.execute(params, cursor, limit, viewer || null, input);
190
+ emit('xrpc', name, { duration_ms: elapsed(), params, cursor, limit, viewer: viewer?.did });
191
+ return result;
192
+ }
193
+ catch (err) {
194
+ emit('xrpc', name, { duration_ms: elapsed(), params, cursor, limit, viewer: viewer?.did, error: err.message });
195
+ throw err;
196
+ }
197
+ }
198
+ /** Call a registered XRPC handler directly (no HTTP). For use in SSR renderers. */
199
+ export async function callXrpc(nsid, params = {}, input) {
200
+ const viewer = globalThis.__hatk_viewer ?? null;
201
+ // In externalized module context (e.g. SSR), delegate to the runner's callXrpc via globalThis.
202
+ // The runner's module instance has all registered handlers; this (Node's) instance may not.
203
+ if (handlers.size === 0 && globalThis.__hatk_callXrpc) {
204
+ return globalThis.__hatk_callXrpc(nsid, params, input);
205
+ }
206
+ const stringParams = {};
207
+ for (const [k, v] of Object.entries(params)) {
208
+ if (v != null)
209
+ stringParams[k] = String(v);
210
+ }
211
+ const limit = params.limit ? Number(params.limit) : 20;
212
+ const cursor = params.cursor ?? undefined;
213
+ const result = await executeXrpc(nsid, stringParams, cursor, limit, viewer, input);
214
+ if (result === null)
215
+ throw new Error(`No XRPC handler registered for ${nsid}`);
216
+ return result;
217
+ }
218
+ /**
219
+ * Register a core XRPC handler directly (no XrpcContext wrapping).
220
+ * Used for built-in dev.hatk.* handlers that manage their own dependencies.
221
+ */
222
+ export function registerCoreXrpcHandler(nsid, fn) {
223
+ handlers.set(nsid, { name: nsid, execute: fn });
136
224
  }
225
+ /** Return all registered XRPC method names. */
137
226
  export function listXrpc() {
138
227
  return Array.from(handlers.keys());
139
228
  }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@hatk/hatk",
3
- "version": "0.0.1-alpha.4",
3
+ "version": "0.0.1-alpha.40",
4
+ "license": "MIT",
4
5
  "bin": {
5
6
  "hatk": "dist/cli.js"
6
7
  },
@@ -19,27 +20,33 @@
19
20
  "./xrpc-client": "./dist/xrpc-client.js",
20
21
  "./views": "./dist/views.js",
21
22
  "./seed": "./dist/seed.js",
23
+ "./hooks": "./dist/hooks.js",
22
24
  "./setup": "./dist/setup.js",
23
25
  "./test": "./dist/test.js",
24
- "./test/browser": "./dist/test-browser.js",
25
- "./vite-plugin": "./dist/vite-plugin.js"
26
+ "./config": "./dist/config.js",
27
+ "./vite-plugin": "./dist/vite-plugin.js",
28
+ "./renderer": "./dist/renderer.js"
26
29
  },
27
30
  "scripts": {
28
31
  "build": "tsc -p tsconfig.build.json",
29
32
  "prepublishOnly": "npm run build"
30
33
  },
31
34
  "dependencies": {
32
- "@bigmoves/lexicon": "^0.2.1",
35
+ "@bigmoves/lexicon": "^0.2.2",
33
36
  "@duckdb/node-api": "^1.4.4-r.1",
34
37
  "@hatk/oauth-client": "*",
35
38
  "@resvg/resvg-js": "^2.6.2",
39
+ "better-sqlite3": "^12.6.2",
36
40
  "satori": "^0.19.2",
37
41
  "vitest": "^4",
38
42
  "yaml": "^2.7.0"
39
43
  },
40
44
  "devDependencies": {
41
- "@playwright/test": "^1.58.2",
45
+ "@types/better-sqlite3": "^7.6.13",
42
46
  "@types/react": "^19.2.14",
43
- "vite": "^6"
47
+ "vite": "^8.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "vite": "^8.0.0"
44
51
  }
45
52
  }
package/public/admin.html CHANGED
@@ -783,23 +783,6 @@
783
783
  font-size: 1rem;
784
784
  }
785
785
 
786
- /* ── Schema ── */
787
- .schema-pre {
788
- font-family: var(--mono);
789
- font-size: 0.8rem;
790
- line-height: 1.6;
791
- padding: 1rem;
792
- margin: 0;
793
- background: var(--bg-recessed);
794
- border-radius: 0 0 6px 6px;
795
- white-space: pre-wrap;
796
- word-break: break-word;
797
- color: var(--text);
798
- overflow-x: auto;
799
- }
800
- .schema-section {
801
- margin-bottom: 1.5rem;
802
- }
803
786
  .loading {
804
787
  color: var(--text-3);
805
788
  font-size: 0.9375rem;
@@ -1220,7 +1203,6 @@
1220
1203
  <button class="tab active" data-tab="overview">Overview</button>
1221
1204
  <button class="tab" data-tab="repos">Repos</button>
1222
1205
  <button class="tab" data-tab="content">Content</button>
1223
- <button class="tab" data-tab="schema">Schema</button>
1224
1206
  </nav>
1225
1207
 
1226
1208
  <!-- Overview -->
@@ -1277,10 +1259,6 @@
1277
1259
  <div id="repos-results"><div class="loading">Loading</div></div>
1278
1260
  </div>
1279
1261
 
1280
- <!-- Schema -->
1281
- <div class="tab-panel" id="panel-schema">
1282
- <div id="schema-results"><div class="loading">Loading</div></div>
1283
- </div>
1284
1262
 
1285
1263
  <!-- Content -->
1286
1264
  <div class="tab-panel" id="panel-content">
@@ -1307,7 +1285,6 @@
1307
1285
  <button class="bnav-btn active" data-tab="overview">Overview</button>
1308
1286
  <button class="bnav-btn" data-tab="repos">Repos</button>
1309
1287
  <button class="bnav-btn" data-tab="content">Content</button>
1310
- <button class="bnav-btn" data-tab="schema">Schema</button>
1311
1288
  </div>
1312
1289
  </div>
1313
1290
  </div>
@@ -1490,7 +1467,6 @@
1490
1467
  document.getElementById(`panel-${tab}`).classList.add('active')
1491
1468
  if (tab === 'overview') loadOverview()
1492
1469
  if (tab === 'repos') loadRepos()
1493
- if (tab === 'schema') loadSchema()
1494
1470
  if (tab === 'content') loadContent()
1495
1471
  if (push) pushURL({ tab, status: '', q: '', offset: 0, cq: '' })
1496
1472
  }
@@ -1599,36 +1575,6 @@
1599
1575
  }
1600
1576
  })
1601
1577
 
1602
- // ── Schema ──
1603
-
1604
- async function loadSchema() {
1605
- const container = document.getElementById('schema-results')
1606
- try {
1607
- const data = await api('/admin/schema')
1608
- let html = ''
1609
-
1610
- // Lexicons section
1611
- if (data.lexicons && data.lexicons.length) {
1612
- html += '<div class="schema-section"><div class="section-label">Lexicons</div>'
1613
- for (const lex of data.lexicons) {
1614
- html += `<div class="card" style="margin-bottom:0.5rem;"><div style="font-family:var(--mono);font-size:0.8rem;font-weight:600;padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);">${escapeHtml(lex.nsid)}</div><pre class="schema-pre">${escapeHtml(JSON.stringify(lex.lexicon, null, 2))}</pre></div>`
1615
- }
1616
- html += '</div>'
1617
- }
1618
-
1619
- // DDL section
1620
- if (data.ddl) {
1621
- html += '<div class="schema-section"><div class="section-label">Tables (DuckDB DDL)</div>'
1622
- html += `<div class="card"><pre class="schema-pre">${escapeHtml(data.ddl)}</pre></div>`
1623
- html += '</div>'
1624
- }
1625
-
1626
- container.innerHTML = html || '<div class="empty-state">No schema found</div>'
1627
- } catch (e) {
1628
- container.innerHTML = `<div class="empty-state">${escapeHtml(e.message)}</div>`
1629
- }
1630
- }
1631
-
1632
1578
  // ── Repos ──
1633
1579
 
1634
1580
  let reposLoaded = false