@snagset/server 0.0.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 (105) hide show
  1. package/README.md +83 -0
  2. package/dist/app.js +453 -0
  3. package/dist/app.js.map +1 -0
  4. package/dist/bin.js +951 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/boot.js +321 -0
  7. package/dist/boot.js.map +1 -0
  8. package/dist/config.generated.js +116 -0
  9. package/dist/config.generated.js.map +1 -0
  10. package/dist/config.js +180 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/crypto.js +26 -0
  13. package/dist/crypto.js.map +1 -0
  14. package/dist/db/connect.js +49 -0
  15. package/dist/db/connect.js.map +1 -0
  16. package/dist/db/migrate.js +210 -0
  17. package/dist/db/migrate.js.map +1 -0
  18. package/dist/db/migrations/0001_init.sql +1060 -0
  19. package/dist/db/migrations/0002_maintenance_claim.sql +109 -0
  20. package/dist/db/migrations/0003_email_provider.sql +35 -0
  21. package/dist/db/migrations/0004_unsubscribe.sql +24 -0
  22. package/dist/db/rls.js +58 -0
  23. package/dist/db/rls.js.map +1 -0
  24. package/dist/jobs/index.js +171 -0
  25. package/dist/jobs/index.js.map +1 -0
  26. package/dist/jobs/reaper.js +182 -0
  27. package/dist/jobs/reaper.js.map +1 -0
  28. package/dist/keys.js +77 -0
  29. package/dist/keys.js.map +1 -0
  30. package/dist/mail/index.js +75 -0
  31. package/dist/mail/index.js.map +1 -0
  32. package/dist/mail/mailer.js +27 -0
  33. package/dist/mail/mailer.js.map +1 -0
  34. package/dist/mail/notify.js +405 -0
  35. package/dist/mail/notify.js.map +1 -0
  36. package/dist/mail/resend.js +95 -0
  37. package/dist/mail/resend.js.map +1 -0
  38. package/dist/mail/smtp.js +315 -0
  39. package/dist/mail/smtp.js.map +1 -0
  40. package/dist/mail/unsubscribe.js +134 -0
  41. package/dist/mail/unsubscribe.js.map +1 -0
  42. package/dist/projectCache.js +50 -0
  43. package/dist/projectCache.js.map +1 -0
  44. package/dist/projects.js +101 -0
  45. package/dist/projects.js.map +1 -0
  46. package/dist/routes/attachments.js +159 -0
  47. package/dist/routes/attachments.js.map +1 -0
  48. package/dist/routes/auth.js +55 -0
  49. package/dist/routes/auth.js.map +1 -0
  50. package/dist/routes/commentEdit.js +201 -0
  51. package/dist/routes/commentEdit.js.map +1 -0
  52. package/dist/routes/comments.js +313 -0
  53. package/dist/routes/comments.js.map +1 -0
  54. package/dist/routes/cors.js +174 -0
  55. package/dist/routes/cors.js.map +1 -0
  56. package/dist/routes/jobs.js +56 -0
  57. package/dist/routes/jobs.js.map +1 -0
  58. package/dist/routes/mcpAuth.js +187 -0
  59. package/dist/routes/mcpAuth.js.map +1 -0
  60. package/dist/routes/mcpToken.js +94 -0
  61. package/dist/routes/mcpToken.js.map +1 -0
  62. package/dist/routes/owner.js +288 -0
  63. package/dist/routes/owner.js.map +1 -0
  64. package/dist/routes/reviewLinks.js +169 -0
  65. package/dist/routes/reviewLinks.js.map +1 -0
  66. package/dist/routes/rounds.js +137 -0
  67. package/dist/routes/rounds.js.map +1 -0
  68. package/dist/routes/sessions.js +349 -0
  69. package/dist/routes/sessions.js.map +1 -0
  70. package/dist/routes/sites.js +192 -0
  71. package/dist/routes/sites.js.map +1 -0
  72. package/dist/routes/threads.js +1080 -0
  73. package/dist/routes/threads.js.map +1 -0
  74. package/dist/routes/widget.js +132 -0
  75. package/dist/routes/widget.js.map +1 -0
  76. package/dist/serverless.js +97 -0
  77. package/dist/serverless.js.map +1 -0
  78. package/dist/session.js +152 -0
  79. package/dist/session.js.map +1 -0
  80. package/dist/storage/bucket.js +116 -0
  81. package/dist/storage/bucket.js.map +1 -0
  82. package/dist/storage/settings.js +132 -0
  83. package/dist/storage/settings.js.map +1 -0
  84. package/dist/vendor/adapters/index.js +1 -0
  85. package/dist/vendor/adapters/outbound/fence.js +44 -0
  86. package/dist/vendor/adapters/outbound/sanitise.js +233 -0
  87. package/dist/vendor/types/index.js +4 -0
  88. package/dist/vendor/types/names.js +254 -0
  89. package/dist/vendor/widget/object.js +38 -0
  90. package/dist/vendor/widget/predicates.js +377 -0
  91. package/package.json +53 -0
  92. package/static/dashboard/assets/index-CaPfKhFP.css +1 -0
  93. package/static/dashboard/assets/index-DfTNjHCK.js +1 -0
  94. package/static/dashboard/index.html +14 -0
  95. package/static/widget/snag.anchor-ext.js +1 -0
  96. package/static/widget/snag.anchor-fuzzy.js +1 -0
  97. package/static/widget/snag.capture.js +4 -0
  98. package/static/widget/snag.crumbs.js +4 -0
  99. package/static/widget/snag.dock.js +5 -0
  100. package/static/widget/snag.js +5 -0
  101. package/static/widget/snag.panels.js +2 -0
  102. package/static/widget/snag.runtime.js +2 -0
  103. package/static/widget/snag.screenshot.js +1 -0
  104. package/static/widget/snag.text.js +2 -0
  105. package/vercel.json +6 -0
