botnote 0.1.1

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 (79) hide show
  1. package/README.md +277 -0
  2. package/dist/bin.d.ts +2 -0
  3. package/dist/bin.js +320 -0
  4. package/dist/bin.js.map +1 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +61 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/db/client.d.ts +10 -0
  9. package/dist/db/client.js +16 -0
  10. package/dist/db/client.js.map +1 -0
  11. package/dist/db/migrate.d.ts +1 -0
  12. package/dist/db/migrate.js +57 -0
  13. package/dist/db/migrate.js.map +1 -0
  14. package/dist/db/migrations/0001_init.sql +89 -0
  15. package/dist/db/migrations/0002_tasks_due_at.sql +16 -0
  16. package/dist/db/migrations/0003_task_priority_and_sequence.sql +45 -0
  17. package/dist/db/migrations/0004_pinned.sql +8 -0
  18. package/dist/db/migrations/0005_nullable_title_and_parent.sql +5 -0
  19. package/dist/db/migrations/0006_project_color_icon.sql +3 -0
  20. package/dist/db/migrations/0007_tokens.sql +14 -0
  21. package/dist/db/migrations/0008_simplify_kinds_drop_actors.sql +21 -0
  22. package/dist/db/migrations/0009_sessions.sql +14 -0
  23. package/dist/db/migrations/0010_completed_at.sql +17 -0
  24. package/dist/db/migrations/0011_completed_at_backfill_fix.sql +14 -0
  25. package/dist/db/migrations/0012_completed_at_backfill_due.sql +16 -0
  26. package/dist/db/migrations/0013_due_at_noon_utc.sql +13 -0
  27. package/dist/db/schema.d.ts +811 -0
  28. package/dist/db/schema.js +112 -0
  29. package/dist/db/schema.js.map +1 -0
  30. package/dist/mcp/http-client.d.ts +154 -0
  31. package/dist/mcp/http-client.js +113 -0
  32. package/dist/mcp/http-client.js.map +1 -0
  33. package/dist/mcp/server.d.ts +7 -0
  34. package/dist/mcp/server.js +492 -0
  35. package/dist/mcp/server.js.map +1 -0
  36. package/dist/rest/routes.d.ts +3 -0
  37. package/dist/rest/routes.js +320 -0
  38. package/dist/rest/routes.js.map +1 -0
  39. package/dist/rest/server.d.ts +12 -0
  40. package/dist/rest/server.js +282 -0
  41. package/dist/rest/server.js.map +1 -0
  42. package/dist/service/embedding.d.ts +25 -0
  43. package/dist/service/embedding.js +112 -0
  44. package/dist/service/embedding.js.map +1 -0
  45. package/dist/service/entities.d.ts +22 -0
  46. package/dist/service/entities.js +174 -0
  47. package/dist/service/entities.js.map +1 -0
  48. package/dist/service/index.d.ts +8 -0
  49. package/dist/service/index.js +8 -0
  50. package/dist/service/index.js.map +1 -0
  51. package/dist/service/opening_brief.d.ts +14 -0
  52. package/dist/service/opening_brief.js +104 -0
  53. package/dist/service/opening_brief.js.map +1 -0
  54. package/dist/service/projects.d.ts +8 -0
  55. package/dist/service/projects.js +36 -0
  56. package/dist/service/projects.js.map +1 -0
  57. package/dist/service/search.d.ts +15 -0
  58. package/dist/service/search.js +101 -0
  59. package/dist/service/search.js.map +1 -0
  60. package/dist/service/sessions.d.ts +25 -0
  61. package/dist/service/sessions.js +89 -0
  62. package/dist/service/sessions.js.map +1 -0
  63. package/dist/service/tasks.d.ts +23 -0
  64. package/dist/service/tasks.js +130 -0
  65. package/dist/service/tasks.js.map +1 -0
  66. package/dist/service/tokens.d.ts +18 -0
  67. package/dist/service/tokens.js +41 -0
  68. package/dist/service/tokens.js.map +1 -0
  69. package/dist/service/types.d.ts +294 -0
  70. package/dist/service/types.js +132 -0
  71. package/dist/service/types.js.map +1 -0
  72. package/dist/version.d.ts +1 -0
  73. package/dist/version.js +13 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +73 -0
  76. package/web/dist/assets/index-Bddg7h_x.css +1 -0
  77. package/web/dist/assets/index-DYW6stVx.js +542 -0
  78. package/web/dist/favicon.svg +13 -0
  79. package/web/dist/index.html +17 -0
