@hatk/hatk 0.0.1-alpha.24 → 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 (61) 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/cli.js +186 -66
  5. package/dist/config.d.ts +1 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +1 -1
  8. package/dist/database/db.d.ts.map +1 -1
  9. package/dist/database/db.js +5 -1
  10. package/dist/dev-entry.d.ts +8 -0
  11. package/dist/dev-entry.d.ts.map +1 -0
  12. package/dist/dev-entry.js +109 -0
  13. package/dist/feeds.d.ts +4 -0
  14. package/dist/feeds.d.ts.map +1 -1
  15. package/dist/feeds.js +41 -2
  16. package/dist/hooks.d.ts +7 -0
  17. package/dist/hooks.d.ts.map +1 -1
  18. package/dist/hooks.js +11 -1
  19. package/dist/labels.d.ts +14 -0
  20. package/dist/labels.d.ts.map +1 -1
  21. package/dist/labels.js +13 -1
  22. package/dist/main.js +49 -17
  23. package/dist/oauth/server.d.ts +2 -0
  24. package/dist/oauth/server.d.ts.map +1 -1
  25. package/dist/oauth/server.js +91 -1
  26. package/dist/oauth/session.d.ts +9 -0
  27. package/dist/oauth/session.d.ts.map +1 -0
  28. package/dist/oauth/session.js +65 -0
  29. package/dist/opengraph.d.ts +10 -0
  30. package/dist/opengraph.d.ts.map +1 -1
  31. package/dist/opengraph.js +102 -4
  32. package/dist/pds-proxy.d.ts +39 -0
  33. package/dist/pds-proxy.d.ts.map +1 -0
  34. package/dist/pds-proxy.js +173 -0
  35. package/dist/renderer.d.ts +27 -0
  36. package/dist/renderer.d.ts.map +1 -0
  37. package/dist/renderer.js +46 -0
  38. package/dist/response.d.ts +16 -0
  39. package/dist/response.d.ts.map +1 -0
  40. package/dist/response.js +69 -0
  41. package/dist/scanner.d.ts +21 -0
  42. package/dist/scanner.d.ts.map +1 -0
  43. package/dist/scanner.js +88 -0
  44. package/dist/server-init.d.ts +8 -0
  45. package/dist/server-init.d.ts.map +1 -0
  46. package/dist/server-init.js +59 -0
  47. package/dist/server.d.ts +26 -3
  48. package/dist/server.d.ts.map +1 -1
  49. package/dist/server.js +473 -616
  50. package/dist/setup.d.ts +7 -0
  51. package/dist/setup.d.ts.map +1 -1
  52. package/dist/setup.js +13 -1
  53. package/dist/test.d.ts.map +1 -1
  54. package/dist/test.js +12 -22
  55. package/dist/vite-plugin.d.ts +1 -1
  56. package/dist/vite-plugin.d.ts.map +1 -1
  57. package/dist/vite-plugin.js +245 -75
  58. package/dist/xrpc.d.ts +13 -0
  59. package/dist/xrpc.d.ts.map +1 -1
  60. package/dist/xrpc.js +87 -1
  61. package/package.json +8 -5
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Dev mode entry point — loaded through Vite's module runner.
3
+ * Boots hatk infrastructure and exports the fetch handler.
4
+ */
5
+ import { loadConfig } from "./config.js";
6
+ import { loadLexicons, storeLexicons, discoverCollections, buildSchemas } from "./database/schema.js";
7
+ import { discoverViews } from "./views.js";
8
+ import { initDatabase, migrateSchema, getSchemaDump } from "./database/db.js";
9
+ import { createAdapter } from "./database/adapter-factory.js";
10
+ import { getDialect } from "./database/dialect.js";
11
+ import { setSearchPort } from "./database/fts.js";
12
+ import { configureRelay } from "./xrpc.js";
13
+ import { initOAuth } from "./oauth/server.js";
14
+ import { initServer } from "./server-init.js";
15
+ import { createHandler, registerCoreHandlers } from "./server.js";
16
+ import { startIndexer } from "./indexer.js";
17
+ import { getCursor } from "./database/db.js";
18
+ import { runBackfill } from "./backfill.js";
19
+ import { rebuildAllIndexes } from "./database/fts.js";
20
+ import { relayHttpUrl } from "./config.js";
21
+ import { validateLexicons } from '@bigmoves/lexicon';
22
+ import { log } from "./logger.js";
23
+ import { mkdirSync } from 'node:fs';
24
+ import { dirname, resolve } from 'node:path';
25
+ process.env.DEV_MODE = '1';
26
+ // Boot sequence (mirrors main.ts but exports handler instead of starting server)
27
+ const configPath = 'hatk.config.ts';
28
+ const configDir = dirname(resolve(configPath));
29
+ const config = await loadConfig(configPath);
30
+ configureRelay(config.relay);
31
+ const lexicons = loadLexicons(resolve(configDir, 'lexicons'));
32
+ const lexiconErrors = validateLexicons([...lexicons.values()]);
33
+ if (lexiconErrors) {
34
+ for (const [nsid, errors] of Object.entries(lexiconErrors)) {
35
+ for (const err of errors)
36
+ console.error(`Invalid lexicon ${nsid}: ${err}`);
37
+ }
38
+ throw new Error('Invalid lexicons');
39
+ }
40
+ storeLexicons(lexicons);
41
+ const collections = config.collections.length > 0 ? config.collections : discoverCollections(lexicons);
42
+ discoverViews();
43
+ const engineDialect = getDialect(config.databaseEngine);
44
+ const { schemas, ddlStatements } = buildSchemas(lexicons, collections, engineDialect);
45
+ if (config.database !== ':memory:') {
46
+ mkdirSync(dirname(config.database), { recursive: true });
47
+ }
48
+ const { adapter, searchPort } = await createAdapter(config.databaseEngine);
49
+ setSearchPort(searchPort);
50
+ await initDatabase(adapter, config.database, schemas, ddlStatements);
51
+ await migrateSchema(schemas);
52
+ // Write db/schema.sql
53
+ try {
54
+ const { mkdirSync, writeFileSync } = await import('node:fs');
55
+ const schemaDir = resolve(configDir, 'db');
56
+ mkdirSync(schemaDir, { recursive: true });
57
+ const schemaDump = await getSchemaDump();
58
+ writeFileSync(resolve(schemaDir, 'schema.sql'), `-- This file is auto-generated by hatk on startup. Do not edit.\n-- Database engine: ${config.databaseEngine}\n\n${schemaDump}\n`);
59
+ log(`[hatk] Schema written to db/schema.sql`);
60
+ }
61
+ catch { }
62
+ // Initialize handlers from server/ directory
63
+ await initServer(resolve(configDir, 'server'));
64
+ // Register built-in dev.hatk.* handlers so callXrpc() can find them
65
+ registerCoreHandlers(collections, config.oauth);
66
+ if (config.oauth) {
67
+ await initOAuth(config.oauth, config.plc, config.relay);
68
+ }
69
+ // Start indexer
70
+ const collectionSet = new Set(collections);
71
+ const cursor = await getCursor('relay');
72
+ startIndexer({
73
+ relayUrl: config.relay,
74
+ collections: collectionSet,
75
+ signalCollections: config.backfill.signalCollections ? new Set(config.backfill.signalCollections) : undefined,
76
+ pinnedRepos: config.backfill.repos ? new Set(config.backfill.repos) : undefined,
77
+ cursor,
78
+ fetchTimeout: config.backfill.fetchTimeout,
79
+ maxRetries: config.backfill.maxRetries,
80
+ parallelism: config.backfill.parallelism,
81
+ ftsRebuildInterval: config.ftsRebuildInterval,
82
+ });
83
+ // Run backfill in background (no restart in dev mode)
84
+ runBackfill({
85
+ pdsUrl: relayHttpUrl(config.relay),
86
+ plcUrl: config.plc,
87
+ collections: collectionSet,
88
+ config: config.backfill,
89
+ }).then(() => rebuildAllIndexes(Array.from(collectionSet)))
90
+ .catch((err) => console.error('[backfill]', err.message));
91
+ // Export the handler for Vite middleware
92
+ export const handler = createHandler({
93
+ collections: Array.from(collectionSet),
94
+ publicDir: null, // Vite serves static assets in dev
95
+ oauth: config.oauth,
96
+ admins: config.admins,
97
+ });
98
+ /** Re-scan server/ directory to pick up changed handlers in dev mode. */
99
+ export async function reloadServer() {
100
+ await initServer(resolve(configDir, 'server'));
101
+ }
102
+ export { renderPage } from "./renderer.js";
103
+ export { getRenderer } from "./renderer.js";
104
+ export { callXrpc } from "./xrpc.js";
105
+ export { parseSessionCookie, getSessionCookieName } from "./oauth/session.js";
106
+ log(`[hatk] Dev server ready`);
107
+ log(` Relay: ${config.relay}`);
108
+ log(` Database: ${config.database}`);
109
+ log(` Collections: ${collections.join(', ')}`);
package/dist/feeds.d.ts CHANGED
@@ -71,13 +71,17 @@ export declare function defineFeed(opts: FeedOpts): {
71
71
  view?: string;
72
72
  label: string;
73
73
  hydrate?: (ctx: HydrateContext<any>) => Promise<unknown[]>;
74
+ __type: "feed";
74
75
  } | {
75
76
  generate: (ctx: any) => Promise<Checked<FeedResult>>;
76
77
  collection?: never;
77
78
  view?: never;
78
79
  label: string;
79
80
  hydrate: (ctx: HydrateContext<any>) => Promise<unknown[]>;
81
+ __type: "feed";
80
82
  };