@@ -0,0 +1,187 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { CREDENTIAL_PREFIX } from "../vendor/types/names.js";
3
+ import { verifyMcpToken, touchMcpToken } from "./mcpToken.js";
4
+ /**
5
+ * The write budget — 07 §8.4, `mcpw:token`, 20 writes per 10 minutes
6
+ * *(calibration)*, and §8.6 mitigation 5 is why it exists: *"An agent injected
7
+ * into 'resolve every thread' stops on the 21st and must tell its user."*
8
+ *
9
+ * Counted out of `events` rather than a bucket in memory, and that is not
10
+ * laziness. A budget in process memory resets when the process restarts and
11
+ * does not exist at all across two instances — so on the serverless entry
12
+ * (`serverless.ts`) an attacker gets 20 writes *per warm instance*, which is
13
+ * not a budget. §8.6 mitigation 8 already requires every agent write to land in
14
+ * `events` with the token id; counting the rows that must exist anyway makes
15
+ * the budget exactly as durable as the audit trail.
16
+ */
17
+ export const WRITE_BUDGET = { writes: 20, windowMinutes: 10 };
18
+ export function mcpAuth(sql) {
19
+ return async (c, next) => {
20
+ const presented = /^Bearer (.+)$/i.exec(c.req.header("Authorization") ?? "")?.[1];
21
+ // Not an MCP token — a reviewer bearer, or nothing. Costs one string
22
+ // comparison and no query, which is what keeps this mountable app-wide.
23
+ if (!presented?.startsWith(CREDENTIAL_PREFIX.mcp))
24
+ return next();
25
+ const claims = await verifyMcpToken(sql, presented);
26
+ if (!claims) {
27
+ return c.json({
28
+ error: {
29
+ code: "unauthorized",
30
+ // One sentence for unknown, revoked and expired alike. Which of
31
+ // the three it was is exactly what a probe is trying to learn, and
32
+ // the operator can see it with `snagset mcp-token list`.
33
+ message: "That MCP token is not valid. It may have been revoked or have expired — " +
34
+ "mint a new one with `snagset mcp-token create`.",
35
+ requestId: c.get("requestId"),
36
+ },
37
+ }, 401);
38
+ }
39
+ c.set("mcp", claims);
40
+ /**
41
+ * The audit timestamp, and the address as a per-token salted hash.
42
+ *
43
+ * `void` on purpose — F29 reads `last_used_at`, but a failure to record it
44
+ * must never fail a request that otherwise succeeded.
45
+ *
46
+ * The salt is the token's OWN hash. 00 §14.2b puts the IP salt on
47
+ * `projects`, which works for a project-scoped token and has nothing to
48
+ * offer an org-wide one — and an org-wide token is what the CLI mints, so
49
+ * "use the project salt" would mean recording nothing in the common case
50
+ * and leaving F29 blind. `token_hash` is per-row, already stored, cannot
51
+ * predate the row it is on, and is not derivable by whoever is being
52
+ * hashed. The raw address is still never stored (08 §4.7).
53
+ */
54
+ void touchMcpToken(sql, claims.tokenId, hashAddress(c, claims)).catch(() => { });
55
+ return next();
56
+ };
57
+ }
58
+ function hashAddress(c, claims) {
59
+ const raw = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip")?.trim() || "";
60
+ if (!raw)
61
+ return null;
62
+ return createHash("sha256").update(raw, "utf8").update(claims.tokenId, "utf8").digest();
63
+ }
64
+ /**
65
+ * Does this token carry the scope, for this org?
66
+ *
67
+ * BOTH halves matter and the org half is the one that is easy to forget: a
68
+ * valid token for another org is a valid token, and every route here resolves
69
+ * its project independently of the credential. This is the same check
70
+ * `asOwner` makes (`owner.orgId === p.org_id`), for the same reason.
71
+ */
72
+ export function mcpAllows(claims, orgId, scope) {
73
+ return !!claims && claims.orgId === orgId && claims.scopes.includes(scope);
74
+ }
75
+ /**
76
+ * The refusal, with the scope named — 05's registered `mcp_scope_denied`.
77
+ *
78
+ * The message says which scope and how to fix it, because the reader is an
79
+ * agent relaying to a developer who minted the token minutes ago and has no
80
+ * idea the default is read-only. "Forbidden" would send them to the dashboard
81
+ * looking for a permission that is not there.
82
+ */
83
+ export function scopeDenied(c, scope) {
84
+ return c.json({
85
+ error: {
86
+ code: "mcp_scope_denied",
87
+ message: `This MCP token does not carry the "${scope}" scope. Mint one that does with ` +
88
+ `\`snagset mcp-token create <name> ${scope}\` — the default token is read-only on purpose.`,
89
+ requestId: c.get("requestId"),
90
+ },
91
+ }, 403);
92
+ }
93
+ /**
94
+ * Spend one write from the budget, or refuse.
95
+ *
96
+ * Returns null when the write may proceed. Deliberately checked BEFORE the
97
+ * write rather than after: a budget enforced by refusing the 21st response
98
+ * while having already made the 21st change is not a budget.
99
+ */
100
+ export async function checkWriteBudget(sql, tokenId) {
101
+ const [row] = await sql `
102
+ SELECT count(*)::int AS n FROM events
103
+ WHERE actor_kind = 'integration'
104
+ AND payload->>'mcpTokenId' = ${tokenId}
105
+ AND created_at > now() - ${`${WRITE_BUDGET.windowMinutes} minutes`}::interval
106
+ `;
107
+ const spent = row?.n ?? 0;
108
+ return spent >= WRITE_BUDGET.writes ? spent : null;
109
+ }
110
+ /** 07 §8.4's stop signal, as an HTTP response. */
111
+ export function budgetExhausted(c) {
112
+ return c.json({
113
+ error: {
114
+ code: "rate_limited",
115
+ message: `This agent has made ${WRITE_BUDGET.writes} writes in the last ` +
116
+ `${WRITE_BUDGET.windowMinutes} minutes, which is the limit. Stop and tell your user ` +
117
+ `what you were doing — do not retry.`,
118
+ requestId: c.get("requestId"),
119
+ },
120
+ }, 429);
121
+ }
122
+ /**
123
+ * The audit row — 07 §8.6 mitigation 8, *"so 'what did the agent do last
124
+ * Tuesday' is one query"*.
125
+ *
126
+ * Takes a transaction, and every caller passes the one doing the write. That
127
+ * is what makes the budget above sound: the audit row and the change it
128
+ * records commit together, so there is no window in which a write has happened
129
+ * and is not yet counted.
130
+ */
131
+ export async function recordAgentWrite(tx, what) {
132
+ // `tx.json(…)`, NOT `JSON.stringify(…)::jsonb`.
133
+ //
134
+ // postgres.js JSON-encodes a string parameter bound to a jsonb column, so a
135
+ // stringified object arrives as a JSON *string scalar*: `jsonb_typeof` says
136
+ // `string`, and `payload->>'mcpTokenId'` is null. Which silently disabled the
137
+ // write budget — `checkWriteBudget` counted zero, forever, and an agent could
138
+ // write without limit. The audit rows were all there and all unqueryable.
139
+ //
140
+ // Missed by the unit test because that test SEEDED the rows itself, correctly,
141
+ // and then asserted the refusal. It proved the query, and the query was never
142
+ // the broken half. Caught by making 21 real writes through a published tarball
143
+ // and watching all 21 succeed.
144
+ await tx `
145
+ INSERT INTO events (id, org_id, project_id, type, actor_kind, actor_id, subject_type, subject_id, payload)
146
+ VALUES (${randomUUID()}, ${what.orgId}, ${what.projectId}, ${what.type}, 'integration',
147
+ ${what.reviewerId}, ${what.subjectType}, ${what.subjectId},
148
+ ${tx.json({ mcpTokenId: what.tokenId })})
149
+ `;
150
+ }
151
+ /**
152
+ * Find-or-create the agent's reviewer row — 07 §8.2 *Authorship*, and 05 §9.6's
153
+ * representation: `kind = 'guest'` with `is_agent = true` and
154
+ * `agent_mcp_token_id` set.
155
+ *
156
+ * **Never `verified`.** Draft 2 of 07 posted agent comments as `kind:
157
+ * 'verified'` with an `(agent)` name suffix and that is deleted: `verified`
158
+ * means the host application's server vouched for a named human, and it
159
+ * carries a moderation bypass. Laundering a machine credential into it hands
160
+ * an agent — possibly one acting on injected instructions — the highest trust
161
+ * tier in the system on the strength of a name convention.
162
+ *
163
+ * `holder_secret_hash` is 32 random bytes whose plaintext is DISCARDED here and
164
+ * never existed anywhere else. 0001's CHECK requires a non-verified row to be a
165
+ * credential holder, a studio member or a placeholder, and an agent is the
166
+ * first of those — its credential is the MCP token. Hashing something guessable
167
+ * instead (the token id, say) would make this row impersonable through
168
+ * `/sessions`' guest path by anyone who learned a uuid; hashing a secret nobody
169
+ * holds means that door is not merely locked, there is no key.
170
+ */
171
+ export async function agentReviewer(tx, what) {
172
+ const [existing] = await tx `
173
+ SELECT id FROM reviewers
174
+ WHERE project_id = ${what.projectId}::uuid AND agent_mcp_token_id = ${what.tokenId}::uuid
175
+ `;
176
+ if (existing)
177
+ return existing.id;
178
+ const id = randomUUID();
179
+ await tx `
180
+ INSERT INTO reviewers (id, org_id, project_id, kind, name, holder_secret_hash, is_agent, agent_mcp_token_id)
181
+ VALUES (${id}::uuid, ${what.orgId}::uuid, ${what.projectId}::uuid, 'guest',
182
+ ${what.tokenName}, ${createHash("sha256").update(randomBytes(32)).digest()},
183
+ true, ${what.tokenId}::uuid)
184
+ `;
185
+ return id;
186
+ }
187
+ //# sourceMappingURL=mcpAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpAuth.js","sourceRoot":"","sources":["../../src/routes/mcpAuth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { CREDENTIAL_PREFIX } from "../vendor/types/names.js";
3
+ /** 07 §8.2's vocabulary. This spec owns it; the server enforces it. */
4
+ export const MCP_SCOPES = [
5
+ "threads:read",
6
+ "context:read",
7
+ "comments:write",
8
+ "threads:write",
9
+ "source:map",
10
+ ];
11
+ /**
12
+ * What the dashboard issues by default: **read-only, plus local source
13
+ * mapping**.
14
+ *
15
+ * 07 §8.2: *"The default token issued by the dashboard carries `threads:read`
16
+ * and `source:map` only."* `context:read` is separate on purpose — breadcrumbs
17
+ * and capture context are the personal-data-bearing half, and an agent that
18
+ * only needs "what is broken and where" should not be handed a console log.
19
+ */
20
+ export const DEFAULT_SCOPES = ["threads:read", "source:map"];
21
+ const hash = (token) => createHash("sha256").update(token, "utf8").digest();
22
+ export async function mintMcpToken(sql, id, opts) {
23
+ const secret = randomBytes(32).toString("base64url");
24
+ const prefix = randomBytes(6).toString("base64url").slice(0, 8);
25
+ const token = `${CREDENTIAL_PREFIX.mcp}${prefix}_${secret}`;
26
+ const scopes = opts.scopes ?? DEFAULT_SCOPES;
27
+ for (const scope of scopes) {
28
+ if (!MCP_SCOPES.includes(scope)) {
29
+ throw new Error(`unknown MCP scope ${JSON.stringify(scope)}. Known: ${MCP_SCOPES.join(", ")}`);
30
+ }
31
+ }
32
+ await sql `
33
+ INSERT INTO mcp_tokens (id, org_id, project_id, name, prefix, token_hash, scopes, created_by, expires_at)
34
+ VALUES (${id}, ${opts.orgId}, ${opts.projectId ?? null}, ${opts.name}, ${prefix},
35
+ ${hash(token)}, ${sql.array(scopes)}, ${opts.createdBy ?? null},
36
+ ${opts.expiresAt ?? null})
37
+ `;
38
+ return { id, token, prefix };
39
+ }
40
+ /**
41
+ * Verify a presented token. Returns null for anything not currently valid —
42
+ * unknown, revoked, or expired — without saying which.
43
+ *
44
+ * The lookup is by HASH, so a token is found in one indexed comparison rather
45
+ * than by scanning and comparing; `timingSafeEqual` on the row that comes back
46
+ * closes the remaining timing channel.
47
+ */
48
+ export async function verifyMcpToken(sql, presented, now = new Date()) {
49
+ if (!presented.startsWith(CREDENTIAL_PREFIX.mcp))
50
+ return null;
51
+ const digest = hash(presented);
52
+ const [row] = await sql `
53
+ SELECT id, name, org_id, project_id, scopes, confirm_writes, allow_remote_writes,
54
+ token_hash, revoked_at, expires_at
55
+ FROM mcp_tokens
56
+ WHERE token_hash = ${digest}
57
+ LIMIT 1
58
+ `;
59
+ if (!row)
60
+ return null;
61
+ if (row.token_hash.length !== digest.length || !timingSafeEqual(row.token_hash, digest))
62
+ return null;
63
+ if (row.revoked_at)
64
+ return null;
65
+ if (row.expires_at && row.expires_at <= now)
66
+ return null;
67
+ return {
68
+ tokenId: row.id,
69
+ name: row.name,
70
+ orgId: row.org_id,
71
+ projectId: row.project_id,
72
+ scopes: row.scopes,
73
+ confirmWrites: row.confirm_writes,
74
+ allowRemoteWrites: row.allow_remote_writes,
75
+ };
76
+ }
77
+ /**
78
+ * Record use — `last_used_at`, and the address as a SALTED HASH.
79
+ *
80
+ * 07 F29 reads this column and 00 §14.2b put the salt in migration 0001 so no
81
+ * historical hash predates it. The raw address is never stored anywhere in this
82
+ * system (08 §4.7), and a token audit trail is not the place to make the first
83
+ * exception.
84
+ *
85
+ * Deliberately fire-and-forget at the call site: a failure to write an audit
86
+ * timestamp must not fail the request that succeeded.
87
+ */
88
+ export async function touchMcpToken(sql, tokenId, ipHash) {
89
+ await sql `
90
+ UPDATE mcp_tokens SET last_used_at = now(), last_used_ip = ${ipHash}
91
+ WHERE id = ${tokenId}
92
+ `;
93
+ }
94
+ //# sourceMappingURL=mcpToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpToken.js","sourceRoot":"","sources":["../../src/routes/mcpToken.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,288 @@
1
+ import { Hono } from "hono";
2
+ import { randomUUID, randomBytes, createHash, timingSafeEqual, scryptSync } from "node:crypto";
3
+ import { DASHBOARD_SESSION_COOKIE, DASHBOARD_CSRF_COOKIE, HTTP_HEADER } from "../vendor/types/names.js";
4
+ /** 05 §4.7: 30-day absolute, 7-day idle, rolling refresh on use. */
5
+ const ABSOLUTE_MS = 30 * 24 * 60 * 60 * 1000;
6
+ const IDLE_MS = 7 * 24 * 60 * 60 * 1000;
7
+ const sha256 = (s) => createHash("sha256").update(s, "utf8").digest();
8
+ function fail(c, status, code, message) {
9
+ return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
10
+ }
11
+ /**
12
+ * scrypt, not argon2id, and the deviation is deliberate.
13
+ *
14
+ * 05 §4.7's table says argon2id via `node-argon2`. That is a NATIVE module: it
15
+ * needs a compiler or a prebuilt binary for the exact platform and Node ABI,
16
+ * and A9's promise is one npm package that `npx` runs with no build step. A
17
+ * dependency that fails to install on a machine with no toolchain would make
18
+ * the first-run login unreachable, which is the same failure mode §4.7 rejects
19
+ * magic-link-only auth for.
20
+ *
21
+ * scrypt is memory-hard, in the standard library, and OWASP-parameterised here
22
+ * (N=2^17, r=8, p=1 — their scrypt baseline). The format carries its
23
+ * parameters so an argon2 row can be added later without a migration.
24
+ */
25
+ const SCRYPT = { N: 2 ** 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
26
+ export function hashPassword(password) {
27
+ const salt = randomBytes(16);
28
+ const key = scryptSync(password, salt, 64, SCRYPT);
29
+ return `scrypt$${salt.toString("base64url")}$${key.toString("base64url")}`;
30
+ }
31
+ /**
32
+ * Verify, in constant time, and never leak WHICH part was wrong.
33
+ *
34
+ * Returns false for a user with no password (OAuth-only, §4.7) rather than
35
+ * throwing — but still spends the same work, because an early return on
36
+ * "this account has no password" is an account-enumeration oracle measurable
37
+ * over a few hundred requests.
38
+ */
39
+ export function verifyPassword(password, stored) {
40
+ const parts = (stored ?? "").split("$");
41
+ const decoy = parts.length === 3 ? null : "scrypt$AAAAAAAAAAAAAAAAAAAAAA$AAAA";
42
+ const [, saltB64, keyB64] = (decoy ? decoy.split("$") : parts);
43
+ const salt = Buffer.from(saltB64, "base64url");
44
+ const want = Buffer.from(keyB64, "base64url");
45
+ const got = scryptSync(password, salt, want.length || 64, SCRYPT);
46
+ if (decoy)
47
+ return false;
48
+ return want.length === got.length && timingSafeEqual(want, got);
49
+ }
50
+ /** Both cookies, set together — they are one credential in two halves. */
51
+ function setSessionCookies(c, token, csrf, secure, maxAgeS) {
52
+ const base = `Path=/; SameSite=Lax; Max-Age=${maxAgeS}${secure ? "; Secure" : ""}`;
53
+ // HttpOnly: the value is the credential and no script has any reason to read it.
54
+ c.header("Set-Cookie", `${DASHBOARD_SESSION_COOKIE}=${token}; HttpOnly; ${base}`, { append: true });
55
+ // NOT HttpOnly, deliberately: double-submit works precisely because the
56
+ // dashboard's own JS reads this one and echoes it in a header, which a
57
+ // cross-site page cannot do — it can send the cookie, but it cannot read it.
58
+ c.header("Set-Cookie", `${DASHBOARD_CSRF_COOKIE}=${csrf}; ${base}`, { append: true });
59
+ }
60
+ function clearSessionCookies(c, secure) {
61
+ const base = `Path=/; SameSite=Lax; Max-Age=0${secure ? "; Secure" : ""}`;
62
+ c.header("Set-Cookie", `${DASHBOARD_SESSION_COOKIE}=; HttpOnly; ${base}`, { append: true });
63
+ c.header("Set-Cookie", `${DASHBOARD_CSRF_COOKIE}=; ${base}`, { append: true });
64
+ }
65
+ function readCookie(c, name) {
66
+ const raw = c.req.header("Cookie");
67
+ if (!raw)
68
+ return null;
69
+ for (const part of raw.split(";")) {
70
+ const i = part.indexOf("=");
71
+ if (i < 0)
72
+ continue;
73
+ if (part.slice(0, i).trim() === name)
74
+ return part.slice(i + 1).trim();
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * The middleware. Attaches `owner` when a session verifies; refuses nothing by
80
+ * itself — the routes decide, exactly as `bearerAuth` does on the widget side,
81
+ * so a route that forgot to check is a visible omission rather than an
82
+ * invisible one.
83
+ */
84
+ export function ownerAuth(deps) {
85
+ const { sql } = deps;
86
+ const now = deps.now ?? (() => new Date());
87
+ return async (c, next) => {
88
+ const token = readCookie(c, DASHBOARD_SESSION_COOKIE);
89
+ if (!token)
90
+ return next();
91
+ const at = now();
92
+ const [row] = await sql `
93
+ SELECT s.id, s.org_id, s.user_id, u.email, u.name, s.last_seen_at
94
+ FROM user_sessions s
95
+ JOIN users u ON u.id = s.user_id AND u.deleted_at IS NULL
96
+ WHERE s.token_hash = ${sha256(token)}
97
+ AND s.revoked_at IS NULL
98
+ AND s.expires_at > ${at}
99
+ AND s.last_seen_at > ${new Date(at.getTime() - IDLE_MS)}
100
+ `;
101
+ if (!row)
102
+ return next();
103
+ c.set("owner", {
104
+ userId: row.user_id,
105
+ orgId: row.org_id,
106
+ email: row.email,
107
+ name: row.name,
108
+ sessionId: row.id,
109
+ });
110
+ // Rolling refresh (§4.7), but not on every request: `last_seen_at` only
111
+ // needs to be accurate to within the idle window, and a write per request
112
+ // turns a read-only dashboard page into a write on the hot path.
113
+ if (at.getTime() - row.last_seen_at.getTime() > 60 * 60 * 1000) {
114
+ void sql `UPDATE user_sessions SET last_seen_at = ${at} WHERE id = ${row.id}`.catch(() => { });
115
+ }
116
+ return next();
117
+ };
118
+ }
119
+ /**
120
+ * The double-submit compare itself, exported so a route that cannot mount
121
+ * `requireOwner()` wholesale — because it also serves a different credential
122
+ * — can still run the same check inline (`comments.ts`: a reviewer session
123
+ * posts to the same path an owner cookie does, so the route itself has to
124
+ * branch on which credential arrived rather than gating the whole path).
125
+ *
126
+ * `Origin` is NOT re-checked here — that half of `requireOwner()`'s story is
127
+ * about login CSRF specifically (§4.7's comment on `/auth/login`) and every
128
+ * other owner-cookie route relies on `SameSite=Lax` plus this pair.
129
+ */
130
+ export function csrfOk(c) {
131
+ const sent = c.req.header(HTTP_HEADER.csrf);
132
+ const cookie = readCookie(c, DASHBOARD_CSRF_COOKIE);
133
+ return (!!sent && !!cookie && sent.length === cookie.length && timingSafeEqual(Buffer.from(sent), Buffer.from(cookie)));
134
+ }
135
+ /**
136
+ * §4.7's CSRF pair, on mutations only.
137
+ *
138
+ * TWO checks, and `SameSite=Lax` is explicitly not treated as sufficient by the
139
+ * spec: `Origin` must equal the server's own, AND `x-snagset-csrf` must equal
140
+ * the non-HttpOnly cookie. The second is what a cross-site page cannot forge —
141
+ * it can cause the browser to SEND our cookies, but it cannot READ one to echo
142
+ * it back in a header.
143
+ *
144
+ * A safe method is exempt because a GET must not be a mutation in the first
145
+ * place; if one ever is, that is the bug to fix rather than the check to add.
146
+ */
147
+ export function requireOwner() {
148
+ return async (c, next) => {
149
+ const owner = c.get("owner");
150
+ if (!owner) {
151
+ return c.json({
152
+ error: {
153
+ code: "unauthorized",
154
+ message: "Sign in to the dashboard first.",
155
+ requestId: c.get("requestId"),
156
+ },
157
+ }, 401);
158
+ }
159
+ if (!["GET", "HEAD", "OPTIONS"].includes(c.req.method) && !csrfOk(c)) {
160
+ return c.json({
161
+ error: {
162
+ code: "csrf_failed",
163
+ message: "That request could not be verified. Reload the dashboard and try again.",
164
+ requestId: c.get("requestId"),
165
+ },
166
+ }, 403);
167
+ }
168
+ return next();
169
+ };
170
+ }
171
+ export function ownerRoutes(deps) {
172
+ const app = new Hono();
173
+ const { sql } = deps;
174
+ const now = deps.now ?? (() => new Date());
175
+ /**
176
+ * Sign in. ONE error for every failure — unknown email, wrong password,
177
+ * deleted user — because the difference between them is exactly the
178
+ * information an account-enumeration attack is after.
179
+ */
180
+ app.post("/auth/login", async (c) => {
181
+ // The origin check applies here too: a cross-site page cannot read the
182
+ // response, but it CAN log a victim into an account the attacker controls
183
+ // (login CSRF), and every subsequent action the victim takes lands in the
184
+ // attacker's org.
185
+ // The origin must be OURS — but "ours" is not only the configured base URL.
186
+ //
187
+ // The first attempt compared against `SNAGSET_BASE_URL` alone, which
188
+ // defaults to `http://localhost:8080`, and then refused a browser sitting
189
+ // on `http://127.0.0.1:8080` — the same server, reached by the other name
190
+ // for it. An operator does not reach a self-hosted instance by the name a
191
+ // config default guessed; they reach it by an IP, a LAN hostname, or a
192
+ // tunnel, and being told "this instance does not serve that" by the
193
+ // instance currently serving it is a dead end with no next step.
194
+ //
195
+ // So the request's own Host counts too. That is the standard formulation
196
+ // of this check and it keeps the property that matters: a cross-site page
197
+ // cannot set `Origin`, which is the header being tested. Host is
198
+ // attacker-influenced in principle, but a Host that reaches this process
199
+ // is one the deployment already routes here, and spoofing it does not
200
+ // deliver the victim's cookie anywhere new.
201
+ const origin = c.req.header("Origin");
202
+ const host = c.req.header("Host");
203
+ const ours = new Set([deps.origin, host ? `http://${host}` : "", host ? `https://${host}` : ""]);
204
+ if (origin && !ours.has(origin)) {
205
+ return fail(c, 403, "forbidden", "That sign-in request came from another site.");
206
+ }
207
+ const body = (await c.req.json().catch(() => null));
208
+ const email = typeof body?.email === "string" ? body.email.trim().toLowerCase() : "";
209
+ const password = typeof body?.password === "string" ? body.password : "";
210
+ if (!email || !password) {
211
+ return fail(c, 400, "validation_failed", "Both an email address and a password are required.");
212
+ }
213
+ const [user] = await sql `
214
+ SELECT id, org_id, email, name, password_hash
215
+ FROM users WHERE email = ${email} AND deleted_at IS NULL
216
+ `;
217
+ // `verifyPassword` spends the same work on a missing row as on a wrong
218
+ // password, so this branch costs what the real one costs.
219
+ if (!verifyPassword(password, user?.password_hash ?? null)) {
220
+ return fail(c, 401, "invalid_credentials", "That email address and password do not match an account.");
221
+ }
222
+ const token = randomBytes(32).toString("base64url");
223
+ const csrf = randomBytes(32).toString("base64url");
224
+ const expires = new Date(now().getTime() + ABSOLUTE_MS);
225
+ await sql `
226
+ INSERT INTO user_sessions (id, org_id, user_id, token_hash, user_agent, expires_at)
227
+ VALUES (${randomUUID()}, ${user.org_id}, ${user.id}, ${sha256(token)},
228
+ ${(c.req.header("User-Agent") ?? "").slice(0, 400)}, ${expires})
229
+ `;
230
+ setSessionCookies(c, token, csrf, deps.secure, Math.floor(ABSOLUTE_MS / 1000));
231
+ // No id, for the reason `/api/v1/me` gives: nothing outside the database
232
+ // needs it, and an internal id put on the wire because it was to hand is
233
+ // how the first one always gets out.
234
+ return c.json({ user: { email: user.email, name: user.name }, csrfToken: csrf });
235
+ });
236
+ /** Sign out. Revokes the ROW, so the cookie being kept changes nothing. */
237
+ app.post("/auth/logout", async (c) => {
238
+ const owner = c.get("owner");
239
+ if (owner)
240
+ await sql `UPDATE user_sessions SET revoked_at = now() WHERE id = ${owner.sessionId}`;
241
+ clearSessionCookies(c, deps.secure);
242
+ return c.json({ ok: true });
243
+ });
244
+ /**
245
+ * Who am I, and what can I see — the dashboard's first call on every load.
246
+ *
247
+ * It returns the projects too, because every dashboard screen needs them and
248
+ * a separate round trip for a list this size is a round trip for nothing.
249
+ */
250
+ app.get("/api/v1/me", async (c) => {
251
+ const owner = c.get("owner");
252
+ if (!owner)
253
+ return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
254
+ const [org] = await sql `
255
+ SELECT name, slug FROM orgs WHERE id = ${owner.orgId}
256
+ `;
257
+ const projects = await sql `
258
+ SELECT p.uid, p.name, p.key_prefix, p.write_mode,
259
+ count(t.id)::int AS threads,
260
+ count(t.id) FILTER (WHERE t.status <> 'resolved')::int AS unresolved
261
+ FROM projects p
262
+ LEFT JOIN threads t ON t.project_id = p.id AND t.deleted_at IS NULL AND t.private = false
263
+ WHERE p.org_id = ${owner.orgId} AND p.deleted_at IS NULL
264
+ GROUP BY p.id, p.uid, p.name, p.key_prefix, p.write_mode
265
+ ORDER BY p.created_at
266
+ `;
267
+ return c.json({
268
+ // NO id. Foundations §2.10 mints no `usr_` prefix, and the dashboard has
269
+ // no use for one: it renders a name and an email. AC-1-9 is about
270
+ // project uuids specifically, but the habit is the point — an internal
271
+ // id put on the wire because it was to hand is how the first one always
272
+ // gets out.
273
+ user: { email: owner.email, name: owner.name },
274
+ org: { name: org?.name ?? "", slug: org?.slug ?? "" },
275
+ projects: projects.map((p) => ({
276
+ uid: p.uid,
277
+ name: p.name,
278
+ keyPrefix: p.key_prefix,
279
+ writeMode: p.write_mode,
280
+ threads: p.threads,
281
+ unresolved: p.unresolved,
282
+ })),
283
+ });
284
+ });
285
+ return app;
286
+ }
287
+ export { setSessionCookies };
288
+ //# sourceMappingURL=owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner.js","sourceRoot":"","sources":["../../src/routes/owner.ts"],"names":[],"mappings":"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"}