@@ -0,0 +1,101 @@
1
+ import { inArray, sql } from "drizzle-orm";
2
+ import { entities as entitiesTable } from "../db/schema.js";
3
+ const RRF_K = 60;
4
+ const KIND_WEIGHTS = {
5
+ decision: 0.3,
6
+ memory: 0.1,
7
+ task: 0.05,
8
+ comment: -0.1,
9
+ log: -0.2
10
+ };
11
+ function rrfScore(rank) {
12
+ if (rank == null)
13
+ return 0;
14
+ return 1 / (RRF_K + rank);
15
+ }
16
+ export async function search(db, input, opts = {}) {
17
+ const projectFilter = input.projectId
18
+ ? sql `AND project_id = ${input.projectId}::uuid`
19
+ : sql ``;
20
+ const kindFilter = input.kind ? sql `AND kind = ${input.kind}` : sql ``;
21
+ const fetchN = Math.max(input.limit * 4, 40);
22
+ const bm25Rows = (await db.execute(sql `
23
+ SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(body_tsv, q) DESC) AS rank_pos
24
+ FROM entities, websearch_to_tsquery('simple', ${input.query}) q
25
+ WHERE body_tsv @@ q ${projectFilter} ${kindFilter}
26
+ ORDER BY ts_rank_cd(body_tsv, q) DESC
27
+ LIMIT ${fetchN}
28
+ `)).rows;
29
+ const ilikeFallback = bm25Rows.length === 0
30
+ ? (await db.execute(sql `
31
+ SELECT id,
32
+ ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rank_pos
33
+ FROM entities
34
+ WHERE (title ILIKE ${`%${input.query}%`} OR body ILIKE ${`%${input.query}%`})
35
+ ${projectFilter} ${kindFilter}
36
+ LIMIT ${fetchN}
37
+ `)).rows
38
+ : [];
39
+ let vecRows = [];
40
+ if (opts.queryEmbedding && opts.queryEmbedding.length > 0) {
41
+ const vecLiteral = `[${opts.queryEmbedding.join(",")}]`;
42
+ vecRows = (await db.execute(sql `
43
+ SELECT id, ROW_NUMBER() OVER (ORDER BY body_vec <=> ${vecLiteral}::vector) AS rank_pos
44
+ FROM entities
45
+ WHERE body_vec IS NOT NULL ${projectFilter} ${kindFilter}
46
+ ORDER BY body_vec <=> ${vecLiteral}::vector
47
+ LIMIT ${fetchN}
48
+ `)).rows;
49
+ }
50
+ const timeRows = (await db.execute(sql `
51
+ SELECT id, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rank_pos
52
+ FROM entities
53
+ WHERE 1=1 ${projectFilter} ${kindFilter}
54
+ ORDER BY created_at DESC
55
+ LIMIT ${fetchN}
56
+ `)).rows;
57
+ const bm25Map = new Map();
58
+ for (const r of bm25Rows.length > 0 ? bm25Rows : ilikeFallback) {
59
+ bm25Map.set(r.id, Number(r.rank_pos));
60
+ }
61
+ const vecMap = new Map();
62
+ for (const r of vecRows)
63
+ vecMap.set(r.id, Number(r.rank_pos));
64
+ const timeMap = new Map();
65
+ for (const r of timeRows)
66
+ timeMap.set(r.id, Number(r.rank_pos));
67
+ const ids = new Set([...bm25Map.keys(), ...vecMap.keys()]);
68
+ if (ids.size === 0)
69
+ return [];
70
+ const idArr = [...ids];
71
+ const entitiesRows = await db
72
+ .select()
73
+ .from(entitiesTable)
74
+ .where(inArray(entitiesTable.id, idArr));
75
+ const entityMap = new Map();
76
+ for (const r of entitiesRows)
77
+ entityMap.set(r.id, r);
78
+ const hits = [];
79
+ for (const id of idArr) {
80
+ const entity = entityMap.get(id);
81
+ if (!entity)
82
+ continue;
83
+ const bm25 = rrfScore(bm25Map.get(id));
84
+ const cosine = rrfScore(vecMap.get(id));
85
+ const timeDecay = rrfScore(timeMap.get(id)) * 0.3;
86
+ const kindWeight = KIND_WEIGHTS[entity.kind] ?? 0;
87
+ const score = bm25 + cosine + timeDecay + kindWeight * 0.01;
88
+ hits.push({
89
+ entity,
90
+ score,
91
+ components: {
92
+ bm25: bm25 || undefined,
93
+ cosine: cosine || undefined,
94
+ timeDecay: timeDecay || undefined
95
+ }
96
+ });
97
+ }
98
+ hits.sort((a, b) => b.score - a.score);
99
+ return hits.slice(0, input.limit);
100
+ }
101
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/service/search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { Database } from "../db/client.js";
3
+ import { type Session } from "../db/schema.js";
4
+ /** Verify a candidate master password against BOTNOTE_PASSWORD env. Returns
5
+ * false (not throws) when the env is unset, so callers can degrade safely. */
6
+ export declare function verifyMasterPassword(candidate: string): boolean;
7
+ export interface CreatedSession {
8
+ session: Session;
9
+ plaintext: string;
10
+ }
11
+ export declare function createSession(db: Database["db"], opts?: {
12
+ userAgent?: string | null;
13
+ ttlDays?: number;
14
+ }): Promise<CreatedSession>;
15
+ /** Look up a session by cookie plaintext. Returns the row if it exists and
16
+ * has not expired; otherwise null. Bumps last_used_at on hit. */
17
+ export declare function consumeSession(db: Database["db"], plaintext: string): Promise<Session | null>;
18
+ /** Revoke a single session by cookie plaintext. No-op when the cookie does
19
+ * not match any row. */
20
+ export declare function revokeSession(db: Database["db"], plaintext: string): Promise<boolean>;
21
+ /** Periodic cleanup — called on daemon start. Deletes expired rows so the
22
+ * table doesn't bloat over months. */
23
+ export declare function pruneExpiredSessions(db: Database["db"]): Promise<number>;
24
+ /** Drizzle helper to noop a linter complaint when sql is unused. */
25
+ export declare const _sql: typeof sql;
@@ -0,0 +1,89 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { and, eq, gt, lt, sql } from "drizzle-orm";
3
+ import { sessions } from "../db/schema.js";
4
+ const DEFAULT_TTL_DAYS = 30;
5
+ function hashToken(plaintext) {
6
+ return createHash("sha256").update(plaintext).digest("hex");
7
+ }
8
+ /** Constant-time equality on UTF-8 strings of arbitrary length. */
9
+ function constantTimeEqual(a, b) {
10
+ const ab = Buffer.from(a, "utf8");
11
+ const bb = Buffer.from(b, "utf8");
12
+ // Pad to equal length so timingSafeEqual doesn't throw. We compare lengths
13
+ // separately — if lengths differ, the result is false. Both branches still
14
+ // do a fixed-time compare of the same number of bytes.
15
+ const len = Math.max(ab.length, bb.length);
16
+ const a2 = Buffer.alloc(len);
17
+ const b2 = Buffer.alloc(len);
18
+ ab.copy(a2);
19
+ bb.copy(b2);
20
+ const same = timingSafeEqual(a2, b2);
21
+ return same && ab.length === bb.length;
22
+ }
23
+ /** Verify a candidate master password against BOTNOTE_PASSWORD env. Returns
24
+ * false (not throws) when the env is unset, so callers can degrade safely. */
25
+ export function verifyMasterPassword(candidate) {
26
+ const expected = process.env.BOTNOTE_PASSWORD;
27
+ if (!expected)
28
+ return false;
29
+ if (typeof candidate !== "string" || candidate.length === 0)
30
+ return false;
31
+ return constantTimeEqual(candidate, expected);
32
+ }
33
+ export async function createSession(db, opts = {}) {
34
+ const ttlDays = opts.ttlDays ?? DEFAULT_TTL_DAYS;
35
+ const plaintext = randomBytes(32).toString("hex");
36
+ const tokenHash = hashToken(plaintext);
37
+ const expiresAt = new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000);
38
+ const [row] = await db
39
+ .insert(sessions)
40
+ .values({
41
+ tokenHash,
42
+ expiresAt,
43
+ userAgent: opts.userAgent ?? null
44
+ })
45
+ .returning();
46
+ return { session: row, plaintext };
47
+ }
48
+ /** Look up a session by cookie plaintext. Returns the row if it exists and
49
+ * has not expired; otherwise null. Bumps last_used_at on hit. */
50
+ export async function consumeSession(db, plaintext) {
51
+ if (typeof plaintext !== "string" || plaintext.length === 0)
52
+ return null;
53
+ const tokenHash = hashToken(plaintext);
54
+ const now = new Date();
55
+ const [row] = await db
56
+ .select()
57
+ .from(sessions)
58
+ .where(and(eq(sessions.tokenHash, tokenHash), gt(sessions.expiresAt, now)))
59
+ .limit(1);
60
+ if (!row)
61
+ return null;
62
+ await db
63
+ .update(sessions)
64
+ .set({ lastUsedAt: now })
65
+ .where(eq(sessions.id, row.id));
66
+ return row;
67
+ }
68
+ /** Revoke a single session by cookie plaintext. No-op when the cookie does
69
+ * not match any row. */
70
+ export async function revokeSession(db, plaintext) {
71
+ if (typeof plaintext !== "string" || plaintext.length === 0)
72
+ return false;
73
+ const tokenHash = hashToken(plaintext);
74
+ const result = await db.delete(sessions).where(eq(sessions.tokenHash, tokenHash));
75
+ // node-postgres returns rowCount on the result wrapper drizzle exposes via
76
+ // .rowCount; fall back to true when shape is opaque.
77
+ const r = result;
78
+ return r.rowCount === undefined ? true : r.rowCount > 0;
79
+ }
80
+ /** Periodic cleanup — called on daemon start. Deletes expired rows so the
81
+ * table doesn't bloat over months. */
82
+ export async function pruneExpiredSessions(db) {
83
+ const result = await db.delete(sessions).where(lt(sessions.expiresAt, new Date()));
84
+ const r = result;
85
+ return r.rowCount ?? 0;
86
+ }
87
+ /** Drizzle helper to noop a linter complaint when sql is unused. */
88
+ export const _sql = sql;
89
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/service/sessions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { Database } from "../db/client.js";
2
+ import { type Entity } from "../db/schema.js";
3
+ import type { TasksRangeInput } from "./types.js";
4
+ export interface TasksRangeResult {
5
+ scheduled: Entity[];
6
+ overdue: Entity[];
7
+ backlog: Entity[];
8
+ }
9
+ /**
10
+ * Tasks in a date range, split into scheduled / overdue / backlog buckets.
11
+ *
12
+ * Each task has a "display date" that decides where it lands on the calendar:
13
+ * - status='done' → completedAt (when actually finished)
14
+ * - status='in_progress' → today (rolling — surfaces active work)
15
+ * - everything else → dueAt (when it's supposed to happen)
16
+ *
17
+ * `scheduled` returns any task whose display date falls inside [from, to).
18
+ * Done tasks without a completedAt (legacy / weird state) fall back to dueAt.
19
+ * `overdue` keeps the original meaning — past-due open work — and explicitly
20
+ * excludes done + in_progress so they don't double-surface (done shows on its
21
+ * completion day; in_progress already shows on today).
22
+ */
23
+ export declare function tasksRange(db: Database["db"], input: TasksRangeInput): Promise<TasksRangeResult>;
@@ -0,0 +1,130 @@
1
+ import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, lt, ne, or } from "drizzle-orm";
2
+ import { entities } from "../db/schema.js";
3
+ /**
4
+ * Tasks in a date range, split into scheduled / overdue / backlog buckets.
5
+ *
6
+ * Each task has a "display date" that decides where it lands on the calendar:
7
+ * - status='done' → completedAt (when actually finished)
8
+ * - status='in_progress' → today (rolling — surfaces active work)
9
+ * - everything else → dueAt (when it's supposed to happen)
10
+ *
11
+ * `scheduled` returns any task whose display date falls inside [from, to).
12
+ * Done tasks without a completedAt (legacy / weird state) fall back to dueAt.
13
+ * `overdue` keeps the original meaning — past-due open work — and explicitly
14
+ * excludes done + in_progress so they don't double-surface (done shows on its
15
+ * completion day; in_progress already shows on today).
16
+ */
17
+ export async function tasksRange(db, input) {
18
+ const now = new Date();
19
+ const projectFilter = input.projectIds?.length
20
+ ? inArray(entities.projectId, input.projectIds)
21
+ : undefined;
22
+ // Base filter applied to every bucket: only tasks, optional project filter,
23
+ // and the includeDone toggle (which gates done + archived together — both
24
+ // are terminal states that the day cell would otherwise mix into open work).
25
+ const baseConds = [eq(entities.kind, "task")];
26
+ if (!input.includeDone) {
27
+ baseConds.push(or(ne(entities.status, "done"), isNull(entities.status)));
28
+ baseConds.push(or(ne(entities.status, "archived"), isNull(entities.status)));
29
+ }
30
+ if (projectFilter)
31
+ baseConds.push(projectFilter);
32
+ // Three independent passes, unioned in JS. Drizzle's `or` keeps the SQL
33
+ // clear and the partial completed_at index keeps the done-pass cheap.
34
+ const inRange = (col) => {
35
+ const conds = [];
36
+ if (input.from)
37
+ conds.push(gte(col, input.from));
38
+ if (input.to)
39
+ conds.push(lt(col, input.to));
40
+ return conds;
41
+ };
42
+ // (a) Non-done, non-in_progress tasks scheduled by dueAt.
43
+ const dueByDueConds = [
44
+ ...baseConds,
45
+ isNotNull(entities.dueAt),
46
+ or(ne(entities.status, "done"), isNull(entities.status)),
47
+ or(ne(entities.status, "in_progress"), isNull(entities.status)),
48
+ ...inRange(entities.dueAt)
49
+ ];
50
+ const dueByDue = await db
51
+ .select()
52
+ .from(entities)
53
+ .where(and(...dueByDueConds))
54
+ .orderBy(asc(entities.dueAt));
55
+ // (b) Done tasks rendered on completedAt. Only runs when the caller wants
56
+ // done items included — otherwise it returns nothing and the partial index
57
+ // is never touched.
58
+ let doneByCompleted = [];
59
+ if (input.includeDone) {
60
+ const doneConds = [
61
+ eq(entities.kind, "task"),
62
+ eq(entities.status, "done"),
63
+ isNotNull(entities.completedAt),
64
+ ...inRange(entities.completedAt)
65
+ ];
66
+ if (projectFilter)
67
+ doneConds.push(projectFilter);
68
+ doneByCompleted = await db
69
+ .select()
70
+ .from(entities)
71
+ .where(and(...doneConds))
72
+ .orderBy(asc(entities.completedAt));
73
+ }
74
+ // (c) in_progress tasks → today. Only relevant when "now" falls inside the
75
+ // requested window; otherwise the day cell would never render them anyway.
76
+ let inProgressToday = [];
77
+ const todayInRange = (!input.from || now >= new Date(input.from)) &&
78
+ (!input.to || now < new Date(input.to));
79
+ if (todayInRange) {
80
+ const ipConds = [eq(entities.kind, "task"), eq(entities.status, "in_progress")];
81
+ if (projectFilter)
82
+ ipConds.push(projectFilter);
83
+ inProgressToday = await db
84
+ .select()
85
+ .from(entities)
86
+ .where(and(...ipConds))
87
+ .orderBy(asc(entities.dueAt));
88
+ }
89
+ // Dedup by id — a status='done' task with a completedAt also matched (a) if
90
+ // its dueAt landed in range; (b) wins because it's the canonical display.
91
+ const scheduledMap = new Map();
92
+ for (const e of dueByDue)
93
+ scheduledMap.set(e.id, e);
94
+ for (const e of doneByCompleted)
95
+ scheduledMap.set(e.id, e);
96
+ for (const e of inProgressToday)
97
+ scheduledMap.set(e.id, e);
98
+ const scheduled = Array.from(scheduledMap.values());
99
+ // Overdue = past-due work that still needs attention. Done + in_progress
100
+ // tasks are intentionally excluded: done is shown on its completion day,
101
+ // in_progress is shown on today, neither belongs in an "overdue" alert.
102
+ const overdueConds = [
103
+ eq(entities.kind, "task"),
104
+ isNotNull(entities.dueAt),
105
+ lt(entities.dueAt, now),
106
+ or(ne(entities.status, "done"), isNull(entities.status)),
107
+ or(ne(entities.status, "in_progress"), isNull(entities.status)),
108
+ or(ne(entities.status, "archived"), isNull(entities.status))
109
+ ];
110
+ if (projectFilter)
111
+ overdueConds.push(projectFilter);
112
+ const overdue = !input.includeDone
113
+ ? await db
114
+ .select()
115
+ .from(entities)
116
+ .where(and(...overdueConds))
117
+ .orderBy(asc(entities.dueAt))
118
+ : [];
119
+ let backlog = [];
120
+ if (input.includeBacklog) {
121
+ backlog = await db
122
+ .select()
123
+ .from(entities)
124
+ .where(and(...baseConds, isNull(entities.dueAt)))
125
+ .orderBy(desc(entities.createdAt))
126
+ .limit(200);
127
+ }
128
+ return { scheduled, overdue, backlog };
129
+ }
130
+ //# sourceMappingURL=tasks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tasks.js","sourceRoot":"","sources":["../../src/service/tasks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Database } from "../db/client.js";
2
+ import { type Token } from "../db/schema.js";
3
+ export interface CreatedToken {
4
+ token: Token;
5
+ /** The plaintext token. Only available immediately after creation. */
6
+ plaintext: string;
7
+ }
8
+ export declare function createToken(db: Database["db"], input: {
9
+ name: string;
10
+ }): Promise<CreatedToken>;
11
+ export declare function listTokens(db: Database["db"]): Promise<Token[]>;
12
+ export declare function revokeToken(db: Database["db"], id: string): Promise<boolean>;
13
+ /**
14
+ * Look up a token by its plaintext. Returns null if not found.
15
+ * Bumps last_used_at on success. Not currently called by middleware; reserved
16
+ * for when auth enforcement is enabled.
17
+ */
18
+ export declare function consumeToken(db: Database["db"], plaintext: string): Promise<Token | null>;
@@ -0,0 +1,41 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { desc, eq } from "drizzle-orm";
3
+ import { tokens } from "../db/schema.js";
4
+ const TOKEN_PREFIX = "bn_";
5
+ function sha256(input) {
6
+ return createHash("sha256").update(input).digest("hex");
7
+ }
8
+ export async function createToken(db, input) {
9
+ const plaintext = `${TOKEN_PREFIX}${randomBytes(24).toString("hex")}`;
10
+ const tokenHash = sha256(plaintext);
11
+ const prefix = plaintext.slice(0, 11); // bn_ + first 8 hex
12
+ const [row] = await db
13
+ .insert(tokens)
14
+ .values({ name: input.name, tokenHash, prefix })
15
+ .returning();
16
+ if (!row)
17
+ throw new Error("token insert returned no row");
18
+ return { token: row, plaintext };
19
+ }
20
+ export async function listTokens(db) {
21
+ return db.select().from(tokens).orderBy(desc(tokens.createdAt));
22
+ }
23
+ export async function revokeToken(db, id) {
24
+ const res = await db.delete(tokens).where(eq(tokens.id, id)).returning({ id: tokens.id });
25
+ return res.length > 0;
26
+ }
27
+ /**
28
+ * Look up a token by its plaintext. Returns null if not found.
29
+ * Bumps last_used_at on success. Not currently called by middleware; reserved
30
+ * for when auth enforcement is enabled.
31
+ */
32
+ export async function consumeToken(db, plaintext) {
33
+ const tokenHash = sha256(plaintext);
34
+ const rows = await db.select().from(tokens).where(eq(tokens.tokenHash, tokenHash)).limit(1);
35
+ const row = rows[0];
36
+ if (!row)
37
+ return null;
38
+ await db.update(tokens).set({ lastUsedAt: new Date() }).where(eq(tokens.id, row.id));
39
+ return row;
40
+ }
41
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/service/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,MAAM,EAAc,MAAM,iBAAiB,CAAC;AAErD,MAAM,YAAY,GAAG,KAAK,CAAC;AAE3B,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,EAAkB,EAClB,KAAuB;IAEvB,MAAM,SAAS,GAAG,GAAG,YAAY,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;IACtE,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,oBAAoB;IAC3D,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE;SACnB,MAAM,CAAC,MAAM,CAAC;SACd,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;SAC/C,SAAS,EAAE,CAAC;IACf,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAC1D,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,EAAkB;IACjD,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,EAAkB,EAAE,EAAU;IAC9D,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1F,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAkB,EAClB,SAAiB;IAEjB,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5F,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IACrF,OAAO,GAAG,CAAC;AACb,CAAC"}