83
+ /** Register a single feed from a scanned server/ module. */
84
+ export declare function registerFeed(name: string, generator: ReturnType<typeof defineFeed>): void;
81
85
  export declare function initFeeds(feedsDir: string): Promise<void>;
82
86
  /** Execute a feed and run its hydrate pipeline if present. */
83
87
  export declare function executeFeed(name: string, params: Record<string, string>, cursor: string | undefined, limit: number, viewer?: {
@@ -1 +1 @@
1
- {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"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"}
package/dist/feeds.js CHANGED
@@ -59,7 +59,46 @@ export function createPaginate(deps) {
59
59
  };
60
60
  }
61
61
  export function defineFeed(opts) {
62
- return { ...opts, generate: (ctx) => opts.generate({ ...ctx, ok: (v) => v }) };
62
+ return { __type: 'feed', ...opts, generate: (ctx) => opts.generate({ ...ctx, ok: (v) => v }) };
63
+ }
64
+ /** Register a single feed from a scanned server/ module. */
65
+ export function registerFeed(name, generator) {
66
+ const handler = {
67
+ name,
68
+ label: generator.label || name,
69
+ collection: generator.collection,
70
+ view: generator.view,
71
+ generate: async (params, cursor, limit, viewer) => {
72
+ const paginateDeps = {
73
+ db: { query: querySQL },
74
+ cursor,
75
+ limit,
76
+ packCursor,
77
+ unpackCursor,
78
+ };
79
+ const ctx = {
80
+ db: { query: querySQL },
81
+ params,
82
+ cursor,
83
+ limit,
84
+ viewer,
85
+ packCursor,
86
+ unpackCursor,
87
+ isTakendown: isTakendownDid,
88
+ filterTakendownDids,
89
+ paginate: createPaginate(paginateDeps),
90
+ };
91
+ const result = await generator.generate(ctx);
92
+ if (Array.isArray(result)) {
93
+ return { uris: result.map((r) => r.uri || r) };
94
+ }
95
+ return { uris: result.uris, cursor: result.cursor };
96
+ },
97
+ };
98
+ if (typeof generator.hydrate === 'function') {
99
+ handler.hydrate = generator.hydrate;
100
+ }
101
+ feeds.set(name, handler);
63
102
  }
64
103
  const feeds = new Map();
65
104
  export async function initFeeds(feedsDir) {
@@ -75,7 +114,7 @@ export async function initFeeds(feedsDir) {
75
114
  for (const file of files) {
76
115
  const name = file.replace(/\.(ts|js)$/, '');
77
116
  const scriptPath = resolve(feedsDir, file);
78
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
117
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
79
118
  const generator = mod.default;
80
119
  const handler = {
81
120
  name,
package/dist/hooks.d.ts CHANGED
@@ -5,11 +5,18 @@ export type OnLoginCtx = {
5
5
  /** Trigger a backfill for a DID if it hasn't been indexed yet. */
6
6
  ensureRepo: (did: string) => Promise<void>;
7
7
  };
8
+ export declare function defineHook(event: 'on-login', handler: (ctx: OnLoginCtx) => Promise<void>): {
9
+ __type: "hook";
10
+ event: "on-login";
11
+ handler: (ctx: OnLoginCtx) => Promise<void>;
12
+ };
8
13
  /**
9
14
  * Discover and load the on-login hook from the project's `hooks/` directory.
10
15
  * Looks for `on-login.ts` or `on-login.js`. Safe to call if no hook exists.
11
16
  */
12
17
  export declare function loadOnLoginHook(hooksDir: string): Promise<void>;
18
+ /** Register a hook from a scanned server/ module. */
19
+ export declare function registerHook(event: string, handler: Function): void;
13
20
  /** Fire the on-login hook if loaded. Errors are logged but never block login. */
14
21
  export declare function fireOnLoginHook(did: string): Promise<void>;
15
22
  //# sourceMappingURL=hooks.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AA2BA,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG;IACvB,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3C,CAAA;AAMD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrE;AAQD,iFAAiF;AACjF,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhE"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AA2BA,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG;IACvB,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3C,CAAA;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC;;;mBAA5B,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC;EAExF;AAMD;;;GAGG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrE;AAQD,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,GAAG,IAAI,CAKnE;AAED,iFAAiF;AACjF,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhE"}
package/dist/hooks.js CHANGED
@@ -32,6 +32,9 @@ import { resolve } from 'node:path';
32
32
  import { log } from "./logger.js";
33
33
  import { setRepoStatus } from "./database/db.js";
34
34
  import { triggerAutoBackfill } from "./indexer.js";
35
+ export function defineHook(event, handler) {
36
+ return { __type: 'hook', event, handler };
37
+ }
35
38
  let onLoginHook = null;
36
39
  /**
37
40
  * Discover and load the on-login hook from the project's `hooks/` directory.
@@ -43,7 +46,7 @@ export async function loadOnLoginHook(hooksDir) {
43
46
  const path = existsSync(tsPath) ? tsPath : existsSync(jsPath) ? jsPath : null;
44
47
  if (!path)
45
48
  return;
46
- const mod = await import(__rewriteRelativeImportExtension(path));
49
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${path}?t=${Date.now()}`));
47
50
  onLoginHook = mod.default;
48
51
  log('[hooks] on-login hook loaded');
49
52
  }
@@ -52,6 +55,13 @@ async function ensureRepo(did) {
52
55
  await setRepoStatus(did, 'pending');
53
56
  triggerAutoBackfill(did);
54
57
  }
58
+ /** Register a hook from a scanned server/ module. */
59
+ export function registerHook(event, handler) {
60
+ if (event === 'on-login') {
61
+ onLoginHook = handler;
62
+ log('[hooks] on-login hook registered');
63
+ }
64
+ }
55
65
  /** Fire the on-login hook if loaded. Errors are logged but never block login. */
56
66
  export async function fireOnLoginHook(did) {
57
67
  if (!onLoginHook)
package/dist/labels.d.ts CHANGED
@@ -13,6 +13,15 @@ export interface LabelRuleContext {
13
13
  value: Record<string, any>;
14
14
  };
15
15
  }
16
+ export interface LabelModule {
17
+ definition?: LabelDefinition;
18
+ evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
19
+ }
20
+ export declare function defineLabels(module: LabelModule): {
21
+ definition?: LabelDefinition;
22
+ evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
23
+ __type: "labels";
24
+ };
16
25
  /**
17
26
  * Discover and load label rule modules from the `labels/` directory.
18
27
  *
@@ -23,6 +32,11 @@ export interface LabelRuleContext {
23
32
  * @param labelsDir - Absolute path to the `labels/` directory
24
33
  */
25
34
  export declare function initLabels(labelsDir: string): Promise<void>;
35
+ /** Register a single label module from a scanned server/ module. */
36
+ export declare function registerLabelModule(name: string, labelMod: {
37
+ definition?: LabelDefinition;
38
+ evaluate?: (ctx: LabelRuleContext) => Promise<string[]>;
39
+ }): void;
26
40
  /**
27
41
  * Evaluate all loaded label rules against a record and persist any resulting labels.
28
42
  * Called after each record is indexed. Rule errors are logged but never block indexing.
@@ -1 +1 @@
1
- {"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIlD,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACtD,CAAA;IACD,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAC3B,CAAA;CACF;AAYD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCjE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuCvG;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,IAAI,eAAe,EAAE,CAEvD"}
1
+ {"version":3,"file":"labels.d.ts","sourceRoot":"","sources":["../src/labels.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIlD,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE;QACF,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;QACtD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KACtD,CAAA;IACD,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,GAAG,EAAE,MAAM,CAAA;QACX,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAC3B,CAAA;CACF;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CACxD;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW;iBAJjC,eAAe;eACjB,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;;EAKxD;AAYD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCjE;AAED,oEAAoE;AACpE,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE;IAAE,UAAU,CAAC,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CAO3J;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhB;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAuCvG;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,IAAI,eAAe,EAAE,CAEvD"}
package/dist/labels.js CHANGED
@@ -38,6 +38,9 @@ import { resolve } from 'node:path';
38
38
  import { readdirSync } from 'node:fs';
39
39
  import { querySQL, runSQL, insertLabels, getSchema } from "./database/db.js";
40
40
  import { log, emit } from "./logger.js";
41
+ export function defineLabels(module) {
42
+ return { __type: 'labels', ...module };
43
+ }
41
44
  const rules = [];
42
45
  let labelDefs = [];
43
46
  let labelSrc = 'self';
@@ -63,7 +66,7 @@ export async function initLabels(labelsDir) {
63
66
  for (const file of files) {
64
67
  const name = file.replace(/\.(ts|js)$/, '');
65
68
  const scriptPath = resolve(labelsDir, file);
66
- const mod = await import(__rewriteRelativeImportExtension(scriptPath));
69
+ const mod = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ `${scriptPath}?t=${Date.now()}`));
67
70
  const handler = mod.default;
68
71
  if (handler.definition) {
69
72
  labelDefs.push(handler.definition);
@@ -82,6 +85,15 @@ export async function initLabels(labelsDir) {
82
85
  log(`[labels] ${labelDefs.length} label definitions loaded`);
83
86
  }
84
87
  }
88
+ /** Register a single label module from a scanned server/ module. */
89
+ export function registerLabelModule(name, labelMod) {
90
+ if (labelMod.definition) {
91
+ labelDefs.push(labelMod.definition);
92
+ }
93
+ if (labelMod.evaluate) {
94
+ rules.push({ name, evaluate: labelMod.evaluate });
95
+ }
96
+ }
85
97
  /**
86
98
  * Evaluate all loaded label rules against a record and persist any resulting labels.
87
99
  * Called after each record is indexed. Rule errors are logged but never block indexing.
package/dist/main.js CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, writeFileSync } from 'node:fs';
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
10
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
3
11
  import { dirname, resolve } from 'node:path';
4
12
  import { log } from "./logger.js";
5
13
  import { loadConfig } from "./config.js";
@@ -15,13 +23,15 @@ import { initOpengraph } from "./opengraph.js";
15
23
  import { initLabels, getLabelDefinitions } from "./labels.js";
16
24
  import { startIndexer } from "./indexer.js";
17
25
  import { rebuildAllIndexes } from "./database/fts.js";
18
- import { startServer } from "./server.js";
26
+ import { createHandler, registerCoreHandlers } from "./server.js";
27
+ import { serve } from "./adapter.js";
19
28
  import { validateLexicons } from '@bigmoves/lexicon';
20
29
  import { relayHttpUrl } from "./config.js";
21
30
  import { runBackfill } from "./backfill.js";
22
31
  import { initOAuth } from "./oauth/server.js";
23
32
  import { loadOnLoginHook } from "./hooks.js";
24
33
  import { initSetup } from "./setup.js";
34
+ import { initServer } from "./server-init.js";
25
35
  function logMemory(phase) {
26
36
  const mem = process.memoryUsage();
27
37
  log(`[mem] ${phase}: heap=${Math.round(mem.heapUsed / 1024 / 1024)}MB rss=${Math.round(mem.rss / 1024 / 1024)}MB external=${Math.round(mem.external / 1024 / 1024)}MB arrayBuffers=${Math.round(mem.arrayBuffers / 1024 / 1024)}MB`);
@@ -52,7 +62,6 @@ if (collections.length === 0) {
52
62
  log(`[main] Loaded config: ${collections.length} collections`);
53
63
  // Discover view defs from lexicons
54
64
  discoverViews();
55
- await loadOnLoginHook(resolve(configDir, 'hooks'));
56
65
  const engineDialect = getDialect(config.databaseEngine);
57
66
  const { schemas, ddlStatements } = buildSchemas(lexicons, collections, engineDialect);
58
67
  for (const s of schemas) {
@@ -77,8 +86,27 @@ const migrationChanges = await migrateSchema(schemas);
77
86
  if (migrationChanges.length > 0) {
78
87
  log(`[main] Applied ${migrationChanges.length} schema migration(s)`);
79
88
  }
80
- // 3b. Run setup hooks (after DB init, before server)
81
- await initSetup(resolve(configDir, 'setup'));
89
+ // 3b. Run setup hooks, feeds, xrpc, og, labels
90
+ const serverDir = resolve(configDir, 'server');
91
+ if (existsSync(serverDir)) {
92
+ // New: single server/ directory
93
+ await initServer(serverDir);
94
+ }
95
+ else {
96
+ // Legacy: separate directories
97
+ await initSetup(resolve(configDir, 'setup'));
98
+ await loadOnLoginHook(resolve(configDir, 'hooks'));
99
+ await initFeeds(resolve(configDir, 'feeds'));
100
+ log(`[main] Feeds initialized: ${listFeeds().map((f) => f.name).join(', ') || 'none'}`);
101
+ await initXrpc(resolve(configDir, 'xrpc'));
102
+ log(`[main] XRPC handlers initialized: ${listXrpc().join(', ') || 'none'}`);
103
+ await initOpengraph(resolve(configDir, 'og'));
104
+ log(`[main] OpenGraph initialized`);
105
+ await initLabels(resolve(configDir, 'labels'));
106
+ log(`[main] Labels initialized: ${getLabelDefinitions().length} definitions`);
107
+ }
108
+ // Register built-in dev.hatk.* handlers so callXrpc() can find them
109
+ registerCoreHandlers(collections, config.oauth);
82
110
  // Write db/schema.sql (after setup, so setup-created tables are included)
83
111
  try {
84
112
  const schemaDir = resolve(configDir, 'db');
@@ -100,17 +128,6 @@ try {
100
128
  }
101
129
  }
102
130
  catch { }
103
- // 4. Initialize feeds, xrpc handlers, og, labels from directories
104
- await initFeeds(resolve(configDir, 'feeds'));
105
- log(`[main] Feeds initialized: ${listFeeds()
106
- .map((f) => f.name)
107
- .join(', ') || 'none'}`);
108
- await initXrpc(resolve(configDir, 'xrpc'));
109
- log(`[main] XRPC handlers initialized: ${listXrpc().join(', ') || 'none'}`);
110
- await initOpengraph(resolve(configDir, 'og'));
111
- log(`[main] OpenGraph initialized`);
112
- await initLabels(resolve(configDir, 'labels'));
113
- log(`[main] Labels initialized: ${getLabelDefinitions().length} definitions`);
114
131
  if (config.oauth) {
115
132
  await initOAuth(config.oauth, config.plc, config.relay);
116
133
  log(`[main] OAuth initialized (issuer: ${config.oauth.issuer})`);
@@ -142,7 +159,22 @@ function runBackfillAndRestart() {
142
159
  console.error('[main] Backfill error:', err.message);
143
160
  });
144
161
  }
145
- startServer(config.port, collections, config.publicDir, config.oauth, config.admins, undefined, runBackfillAndRestart);
162
+ const handler = createHandler({
163
+ collections,
164
+ publicDir: config.publicDir,
165
+ oauth: config.oauth,
166
+ admins: config.admins,
167
+ onResync: runBackfillAndRestart,
168
+ });
169
+ // Detect SvelteKit build output and use it as fallback handler
170
+ let fallback = undefined;
171
+ const sveltekitHandler = resolve(configDir, 'build', 'handler.js');
172
+ if (existsSync(sveltekitHandler)) {
173
+ const sk = await import(__rewriteRelativeImportExtension(/* @vite-ignore */ sveltekitHandler));
174
+ fallback = sk.handler;
175
+ log(`[main] SvelteKit handler loaded from build/handler.js`);
176
+ }
177
+ serve(handler, config.port, undefined, fallback);
146
178
  log(`\nhatk running:`);
147
179
  log(` Relay: ${config.relay}`);
148
180
  log(` Database: ${config.database}`);
@@ -64,10 +64,12 @@ export declare function handlePar(config: OAuthConfig, body: Record<string, stri
64
64
  expires_in: number;
65
65
  }>;
66
66
  export declare function buildAuthorizeRedirect(config: OAuthConfig, request: any): string;
67
+ export declare function serverLogin(config: OAuthConfig, handle: string): Promise<string>;
67
68
  export declare function handleCallback(config: OAuthConfig, code: string, state: string | null, iss: string | null): Promise<{
68
69
  requestUri: string;
69
70
  clientRedirectUri: string;
70
71
  clientState: string | null;
72
+ did: string;
71
73
  }>;
72
74
  export declare function handleToken(config: OAuthConfig, body: Record<string, string>, dpopHeader: string, requestUrl: string): Promise<any>;
73
75
  export declare function refreshPdsSession(config: OAuthConfig, session: {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA0E/C,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;EAcpE;AAID,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA2ItD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAShF;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,CAyHxF;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA0JD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtF,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAmEpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/oauth/server.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA2E/C,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBrG;AAID,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;;;;;;;;;;;EAqBxE;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;EAO/E;AAED,wBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;EAWtB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW;;;;;;;;;EAcpE;AAID,wBAAsB,SAAS,CAC7B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA2ItD;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,MAAM,CAShF;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CA8FjB;AAID,wBAAsB,cAAc,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,GAAG,EAAE,MAAM,GAAG,IAAI,GACjB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAyHrG;AAID,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,CAAC,CAUd;AA0JD,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACtF,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAmEpF;AAID,wBAAsB,YAAY,CAChC,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA0BjC"}
@@ -1,6 +1,7 @@
1
1
  // packages/hatk/src/oauth/server.ts
2
2
  import { generateKeyPair, importPrivateKey, computeJwkThumbprint, signJwt, parseJwt, verifyEs256, importPublicKey, randomToken, sha256, base64UrlEncode, } from "./crypto.js";
3
3
  import { parseDpopProof, createDpopProof } from "./dpop.js";
4
+ import { initSession } from "./session.js";
4
5
  import { resolveClient, validateRedirectUri, isLoopbackClient } from "./client.js";
5
6
  import { discoverAuthServer, resolveHandle } from "./discovery.js";
6
7
  import { getServerKey, storeServerKey, storeOAuthRequest, getOAuthRequest, deleteOAuthRequest, storeAuthCode, consumeAuthCode, storeSession, checkAndStoreDpopJti, cleanupExpiredOAuth, storeRefreshToken, getRefreshToken, revokeRefreshToken, } from "./db.js";
@@ -57,6 +58,8 @@ export async function initOAuth(_config, plcUrl, relayUrl) {
57
58
  }
58
59
  serverPrivateKey = await importPrivateKey(serverPrivateJwk);
59
60
  serverJkt = await computeJwkThumbprint(serverPublicJwk);
61
+ // Initialize SSR session cookie signing
62
+ initSession(serverPrivateJwk, _config.cookieName);
60
63
  // Periodic cleanup of expired OAuth data
61
64
  setInterval(() => cleanupExpiredOAuth().catch(() => { }), 60_000);
62
65
  }
@@ -258,6 +261,93 @@ export function buildAuthorizeRedirect(config, request) {
258
261
  });
259
262
  return `${request.pds_auth_server}/oauth/authorize?${params}`;
260
263
  }
264
+ // --- Server-initiated login (no DPoP required from browser) ---
265
+ export async function serverLogin(config, handle) {
266
+ // Resolve handle to DID
267
+ let did = handle;
268
+ if (!did.startsWith('did:')) {
269
+ did = await resolveHandle(handle, _relayUrl);
270
+ }
271
+ // Discover PDS auth server
272
+ const discovery = await discoverAuthServer(did, _plcUrl);
273
+ const pdsAuthServer = discovery.authServerEndpoint;
274
+ // Create PKCE for PAR to PDS
275
+ const pdsCodeVerifier = randomToken();
276
+ const pdsCodeChallenge = base64UrlEncode(await sha256(pdsCodeVerifier));
277
+ const pdsState = randomToken();
278
+ // PAR to the PDS
279
+ const parEndpoint = discovery.authServerMetadata.pushed_authorization_request_endpoint || `${pdsAuthServer}/oauth/par`;
280
+ const serverDpopProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', parEndpoint);
281
+ const scope = config.scopes?.join(' ') || 'atproto transition:generic';
282
+ const pdsParBody = new URLSearchParams({
283
+ client_id: pdsClientId(config.issuer, config),
284
+ redirect_uri: pdsRedirectUri(config.issuer),
285
+ response_type: 'code',
286
+ code_challenge: pdsCodeChallenge,
287
+ code_challenge_method: 'S256',
288
+ scope,
289
+ login_hint: handle,
290
+ state: pdsState,
291
+ });
292
+ let pdsRequestUri;
293
+ const pdsParRes = await fetch(parEndpoint, {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', DPoP: serverDpopProof },
296
+ body: pdsParBody.toString(),
297
+ });
298
+ if (!pdsParRes.ok) {
299
+ const errBody = await pdsParRes.json().catch(() => ({}));
300
+ if (errBody.error === 'use_dpop_nonce') {
301
+ const nonce = pdsParRes.headers.get('DPoP-Nonce');
302
+ if (nonce) {
303
+ const retryProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', parEndpoint, undefined, nonce);
304
+ const retryRes = await fetch(parEndpoint, {
305
+ method: 'POST',
306
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', DPoP: retryProof },
307
+ body: pdsParBody.toString(),
308
+ });
309
+ if (!retryRes.ok) {
310
+ const retryErr = await retryRes.json().catch(() => ({}));
311
+ throw new Error(`PDS PAR failed: ${retryRes.status} ${retryErr.error_description || retryErr.error || ''}`);
312
+ }
313
+ const retryData = await retryRes.json();
314
+ pdsRequestUri = retryData.request_uri;
315
+ }
316
+ }
317
+ else {
318
+ throw new Error(`PDS PAR failed: ${pdsParRes.status} ${errBody.error_description || errBody.error || ''}`);
319
+ }
320
+ }
321
+ else {
322
+ const pdsParData = await pdsParRes.json();
323
+ pdsRequestUri = pdsParData.request_uri;
324
+ }
325
+ // Store the request so the callback can find it
326
+ const requestUri = `urn:ietf:params:oauth:request_uri:${randomToken()}`;
327
+ const expiresAt = Math.floor(Date.now() / 1000) + 600;
328
+ await storeOAuthRequest(requestUri, {
329
+ clientId: pdsClientId(config.issuer, config),
330
+ redirectUri: '/',
331
+ scope,
332
+ state: pdsState,
333
+ codeChallenge: '',
334
+ codeChallengeMethod: 'S256',
335
+ dpopJkt: serverJkt,
336
+ pdsRequestUri,
337
+ pdsAuthServer,
338
+ pdsCodeVerifier,
339
+ pdsState,
340
+ did,
341
+ loginHint: handle,
342
+ expiresAt,
343
+ });
344
+ // Build redirect URL to PDS
345
+ const params = new URLSearchParams({
346
+ request_uri: pdsRequestUri,
347
+ client_id: pdsClientId(config.issuer, config),
348
+ });
349
+ return `${pdsAuthServer}/oauth/authorize?${params}`;
350
+ }
261
351
  // --- OAuth Callback (PDS redirects here) ---
262
352
  export async function handleCallback(config, code, state, iss) {
263
353
  // Find the matching OAuth request by pds_state (unique per PAR)
@@ -356,7 +446,7 @@ export async function handleCallback(config, code, state, iss) {
356
446
  if (request.state)
357
447
  params.set('state', request.state);
358
448
  const clientRedirectUri = `${request.redirect_uri}?${params}`;
359
- return { requestUri: request.request_uri, clientRedirectUri, clientState: request.state };
449
+ return { requestUri: request.request_uri, clientRedirectUri, clientState: request.state, did };
360
450
  }
361
451
  // --- Token Endpoint ---
362
452
  export async function handleToken(config, body, dpopHeader, requestUrl) {
@@ -0,0 +1,9 @@
1
+ export declare function getSessionCookieName(): string;
2
+ export declare function initSession(privateJwk: JsonWebKey, cookieName?: string): void;
3
+ export declare function createSessionCookie(did: string): Promise<string>;
4
+ export declare function sessionCookieHeader(value: string, secure: boolean): string;
5
+ export declare function clearSessionCookieHeader(): string;
6
+ export declare function parseSessionCookie(request: Request): Promise<{
7
+ did: string;
8
+ } | null>;
9
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/oauth/session.ts"],"names":[],"mappings":"AASA,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,WAAW,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAG7E;AAYD,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMtE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAU1E;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoB1F"}