@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.
- package/README.md +83 -0
- package/dist/app.js +453 -0
- package/dist/app.js.map +1 -0
- package/dist/bin.js +951 -0
- package/dist/bin.js.map +1 -0
- package/dist/boot.js +321 -0
- package/dist/boot.js.map +1 -0
- package/dist/config.generated.js +116 -0
- package/dist/config.generated.js.map +1 -0
- package/dist/config.js +180 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.js +26 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db/connect.js +49 -0
- package/dist/db/connect.js.map +1 -0
- package/dist/db/migrate.js +210 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0001_init.sql +1060 -0
- package/dist/db/migrations/0002_maintenance_claim.sql +109 -0
- package/dist/db/migrations/0003_email_provider.sql +35 -0
- package/dist/db/migrations/0004_unsubscribe.sql +24 -0
- package/dist/db/rls.js +58 -0
- package/dist/db/rls.js.map +1 -0
- package/dist/jobs/index.js +171 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/reaper.js +182 -0
- package/dist/jobs/reaper.js.map +1 -0
- package/dist/keys.js +77 -0
- package/dist/keys.js.map +1 -0
- package/dist/mail/index.js +75 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/mailer.js +27 -0
- package/dist/mail/mailer.js.map +1 -0
- package/dist/mail/notify.js +405 -0
- package/dist/mail/notify.js.map +1 -0
- package/dist/mail/resend.js +95 -0
- package/dist/mail/resend.js.map +1 -0
- package/dist/mail/smtp.js +315 -0
- package/dist/mail/smtp.js.map +1 -0
- package/dist/mail/unsubscribe.js +134 -0
- package/dist/mail/unsubscribe.js.map +1 -0
- package/dist/projectCache.js +50 -0
- package/dist/projectCache.js.map +1 -0
- package/dist/projects.js +101 -0
- package/dist/projects.js.map +1 -0
- package/dist/routes/attachments.js +159 -0
- package/dist/routes/attachments.js.map +1 -0
- package/dist/routes/auth.js +55 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/commentEdit.js +201 -0
- package/dist/routes/commentEdit.js.map +1 -0
- package/dist/routes/comments.js +313 -0
- package/dist/routes/comments.js.map +1 -0
- package/dist/routes/cors.js +174 -0
- package/dist/routes/cors.js.map +1 -0
- package/dist/routes/jobs.js +56 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/mcpAuth.js +187 -0
- package/dist/routes/mcpAuth.js.map +1 -0
- package/dist/routes/mcpToken.js +94 -0
- package/dist/routes/mcpToken.js.map +1 -0
- package/dist/routes/owner.js +288 -0
- package/dist/routes/owner.js.map +1 -0
- package/dist/routes/reviewLinks.js +169 -0
- package/dist/routes/reviewLinks.js.map +1 -0
- package/dist/routes/rounds.js +137 -0
- package/dist/routes/rounds.js.map +1 -0
- package/dist/routes/sessions.js +349 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/sites.js +192 -0
- package/dist/routes/sites.js.map +1 -0
- package/dist/routes/threads.js +1080 -0
- package/dist/routes/threads.js.map +1 -0
- package/dist/routes/widget.js +132 -0
- package/dist/routes/widget.js.map +1 -0
- package/dist/serverless.js +97 -0
- package/dist/serverless.js.map +1 -0
- package/dist/session.js +152 -0
- package/dist/session.js.map +1 -0
- package/dist/storage/bucket.js +116 -0
- package/dist/storage/bucket.js.map +1 -0
- package/dist/storage/settings.js +132 -0
- package/dist/storage/settings.js.map +1 -0
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/adapters/outbound/fence.js +44 -0
- package/dist/vendor/adapters/outbound/sanitise.js +233 -0
- package/dist/vendor/types/index.js +4 -0
- package/dist/vendor/types/names.js +254 -0
- package/dist/vendor/widget/object.js +38 -0
- package/dist/vendor/widget/predicates.js +377 -0
- package/package.json +53 -0
- package/static/dashboard/assets/index-CaPfKhFP.css +1 -0
- package/static/dashboard/assets/index-DfTNjHCK.js +1 -0
- package/static/dashboard/index.html +14 -0
- package/static/widget/snag.anchor-ext.js +1 -0
- package/static/widget/snag.anchor-fuzzy.js +1 -0
- package/static/widget/snag.capture.js +4 -0
- package/static/widget/snag.crumbs.js +4 -0
- package/static/widget/snag.dock.js +5 -0
- package/static/widget/snag.js +5 -0
- package/static/widget/snag.panels.js +2 -0
- package/static/widget/snag.runtime.js +2 -0
- package/static/widget/snag.screenshot.js +1 -0
- package/static/widget/snag.text.js +2 -0
- package/vercel.json +6 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# `snagset` — the server
|
|
2
|
+
|
|
3
|
+
One npm package: the API, the widget assets, the dashboard statics and the
|
|
4
|
+
migrations, with two entry points on the same `createApp(deps)` (LOCK L5/L7).
|
|
5
|
+
|
|
6
|
+
## Run it
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
DATABASE_URL=postgres://… npx snagset
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
That is the whole install. The bin migrates its own schema under an advisory
|
|
13
|
+
lock, generates a signing key if it has none, prints a claim URL, and serves.
|
|
14
|
+
Postgres is the only requirement — [A9a, LOCK L6](../../specs/00-foundations.md):
|
|
15
|
+
no Redis, no S3, no queue, no extensions.
|
|
16
|
+
|
|
17
|
+
| Command | What it does |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `snagset` | migrate, then serve (the default) |
|
|
20
|
+
| `snagset migrate` | migrate and exit — for a deploy step |
|
|
21
|
+
| `snagset bootstrap` | mint a fresh claim URL, voiding any outstanding one |
|
|
22
|
+
| `snagset origins <uid>` | what may write to a project, and what has asked to |
|
|
23
|
+
| `snagset allow-origin <uid> <origin>` | approve one |
|
|
24
|
+
|
|
25
|
+
Exit codes: `2` bad config, `3` database unreachable, `4` a migration refused
|
|
26
|
+
(a checksum changed, or the database is ahead of this build).
|
|
27
|
+
|
|
28
|
+
## First run
|
|
29
|
+
|
|
30
|
+
`orgs` is empty, so the instance is **CLAIMABLE**: every route but `/healthz`,
|
|
31
|
+
`/readyz`, `/api/v1/meta` and `/api/v1/setup` answers `409 instance_unclaimed`,
|
|
32
|
+
and the claim URL is printed to stdout and to `<data>/bootstrap.txt`.
|
|
33
|
+
|
|
34
|
+
409 and not 503 is deliberate ([08 §3.4](../../specs/08-deployment.md)): a 503
|
|
35
|
+
tells every proxy in the path that a healthy instance is unhealthy, and a
|
|
36
|
+
fresh container behind one would restart-loop before anyone read the URL.
|
|
37
|
+
|
|
38
|
+
Opening the URL and posting to `/api/v1/setup` creates the org, the owner, the
|
|
39
|
+
first project, its `production` and `preview` environments, **Round 1**, and a
|
|
40
|
+
session — in one transaction. The gate lifts without a restart.
|
|
41
|
+
|
|
42
|
+
Lost the URL? It is stored as a hash and cannot be re-printed. `snagset
|
|
43
|
+
bootstrap` mints a new one.
|
|
44
|
+
|
|
45
|
+
## Two things an operator should know
|
|
46
|
+
|
|
47
|
+
**The signing key.** Generated to `<data>/secret.key` at mode 0600 on first
|
|
48
|
+
boot, or supplied as `SNAGSET_SECRET_KEY`. Back it up and treat it as a
|
|
49
|
+
password: without it, stored integration credentials are unrecoverable
|
|
50
|
+
ciphertext and every reviewer session is dead. On serverless it is **required**
|
|
51
|
+
— the filesystem is ephemeral, so a generated key would differ on every cold
|
|
52
|
+
start.
|
|
53
|
+
|
|
54
|
+
**Row-level security may not be in force.** Migration 0001 enables and `FORCE`s
|
|
55
|
+
RLS on all 31 tenanted tables. A role with the `BYPASSRLS` attribute overrides
|
|
56
|
+
all of it — and managed Postgres often grants it to the role in the connection
|
|
57
|
+
string it hands you (Neon's `neondb_owner` does, and cannot drop it from
|
|
58
|
+
itself). Boot says so, and `/readyz` reports it. Requests are still scoped by
|
|
59
|
+
`org_id` in the application, so this is a lost safety net rather than an open
|
|
60
|
+
door; to get it back, create a role without `BYPASSRLS`, grant it the tables,
|
|
61
|
+
and point `DATABASE_URL` at that. See [D50](../../PRODUCT-DECISIONS.md).
|
|
62
|
+
|
|
63
|
+
## Origins
|
|
64
|
+
|
|
65
|
+
A project starts with an empty allow-list, and nothing is ever allow-listed
|
|
66
|
+
automatically: `Origin` is attacker-controllable, so auto-allowing would be an
|
|
67
|
+
attacker-controlled header granting access. Origins that fetch `/config` are
|
|
68
|
+
*recorded* — `snagset origins <uid>` shows them — and `snagset allow-origin`
|
|
69
|
+
is the human decision ([08 §3.5 D2](../../specs/08-deployment.md)).
|
|
70
|
+
|
|
71
|
+
## Where the schema comes from
|
|
72
|
+
|
|
73
|
+
`src/db/migrations/0001_init.sql` is **generated** from
|
|
74
|
+
[05 §3](../../specs/05-api-data.md) — 32 tables, 15 enums, 58 indexes — plus
|
|
75
|
+
the RLS block this repo adds. `pnpm gen:migration:check` fails on drift, which
|
|
76
|
+
makes editing the spec the only way to change the database. Same for the
|
|
77
|
+
config registry against [08 §4](../../specs/08-deployment.md).
|
|
78
|
+
|
|
79
|
+
## Tests
|
|
80
|
+
|
|
81
|
+
`pnpm test` runs everything that needs no database. Set
|
|
82
|
+
`SNAGSET_TEST_DATABASE_URL` to also run the claim-flow tests — they **drop and
|
|
83
|
+
recreate the public schema**, which is why they refuse to read `DATABASE_URL`.
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { tokenHash } from "./boot.js";
|
|
7
|
+
import { selectMailer } from "./mail/index.js";
|
|
8
|
+
import { widgetRoutes } from "./routes/widget.js";
|
|
9
|
+
import { jobsRoutes } from "./routes/jobs.js";
|
|
10
|
+
import { rlsStatus } from "./db/rls.js";
|
|
11
|
+
import { threadRoutes } from "./routes/threads.js";
|
|
12
|
+
import { commentRoutes } from "./routes/comments.js";
|
|
13
|
+
import { commentEditRoutes } from "./routes/commentEdit.js";
|
|
14
|
+
import { corsForProjects } from "./routes/cors.js";
|
|
15
|
+
import { bearerAuth } from "./routes/auth.js";
|
|
16
|
+
import { attachmentRoutes, features } from "./routes/attachments.js";
|
|
17
|
+
import { sessionRoutes } from "./routes/sessions.js";
|
|
18
|
+
import { reviewLinkRoutes } from "./routes/reviewLinks.js";
|
|
19
|
+
import { roundRoutes } from "./routes/rounds.js";
|
|
20
|
+
import { siteRoutes } from "./routes/sites.js";
|
|
21
|
+
import { mcpAuth } from "./routes/mcpAuth.js";
|
|
22
|
+
import { createKeyring } from "./keys.js";
|
|
23
|
+
import { unsubscribeRoutes } from "./mail/unsubscribe.js";
|
|
24
|
+
// `wireId` moved to `projects.ts`, beside its only consumer. Re-exported
|
|
25
|
+
// because `boot.ts` imports it from here and the name is public surface.
|
|
26
|
+
import { createProject, validateOrigins, wireId } from "./projects.js";
|
|
27
|
+
export { wireId };
|
|
28
|
+
import { createProjectCache } from "./projectCache.js";
|
|
29
|
+
import { ownerAuth, ownerRoutes, requireOwner, hashPassword, setSessionCookies, } from "./routes/owner.js";
|
|
30
|
+
import { ID_PREFIX, THREAD_REF_PREFIX, header } from "./vendor/types/names.js";
|
|
31
|
+
/** §2.2's envelope — every status ≥ 400, always, with a code from §6.9. */
|
|
32
|
+
function fail(c, status, code, message, details) {
|
|
33
|
+
return c.json({ error: { code, message, ...(details ? { details } : {}), requestId: c.get("requestId") } }, status);
|
|
34
|
+
}
|
|
35
|
+
/** A slug from a display name, matching orgs.slug's CHECK. */
|
|
36
|
+
export function slugify(name) {
|
|
37
|
+
const s = name
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "")
|
|
41
|
+
.slice(0, 39);
|
|
42
|
+
return s.length >= 3 ? s : `org-${randomBytes(4).toString("hex")}`;
|
|
43
|
+
}
|
|
44
|
+
export function createApp(deps) {
|
|
45
|
+
const app = new Hono();
|
|
46
|
+
const { sql, cfg } = deps;
|
|
47
|
+
// One keyring and one project cache for the process. The cache is what makes
|
|
48
|
+
// `session_epoch` revocation cost nothing per request (05 §4.4).
|
|
49
|
+
const keys = createKeyring(deps.secretKey, cfg.get("SNAGSET_SECRET_KEY_PREVIOUS"));
|
|
50
|
+
const projects = createProjectCache(sql);
|
|
51
|
+
const ttls = {
|
|
52
|
+
slidingS: cfg.int("SNAGSET_SESSION_TTL_S"),
|
|
53
|
+
absoluteS: cfg.int("SNAGSET_SESSION_ABSOLUTE_TTL_S"),
|
|
54
|
+
};
|
|
55
|
+
// Declared up here because `/api/v1/setup` uses `secure` to set its cookies
|
|
56
|
+
// and is registered long before the middleware below.
|
|
57
|
+
const baseUrl = new URL(cfg.get("SNAGSET_BASE_URL") ?? "http://localhost:8080");
|
|
58
|
+
const ownerDeps = {
|
|
59
|
+
sql,
|
|
60
|
+
origin: baseUrl.origin,
|
|
61
|
+
// A `Secure` cookie is dropped on plain http, which on a first `npx
|
|
62
|
+
// snagset` run against localhost reads as "login silently does nothing".
|
|
63
|
+
secure: baseUrl.protocol === "https:",
|
|
64
|
+
};
|
|
65
|
+
// §2.2: every response carries the request id, the server version and the
|
|
66
|
+
// server clock. The clock is not decoration — §12.3 uses it to correct skew
|
|
67
|
+
// on a reviewer's machine, and a widget with a wrong clock signs identity
|
|
68
|
+
// envelopes that fail verification for reasons nobody can see.
|
|
69
|
+
app.use("*", async (c, next) => {
|
|
70
|
+
const id = c.req.header(header("request-id")) ?? randomUUID();
|
|
71
|
+
c.set("requestId", id);
|
|
72
|
+
await next();
|
|
73
|
+
c.header(header("request-id"), id);
|
|
74
|
+
c.header(header("server"), deps.version);
|
|
75
|
+
c.header(header("time"), new Date().toISOString());
|
|
76
|
+
});
|
|
77
|
+
app.onError((err, c) => {
|
|
78
|
+
console.error(`error [${c.get("requestId")}]`, err);
|
|
79
|
+
return fail(c, 500, "internal_error", "Something went wrong on the server.");
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* LIVENESS. Deliberately touches nothing: the question is "is this process
|
|
83
|
+
* alive", and a health check that queries the database restarts a healthy
|
|
84
|
+
* app every time the database hiccups — turning a brief outage into a
|
|
85
|
+
* restart storm that outlives it.
|
|
86
|
+
*/
|
|
87
|
+
app.get("/healthz", (c) => c.json({ ok: true, version: deps.version }));
|
|
88
|
+
/**
|
|
89
|
+
* READINESS. This one IS allowed to touch the database, because "ready"
|
|
90
|
+
* means "can serve a request", and a request needs the database.
|
|
91
|
+
*
|
|
92
|
+
* An unclaimed instance is READY in the orchestrator's sense — 08 §3.4's
|
|
93
|
+
* whole argument about 409-not-503 applies here too. It is healthy; it is
|
|
94
|
+
* waiting for a human. Reporting otherwise puts it in a restart loop and the
|
|
95
|
+
* operator never gets to read the claim URL.
|
|
96
|
+
*/
|
|
97
|
+
app.get("/readyz", async (c) => {
|
|
98
|
+
try {
|
|
99
|
+
await sql `SELECT 1`;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return c.json({ ok: false, state: deps.state, database: "unreachable", detail: e.message }, 503);
|
|
103
|
+
}
|
|
104
|
+
const rls = await rlsStatus(sql);
|
|
105
|
+
return c.json({
|
|
106
|
+
ok: true,
|
|
107
|
+
state: deps.state,
|
|
108
|
+
database: "ok",
|
|
109
|
+
// D50: surfaced rather than assumed. An operator who has to ask whether
|
|
110
|
+
// RLS is doing anything should be able to read the answer.
|
|
111
|
+
rls: { effective: rls.effective, role: rls.role, warning: rls.warning },
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
/** 05 §6.11. Unauthenticated on purpose: `sourceUrl` is AGPL-3.0 §13's offer
|
|
115
|
+
* of source to anyone interacting with a modified version over a network,
|
|
116
|
+
* and an offer behind a login is not an offer. */
|
|
117
|
+
app.get("/api/v1/meta", (c) => {
|
|
118
|
+
c.header("Cache-Control", "public, max-age=60");
|
|
119
|
+
return c.json({
|
|
120
|
+
version: deps.version,
|
|
121
|
+
commit: deps.commit,
|
|
122
|
+
sourceUrl: deps.sourceUrl,
|
|
123
|
+
modified: deps.modified,
|
|
124
|
+
blockedClientVersions: [],
|
|
125
|
+
apiVersions: ["v1"],
|
|
126
|
+
serverTime: new Date().toISOString(),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* 05 §6.10 — the claim. Single-use, and the transaction either creates an
|
|
131
|
+
* entire working instance or none of it.
|
|
132
|
+
*/
|
|
133
|
+
app.post("/api/v1/setup", async (c) => {
|
|
134
|
+
if (deps.state === "READY") {
|
|
135
|
+
return fail(c, 409, "already_claimed", "This instance has already been claimed.");
|
|
136
|
+
}
|
|
137
|
+
const body = (await c.req.json().catch(() => null));
|
|
138
|
+
const str = (k) => (typeof body?.[k] === "string" ? body[k] : "");
|
|
139
|
+
const [token, orgName, email, password] = ["token", "orgName", "email", "password"].map(str);
|
|
140
|
+
const projectName = str("projectName");
|
|
141
|
+
for (const [k, v] of Object.entries({ token, orgName, email, password })) {
|
|
142
|
+
if (!v)
|
|
143
|
+
return fail(c, 400, "validation_failed", `${k} is required.`, { field: k });
|
|
144
|
+
}
|
|
145
|
+
if (password.length < 12) {
|
|
146
|
+
return fail(c, 400, "validation_failed", "The password must be at least 12 characters.", {
|
|
147
|
+
field: "password",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// Step 1 — constant-time compare. The row is found by hash rather than
|
|
151
|
+
// fetched-then-compared so a wrong token and an unknown token take the
|
|
152
|
+
// same path; `timingSafeEqual` then covers the compare itself.
|
|
153
|
+
const want = tokenHash(token);
|
|
154
|
+
const rows = await sql `
|
|
155
|
+
SELECT id, token_hash FROM setup_tokens
|
|
156
|
+
WHERE consumed_at IS NULL AND (expires_at IS NULL OR expires_at > now())
|
|
157
|
+
`;
|
|
158
|
+
const match = rows.find((r) => r.token_hash.length === want.length && timingSafeEqual(r.token_hash, want));
|
|
159
|
+
// §6.10 step 1 and 08 §3.4 step 6: unknown or consumed is 409
|
|
160
|
+
// already_claimed, NOT 403. Same code for both, so a guesser learns
|
|
161
|
+
// nothing from the difference.
|
|
162
|
+
if (!match) {
|
|
163
|
+
return fail(c, 409, "already_claimed", "That claim token is not valid. If the instance is already set up, sign in instead; " +
|
|
164
|
+
"otherwise run `npx snagset bootstrap` for a fresh one.");
|
|
165
|
+
}
|
|
166
|
+
const orgId = randomUUID();
|
|
167
|
+
const userId = randomUUID();
|
|
168
|
+
const sessionToken = randomBytes(32).toString("base64url");
|
|
169
|
+
const out = await sql.begin(async (tx) => {
|
|
170
|
+
// Every statement below runs under RLS. `app.org_id` is set FIRST and to
|
|
171
|
+
// the org we are about to create: the policies apply to INSERT through
|
|
172
|
+
// their WITH CHECK, so without this the very first insert fails on a
|
|
173
|
+
// correctly-secured deployment. (On a BYPASSRLS role it would appear to
|
|
174
|
+
// work — which is exactly why D50's probe exists.)
|
|
175
|
+
await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
|
|
176
|
+
await tx `INSERT INTO orgs (id, name, slug) VALUES (${orgId}, ${orgName}, ${slugify(orgName)})`;
|
|
177
|
+
await tx `
|
|
178
|
+
INSERT INTO users (id, org_id, email, name, password_hash, email_verified)
|
|
179
|
+
VALUES (${userId}, ${orgId}, ${email}, ${orgName}, ${hashPassword(password)}, true)
|
|
180
|
+
`;
|
|
181
|
+
await tx `
|
|
182
|
+
INSERT INTO memberships (org_id, user_id, role) VALUES (${orgId}, ${userId}, 'owner')
|
|
183
|
+
`;
|
|
184
|
+
// One call, `projects.ts`. A project is a row plus two environments plus
|
|
185
|
+
// a round, and this route is the one that learned that the hard way.
|
|
186
|
+
const created = await createProject(tx, {
|
|
187
|
+
orgId,
|
|
188
|
+
name: projectName || "First project",
|
|
189
|
+
createdBy: userId,
|
|
190
|
+
graceHours: cfg.int("SNAGSET_WRITE_MODE_GRACE_HOURS"),
|
|
191
|
+
});
|
|
192
|
+
await tx `
|
|
193
|
+
INSERT INTO user_sessions (id, org_id, user_id, token_hash, expires_at)
|
|
194
|
+
VALUES (${randomUUID()}, ${orgId}, ${userId}, ${tokenHash(sessionToken)},
|
|
195
|
+
now() + interval '30 days')
|
|
196
|
+
`;
|
|
197
|
+
await tx `UPDATE setup_tokens SET consumed_at = now() WHERE id = ${match.id}`;
|
|
198
|
+
// Step 2's last clause. Any other outstanding token is now a second key
|
|
199
|
+
// to a door that has been opened; there is no reason for one to survive.
|
|
200
|
+
await tx `DELETE FROM setup_tokens WHERE id <> ${match.id} AND consumed_at IS NULL`;
|
|
201
|
+
return { projectUid: created.uid };
|
|
202
|
+
});
|
|
203
|
+
deps.state = "READY";
|
|
204
|
+
// Claiming an instance signs you in. The row was just written above; this
|
|
205
|
+
// sets the two cookies that make it usable, so the first-run flow ends on
|
|
206
|
+
// the dashboard rather than on a login form asking for the password the
|
|
207
|
+
// operator typed ten seconds ago.
|
|
208
|
+
const csrf = randomBytes(32).toString("base64url");
|
|
209
|
+
setSessionCookies(c, sessionToken, csrf, ownerDeps.secure, 30 * 24 * 60 * 60);
|
|
210
|
+
return c.json({
|
|
211
|
+
org: { name: orgName },
|
|
212
|
+
user: { email },
|
|
213
|
+
// The uid, never the row id: it is the project identity everything
|
|
214
|
+
// outside the database uses, and AC-1-9 asserts no dashboard surface
|
|
215
|
+
// carries the other one.
|
|
216
|
+
project: { uid: out.projectUid, name: projectName || "First project", keyPrefix: THREAD_REF_PREFIX.replace("-", "") },
|
|
217
|
+
csrfToken: csrf,
|
|
218
|
+
}, 201);
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* CREDENTIAL HYGIENE — the one place raw credentials are presented.
|
|
222
|
+
*
|
|
223
|
+
* Nothing in any spec keeps `snag_k`, a bearer or a `holderSecret` out of
|
|
224
|
+
* SERVER logs: the fragment argument covers URLs, but the token arrives in
|
|
225
|
+
* the sessions request BODY. So the rule is mechanical rather than a
|
|
226
|
+
* convention — this middleware marks the route, and no body is ever logged
|
|
227
|
+
* on it.
|
|
228
|
+
*/
|
|
229
|
+
/**
|
|
230
|
+
* OWNER auth (05 §4.7), app-wide and free when absent: no dashboard cookie
|
|
231
|
+
* means an immediate `next()`, so the widget's routes — which are on another
|
|
232
|
+
* origin and can never carry it — pay nothing. Mounted here rather than per
|
|
233
|
+
* route group because the last time a credential middleware lived inside one
|
|
234
|
+
* group, the route registered before it answered without protection.
|
|
235
|
+
*/
|
|
236
|
+
app.use("*", ownerAuth(ownerDeps));
|
|
237
|
+
app.route("/", ownerRoutes(ownerDeps));
|
|
238
|
+
/**
|
|
239
|
+
* MCP bearer auth (07 §8.2), app-wide for the same reason `ownerAuth` is:
|
|
240
|
+
* the credential is attached wherever it is presented and every route
|
|
241
|
+
* decides for itself what a scope permits. It costs one `startsWith` when no
|
|
242
|
+
* MCP token is present, which is every widget request.
|
|
243
|
+
*
|
|
244
|
+
* Mounted AFTER `ownerAuth` and before every route, so `c.get("mcp")` is
|
|
245
|
+
* populated by the time a handler asks. The two never collide: an owner
|
|
246
|
+
* arrives in a cookie, an agent in `Authorization: Bearer`.
|
|
247
|
+
*/
|
|
248
|
+
app.use("*", mcpAuth(sql));
|
|
249
|
+
app.use("/api/v1/projects/:uid/sessions", async (c, next) => {
|
|
250
|
+
c.set("noBodyLogging", true);
|
|
251
|
+
await next();
|
|
252
|
+
});
|
|
253
|
+
// The widget's surface. Mounted after /setup so the gate below still catches
|
|
254
|
+
// it while unclaimed: a project route on an unclaimed instance is a 409, not
|
|
255
|
+
// a 404, because there is no project rather than no route.
|
|
256
|
+
/**
|
|
257
|
+
* 05 §5.1's CORS classes, at the APP level so EVERY project route is covered.
|
|
258
|
+
*
|
|
259
|
+
* Route-group middleware only covers routes registered after it IN THAT
|
|
260
|
+
* GROUP. This lived inside `threadRoutes`, and `sessionRoutes` is mounted
|
|
261
|
+
* first — so the credential exchange answered 201 with no
|
|
262
|
+
* `Access-Control-Allow-Origin`, the preflight passed (its OPTIONS handler
|
|
263
|
+
* WAS in the covered group), and the browser reported `net::ERR_FAILED` on
|
|
264
|
+
* the POST underneath a green OPTIONS. A CORS failure wearing the costume of
|
|
265
|
+
* a network fault, on the one route that hands out a credential.
|
|
266
|
+
*/
|
|
267
|
+
app.use("/api/v1/projects/:uid/*", corsForProjects(sql, async (uid) => {
|
|
268
|
+
const p = await projects.get(uid).catch(() => null);
|
|
269
|
+
return p ? { id: p.id, org_id: p.org_id } : null;
|
|
270
|
+
}));
|
|
271
|
+
// After CORS, before the routes: a session is ATTACHED when one verifies and
|
|
272
|
+
// the route decides whether it needed one. `/sessions` is excluded — it is
|
|
273
|
+
// where a bearer is exchanged, not presented.
|
|
274
|
+
app.use("/api/v1/projects/:uid/threads", bearerAuth(keys, projects, ttls.absoluteS));
|
|
275
|
+
app.use("/api/v1/projects/:uid/threads/*", bearerAuth(keys, projects, ttls.absoluteS));
|
|
276
|
+
// The dashboard's unscoped form, `/api/v1/threads/{id}/comments` — no `:uid`
|
|
277
|
+
// segment for `bearerAuth` to read, so this mount is a no-op today (it
|
|
278
|
+
// returns immediately whenever its own `uid` param is absent). Added anyway
|
|
279
|
+
// so the credential-attaching middleware for this surface is declared once,
|
|
280
|
+
// per path, the same way every other one here is — not discovered missing
|
|
281
|
+
// the day a bearer-carrying caller of the unscoped form shows up.
|
|
282
|
+
app.use("/api/v1/threads/*", bearerAuth(keys, projects, ttls.absoluteS));
|
|
283
|
+
app.route("/", sessionRoutes({ sql, keys, projects, ttls }));
|
|
284
|
+
// Whether to QUEUE a notification at all — see CommentDeps.notifications.
|
|
285
|
+
// Computed once at construction: the transport cannot change without a
|
|
286
|
+
// config change, and a config change means a restart.
|
|
287
|
+
const notifications = selectMailer(cfg).kind !== "none";
|
|
288
|
+
app.route("/", threadRoutes(sql, projects, () => features(sql, keys), notifications));
|
|
289
|
+
app.route("/", commentRoutes({ sql, projects, notifications }));
|
|
290
|
+
/**
|
|
291
|
+
* Editing and deleting a comment (endpoints 12 and 13). Mounted here, beside
|
|
292
|
+
* the comment routes, and BEFORE the owner-gated block below — a reviewer
|
|
293
|
+
* session is a legitimate credential on these paths, so `requireOwner()`
|
|
294
|
+
* must not stand in front of them.
|
|
295
|
+
*/
|
|
296
|
+
app.use("/api/v1/comments/*", bearerAuth(keys, projects, ttls.absoluteS));
|
|
297
|
+
app.use("/api/v1/projects/:uid/comments/*", bearerAuth(keys, projects, ttls.absoluteS));
|
|
298
|
+
app.route("/", commentEditRoutes({ sql }));
|
|
299
|
+
app.use("/api/v1/projects/:uid/attachments", bearerAuth(keys, projects, ttls.absoluteS));
|
|
300
|
+
app.route("/", attachmentRoutes({
|
|
301
|
+
sql,
|
|
302
|
+
keys,
|
|
303
|
+
projects,
|
|
304
|
+
maxImageBytes: cfg.int("SNAGSET_STORAGE_MAX_IMAGE_BYTES"),
|
|
305
|
+
}));
|
|
306
|
+
/**
|
|
307
|
+
* Dashboard-class (05 §5.1): no CORS headers, so a reviewed site cannot mint
|
|
308
|
+
* or revoke a link even holding a valid widget session.
|
|
309
|
+
*
|
|
310
|
+
* AND NOW AUTHENTICATED, which it was not. Absent CORS headers stop a
|
|
311
|
+
* BROWSER on another origin from reading the response; they stop nothing
|
|
312
|
+
* else. `curl` against the port minted a working review link with no
|
|
313
|
+
* credential at all — and under D3 the link IS the access-control system, so
|
|
314
|
+
* that was the whole of it. Found by minting one that way while measuring
|
|
315
|
+
* latency.
|
|
316
|
+
*/
|
|
317
|
+
/**
|
|
318
|
+
* Adding a site (D63). Dashboard-class: owner cookie plus the CSRF pair on
|
|
319
|
+
* the mutation, both from `requireOwner()`.
|
|
320
|
+
*
|
|
321
|
+
* `/api/v1/projects` is registered UNSCOPED and therefore before the `:uid`
|
|
322
|
+
* mounts — a middleware mounted on `/api/v1/projects/:uid/*` never sees it,
|
|
323
|
+
* which is precisely how the review-link route ended up mintable by `curl`.
|
|
324
|
+
*
|
|
325
|
+
* THE UNCLAIMED CHECK COMES FIRST, and it is not decoration. §3.4 step 5's
|
|
326
|
+
* gate is the catch-all at the bottom of this file, so before this route
|
|
327
|
+
* existed an unclaimed instance answered `/api/v1/projects` with 409
|
|
328
|
+
* `instance_unclaimed` simply because nothing matched. Mounting
|
|
329
|
+
* `requireOwner()` here made it 401 "sign in to the dashboard first" — advice
|
|
330
|
+
* for an instance that has no users to sign in as, on the exact request an
|
|
331
|
+
* operator makes when they have not finished setting it up. Caught by
|
|
332
|
+
* `setup.test.ts`, which asserts the gate covers everything outside its
|
|
333
|
+
* allow-list.
|
|
334
|
+
*/
|
|
335
|
+
const unclaimedFirst = async (c, next) => {
|
|
336
|
+
if (deps.state !== "READY") {
|
|
337
|
+
return fail(c, 409, "instance_unclaimed", "This instance has not been claimed yet. Open the claim URL from the server logs, " +
|
|
338
|
+
"or run `snagset bootstrap`.");
|
|
339
|
+
}
|
|
340
|
+
return next();
|
|
341
|
+
};
|
|
342
|
+
app.use("/api/v1/projects", unclaimedFirst, requireOwner());
|
|
343
|
+
app.use("/api/v1/projects/:uid/origins", unclaimedFirst);
|
|
344
|
+
app.use("/api/v1/projects/:uid/origins", requireOwner());
|
|
345
|
+
// The audience PATCH is dashboard-class too — but requireOwner cannot mount
|
|
346
|
+
// on the bare :uid path, whose GETs belong to the widget. Method-scoped.
|
|
347
|
+
app.on("PATCH", "/api/v1/projects/:uid", requireOwner());
|
|
348
|
+
/**
|
|
349
|
+
* One-click unsubscribe (B2). Mounted OUTSIDE `/api/v1`, with no auth
|
|
350
|
+
* middleware in front of it, and both facts are the design: the person
|
|
351
|
+
* clicking may hold no session and the link's HMAC is what authorises it.
|
|
352
|
+
* A route class that demanded a credential here would mean the only people
|
|
353
|
+
* who can stop the mail are the ones still able to log in.
|
|
354
|
+
*/
|
|
355
|
+
app.route("/", unsubscribeRoutes({ sql, keys }));
|
|
356
|
+
app.route("/", siteRoutes({ sql, graceHours: cfg.int("SNAGSET_WRITE_MODE_GRACE_HOURS") }));
|
|
357
|
+
app.use("/api/v1/projects/:uid/review-links", requireOwner());
|
|
358
|
+
app.use("/api/v1/review-links/*", requireOwner());
|
|
359
|
+
// Rounds are a STUDIO act (D2, and `rounds.created_by` references `users`
|
|
360
|
+
// rather than `reviewers` for exactly that reason), so the whole surface is
|
|
361
|
+
// owner-gated the same way.
|
|
362
|
+
app.use("/api/v1/projects/:uid/rounds", requireOwner());
|
|
363
|
+
app.use("/api/v1/rounds/*", requireOwner());
|
|
364
|
+
app.route("/", roundRoutes({ sql, projects }));
|
|
365
|
+
app.route("/", reviewLinkRoutes({
|
|
366
|
+
sql,
|
|
367
|
+
projects,
|
|
368
|
+
ttlDays: cfg.int("SNAGSET_REVIEW_LINK_TTL_DAYS"),
|
|
369
|
+
maxTtlDays: cfg.int("SNAGSET_REVIEW_LINK_MAX_TTL_DAYS"),
|
|
370
|
+
}));
|
|
371
|
+
/**
|
|
372
|
+
* The dashboard's static assets — foundations §10 / LOCK L5: the SPA is
|
|
373
|
+
* BUNDLED INTO this package rather than deployed separately, so `npx
|
|
374
|
+
* snagset` is one process with a UI and not a server plus a hosting
|
|
375
|
+
* decision.
|
|
376
|
+
*
|
|
377
|
+
* Served here rather than by a static middleware because there is one
|
|
378
|
+
* subtlety worth being explicit about: an SPA needs every unmatched
|
|
379
|
+
* dashboard path to return `index.html` so a deep link like
|
|
380
|
+
* `/?project=…&t=SN-14` survives a reload — but that fallback must NOT
|
|
381
|
+
* swallow `/api/*`, or a typo in an API path returns 200 and a page of HTML
|
|
382
|
+
* where the caller expected an error envelope.
|
|
383
|
+
*/
|
|
384
|
+
/**
|
|
385
|
+
* The jobs endpoint — 08 §3.2's cron target, and it lives HERE rather than
|
|
386
|
+
* in `serverless.ts` for one reason: everything below this line is a
|
|
387
|
+
* catch-all. The first version registered it on the app returned by this
|
|
388
|
+
* function and got a 404 on every call, because Hono matches in registration
|
|
389
|
+
* order and the dashboard fallback had already claimed `/*`. That is the
|
|
390
|
+
* same mistake the widget routes made an hour earlier, which is why both now
|
|
391
|
+
* sit above the fallback and say so.
|
|
392
|
+
*
|
|
393
|
+
* The bin gets it too. It ticks on its own interval and does not need it,
|
|
394
|
+
* but an operator with a stuck queue having a way to force a tick is worth
|
|
395
|
+
* more than the symmetry of hiding it — and it carries the same
|
|
396
|
+
* authentication either way.
|
|
397
|
+
*/
|
|
398
|
+
app.route("/", jobsRoutes({ sql, cfg }));
|
|
399
|
+
/**
|
|
400
|
+
* The widget, BEFORE the dashboard's catch-all (08 §3.8, L2.3).
|
|
401
|
+
*
|
|
402
|
+
* Order is the whole of it. The fallback below answers every unmatched path
|
|
403
|
+
* with `index.html`, so mounted after it `/widget/snag.js` would return a
|
|
404
|
+
* page of HTML with a 200 — which is precisely how the absence of these
|
|
405
|
+
* routes went unnoticed until a script tag on a real site failed with a MIME
|
|
406
|
+
* type error.
|
|
407
|
+
*/
|
|
408
|
+
app.route("/", widgetRoutes({ version: deps.version }));
|
|
409
|
+
const dashboardDir = join(dirname(fileURLToPath(import.meta.url)), "..", "static", "dashboard");
|
|
410
|
+
const MIME = {
|
|
411
|
+
".html": "text/html; charset=utf-8",
|
|
412
|
+
".js": "text/javascript; charset=utf-8",
|
|
413
|
+
".css": "text/css; charset=utf-8",
|
|
414
|
+
".svg": "image/svg+xml",
|
|
415
|
+
".woff2": "font/woff2",
|
|
416
|
+
".png": "image/png",
|
|
417
|
+
};
|
|
418
|
+
app.get("/*", async (c, next) => {
|
|
419
|
+
const path = new URL(c.req.url).pathname;
|
|
420
|
+
if (path.startsWith("/api/") || path.startsWith("/auth/") || path === "/healthz" || path === "/readyz") {
|
|
421
|
+
return next();
|
|
422
|
+
}
|
|
423
|
+
// Vite's hashed assets live under /assets/; everything else falls back to
|
|
424
|
+
// the shell. `..` cannot escape because the join is resolved and checked.
|
|
425
|
+
const wanted = path === "/" ? "index.html" : path.replace(/^\/+/, "");
|
|
426
|
+
const full = resolve(dashboardDir, wanted);
|
|
427
|
+
const file = full.startsWith(resolve(dashboardDir)) && existsSync(full) && statSync(full).isFile()
|
|
428
|
+
? full
|
|
429
|
+
: resolve(dashboardDir, "index.html");
|
|
430
|
+
if (!existsSync(file))
|
|
431
|
+
return next();
|
|
432
|
+
const ext = file.slice(file.lastIndexOf("."));
|
|
433
|
+
c.header("Content-Type", MIME[ext] ?? "application/octet-stream");
|
|
434
|
+
// Hashed filenames are immutable; the shell must never be.
|
|
435
|
+
c.header("Cache-Control", file.includes("/assets/") ? "public, max-age=31536000, immutable" : "no-cache");
|
|
436
|
+
return c.body(readFileSync(file));
|
|
437
|
+
});
|
|
438
|
+
/**
|
|
439
|
+
* 08 §3.4 step 5 — the gate. Everything not listed above answers 409 while
|
|
440
|
+
* the instance is unclaimed, with a message that tells the operator what to
|
|
441
|
+
* do rather than that the route does not exist (05 §6.10 step 4).
|
|
442
|
+
*/
|
|
443
|
+
app.all("*", (c) => {
|
|
444
|
+
if (deps.state !== "READY") {
|
|
445
|
+
return fail(c, 409, "instance_unclaimed", "This Snagset instance is running but has not been claimed yet. Open the claim URL " +
|
|
446
|
+
"printed in the server logs (or in <data>/bootstrap.txt) to create the first owner. " +
|
|
447
|
+
"Lost it? Run `npx snagset bootstrap`.");
|
|
448
|
+
}
|
|
449
|
+
return fail(c, 404, "not_found", `No route for ${c.req.method} ${new URL(c.req.url).pathname}.`);
|
|
450
|
+
});
|
|
451
|
+
return app;
|
|
452
|
+
}
|
|
453
|
+
//# sourceMappingURL=app.js.map
|
package/dist/app.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
|