@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
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAwC,MAAM,MAAM,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EAAkB,SAAS,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,yEAAyE;AACzE,yEAAyE;AACzE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EACL,SAAS,EACT,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,iBAAiB,GAElB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAwC5E,2EAA2E;AAC3E,SAAS,IAAI,CAAC,CAAM,EAAE,MAAc,EAAE,IAAY,EAAE,OAAe,EAAE,OAAiB;IACpF,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,EAC5F,MAAa,CACd,CAAC;AACJ,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,OAAO,CAAC,IAAY;IAClC,MAAM,CAAC,GAAG,IAAI;SACX,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;SACvB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAU;IAClC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAO,CAAC;IAC5B,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE1B,6EAA6E;IAC7E,iEAAiE;IACjE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC,CAAC;IACnF,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG;QACX,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC;QAC1C,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,gCAAgC,CAAC;KACrD,CAAC;IAEF,4EAA4E;IAC5E,sDAAsD;IACtD,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,uBAAuB,CAAC,CAAC;IAChF,MAAM,SAAS,GAAG;QAChB,GAAG;QACH,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,oEAAoE;QACpE,yEAAyE;QACzE,MAAM,EAAE,OAAO,CAAC,QAAQ,KAAK,QAAQ;KACtC,CAAC;IAEF,0EAA0E;IAC1E,4EAA4E;IAC5E,0EAA0E;IAC1E,+DAA+D;IAC/D,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,UAAU,EAAE,CAAC;QAC9D,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACvB,MAAM,IAAI,EAAE,CAAC;QACb,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC;QACnC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QACrB,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACpD,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,gBAAgB,EAAE,qCAAqC,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH;;;;;OAKG;IACH,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAExE;;;;;;;;OAQG;IACH,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC7B,IAAI,CAAC;YACH,MAAM,GAAG,CAAA,UAAU,CAAC;QACtB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,CAAC,IAAI,CACX,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,EAAG,CAAW,CAAC,OAAO,EAAE,EACvF,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,IAAI;YACR,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,QAAQ,EAAE,IAAI;YACd,wEAAwE;YACxE,2DAA2D;YAC3D,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE;SACxE,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH;;uDAEmD;IACnD,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE;QAC5B,CAAC,CAAC,MAAM,CAAC,eAAe,EAAE,oBAAoB,CAAC,CAAC;QAChD,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,qBAAqB,EAAE,EAAE;YACzB,WAAW,EAAE,CAAC,IAAI,CAAC;YACnB,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH;;;OAGG;IACH,GAAG,CAAC,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACpC,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,iBAAiB,EAAE,yCAAyC,CAAC,CAAC;QACpF,CAAC;QAED,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAmC,CAAC;QACtF,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAI,CAAC,CAAC,CAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9F,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,CAK1F,CAAC;QACF,MAAM,WAAW,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;YACzE,IAAI,CAAC,CAAC;gBAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,GAAG,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;YACzB,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,8CAA8C,EAAE;gBACvF,KAAK,EAAE,UAAU;aAClB,CAAC,CAAC;QACL,CAAC;QAED,uEAAuE;QACvE,uEAAuE;QACvE,+DAA+D;QAC/D,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,MAAM,GAAG,CAAsC;;;KAG3D,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CACrB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,eAAe,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAClF,CAAC;QACF,8DAA8D;QAC9D,oEAAoE;QACpE,+BAA+B;QAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CACT,CAAC,EACD,GAAG,EACH,iBAAiB,EACjB,qFAAqF;gBACnF,wDAAwD,CAC3D,CAAC;QACJ,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;QAC3B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,YAAY,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QAE3D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACvC,yEAAyE;YACzE,uEAAuE;YACvE,qEAAqE;YACrE,wEAAwE;YACxE,mDAAmD;YACnD,MAAM,EAAE,CAAA,mCAAmC,KAAK,SAAS,CAAC;YAE1D,MAAM,EAAE,CAAA,6CAA6C,KAAK,KAAK,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;YAC/F,MAAM,EAAE,CAAA;;kBAEI,MAAM,KAAK,KAAK,KAAK,KAAK,KAAK,OAAO,KAAK,YAAY,CAAC,QAAQ,CAAC;OAC5E,CAAC;YACF,MAAM,EAAE,CAAA;kEACoD,KAAK,KAAK,MAAM;OAC3E,CAAC;YACF,yEAAyE;YACzE,qEAAqE;YACrE,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,EAAE,EAAE;gBACtC,KAAK;gBACL,IAAI,EAAE,WAAW,IAAI,eAAe;gBACpC,SAAS,EAAE,MAAM;gBACjB,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,gCAAgC,CAAC;aACtD,CAAC,CAAC;YACH,MAAM,EAAE,CAAA;;kBAEI,UAAU,EAAE,KAAK,KAAK,KAAK,MAAM,KAAK,SAAS,CAAC,YAAY,CAAC;;OAExE,CAAC;YACF,MAAM,EAAE,CAAA,0DAA0D,KAAK,CAAC,EAAE,EAAE,CAAC;YAC7E,wEAAwE;YACxE,yEAAyE;YACzE,MAAM,EAAE,CAAA,wCAAwC,KAAK,CAAC,EAAE,0BAA0B,CAAC;YAEnF,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;QACrC,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC;QAErB,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,kCAAkC;QAClC,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACnD,iBAAiB,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;QAE9E,OAAO,CAAC,CAAC,IAAI,CACX;YACE,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE;YACtB,IAAI,EAAE,EAAE,KAAK,EAAE;YACf,mEAAmE;YACnE,qEAAqE;YACrE,yBAAyB;YACzB,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,UAAU,EAAE,IAAI,EAAE,WAAW,IAAI,eAAe,EAAE,SAAS,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE;YACrH,SAAS,EAAE,IAAI;SAChB,EACD,GAAG,CACJ,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH;;;;;;;;OAQG;IACH;;;;;;OAMG;IACH,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;IACnC,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;IAEvC;;;;;;;;;OASG;IACH,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAE3B,GAAG,CAAC,GAAG,CAAC,gCAAgC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAC1D,CAAC,CAAC,GAAG,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;QAC7B,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,6EAA6E;IAC7E,6EAA6E;IAC7E,2DAA2D;IAC3D;;;;;;;;;;OAUG;IACH,GAAG,CAAC,GAAG,CACL,yBAAyB,EACzB,eAAe,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QACjC,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACnD,CAAC,CAAC,CACH,CAAC;IAEF,6EAA6E;IAC7E,2EAA2E;IAC3E,8CAA8C;IAC9C,GAAG,CAAC,GAAG,CAAC,+BAA+B,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IACrF,GAAG,CAAC,GAAG,CAAC,iCAAiC,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IACvF,6EAA6E;IAC7E,uEAAuE;IACvE,4EAA4E;IAC5E,4EAA4E;IAC5E,0EAA0E;IAC1E,kEAAkE;IAClE,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAEzE,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAC7D,0EAA0E;IAC1E,uEAAuE;IACvE,sDAAsD;IACtD,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;IACxD,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;IACtF,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;IAChE;;;;;OAKG;IACH,GAAG,CAAC,GAAG,CAAC,oBAAoB,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1E,GAAG,CAAC,GAAG,CAAC,kCAAkC,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IACxF,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAE3C,GAAG,CAAC,GAAG,CAAC,mCAAmC,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IACzF,GAAG,CAAC,KAAK,CACP,GAAG,EACH,gBAAgB,CAAC;QACf,GAAG;QACH,IAAI;QACJ,QAAQ;QACR,aAAa,EAAE,GAAG,CAAC,GAAG,CAAC,iCAAiC,CAAC;KAC1D,CAAC,CACH,CAAC;IACF;;;;;;;;;;OAUG;IACH;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,cAAc,GAA2B,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAC/D,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO,IAAI,CACT,CAAC,EACD,GAAG,EACH,oBAAoB,EACpB,mFAAmF;gBACjF,6BAA6B,CAChC,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC,CAAC;IACF,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;IAC5D,GAAG,CAAC,GAAG,CAAC,+BAA+B,EAAE,cAAc,CAAC,CAAC;IACzD,GAAG,CAAC,GAAG,CAAC,+BAA+B,EAAE,YAAY,EAAE,CAAC,CAAC;IACzD,4EAA4E;IAC5E,yEAAyE;IACzE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,CAAC,CAAC;IACzD;;;;;;OAMG;IACH,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAEjD,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,gCAAgC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE3F,GAAG,CAAC,GAAG,CAAC,oCAAoC,EAAE,YAAY,EAAE,CAAC,CAAC;IAC9D,GAAG,CAAC,GAAG,CAAC,wBAAwB,EAAE,YAAY,EAAE,CAAC,CAAC;IAClD,0EAA0E;IAC1E,4EAA4E;IAC5E,4BAA4B;IAC5B,GAAG,CAAC,GAAG,CAAC,8BAA8B,EAAE,YAAY,EAAE,CAAC,CAAC;IACxD,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,YAAY,EAAE,CAAC,CAAC;IAC5C,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC/C,GAAG,CAAC,KAAK,CACP,GAAG,EACH,gBAAgB,CAAC;QACf,GAAG;QACH,QAAQ;QACR,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC,8BAA8B,CAAC;QAChD,UAAU,EAAE,GAAG,CAAC,GAAG,CAAC,kCAAkC,CAAC;KACxD,CAAC,CACH,CAAC;IAEF;;;;;;;;;;;;OAYG;IACH;;;;;;;;;;;;;OAaG;IACH,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;IAEzC;;;;;;;;OAQG;IACH,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAExD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC;IAChG,MAAM,IAAI,GAA2B;QACnC,OAAO,EAAE,0BAA0B;QACnC,KAAK,EAAE,gCAAgC;QACvC,MAAM,EAAE,yBAAyB;QACjC,MAAM,EAAE,eAAe;QACvB,QAAQ,EAAE,YAAY;QACtB,MAAM,EAAE,WAAW;KACpB,CAAC;IACF,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QAC9B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC;QACzC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvG,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QACD,0EAA0E;QAC1E,0EAA0E;QAC1E,MAAM,MAAM,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACtE,MAAM,IAAI,GAAG,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE;YAChG,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QACxC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,CAAC,CAAC,MAAM,CAAC,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC,CAAC;QAClE,2DAA2D;QAC3D,CAAC,CAAC,MAAM,CACN,eAAe,EACf,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,qCAAqC,CAAC,CAAC,CAAC,UAAU,CAC/E,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH;;;;OAIG;IACH,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE;QACjB,IAAI,IAAI,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;YAC3B,OAAO,IAAI,CACT,CAAC,EACD,GAAG,EACH,oBAAoB,EACpB,oFAAoF;gBAClF,qFAAqF;gBACrF,uCAAuC,CAC1C,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC,GAAG,CAAC,MAAM,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;IACnG,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}