@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
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { randomUUID, randomBytes, createHash } from "node:crypto";
|
|
3
|
+
import { ID_PREFIX } from "../vendor/types/names.js";
|
|
4
|
+
import { ProjectUnavailable } from "../projectCache.js";
|
|
5
|
+
import { mintSession, verifySession, withinRefreshWindow } from "../session.js";
|
|
6
|
+
function fail(c, status, code, message) {
|
|
7
|
+
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
8
|
+
}
|
|
9
|
+
/** Every secret in this product: 32 bytes, unpadded base64url, 43 chars. The
|
|
10
|
+
* hash is taken over the string that arrived on the wire, never a re-decode. */
|
|
11
|
+
const sha256 = (s) => createHash("sha256").update(s, "utf8").digest();
|
|
12
|
+
const mintSecret = () => randomBytes(32).toString("base64url");
|
|
13
|
+
/** `prj_…`/`rev_…` — the row id is bare, the wire form is prefixed (§2.10). */
|
|
14
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
15
|
+
function rowId(wire, prefix) {
|
|
16
|
+
if (typeof wire !== "string")
|
|
17
|
+
return null;
|
|
18
|
+
const bare = wire.startsWith(prefix) ? wire.slice(prefix.length) : wire;
|
|
19
|
+
return UUID.test(bare) ? bare.toLowerCase() : null;
|
|
20
|
+
}
|
|
21
|
+
/** §3.4's default: what an ordinary client link may do. */
|
|
22
|
+
const CLIENT_PERMISSIONS = ["thread:create", "comment:create", "thread:resolve"];
|
|
23
|
+
/**
|
|
24
|
+
* 05 §4.2's read-time downgrade. There is no jobs engine yet (05 §3.11 item
|
|
25
|
+
* 12, the reaper), so `write_mode = 'open'` past its `write_mode_downgrade_at`
|
|
26
|
+
* is evaluated HERE, on every session mint, rather than flipped by a
|
|
27
|
+
* background pass. When M2-31's `runDueJobs` lands it should ALSO persist the
|
|
28
|
+
* downgrade (`UPDATE projects SET write_mode = 'link-required', … WHERE …`) so
|
|
29
|
+
* this stays a fallback rather than the only enforcement — but a session
|
|
30
|
+
* request must never trust a `write_mode` column that could be 24 hours
|
|
31
|
+
* stale, which is why this function exists independently of that job.
|
|
32
|
+
*/
|
|
33
|
+
export function effectiveWriteMode(row, at) {
|
|
34
|
+
if (row.write_mode === "open" &&
|
|
35
|
+
row.write_mode_downgrade_at &&
|
|
36
|
+
row.write_mode_downgrade_at.getTime() <= at.getTime()) {
|
|
37
|
+
return "link-required";
|
|
38
|
+
}
|
|
39
|
+
return row.write_mode;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* `perm` is computed ONCE, from the credential, here — §4.4's table.
|
|
43
|
+
*
|
|
44
|
+
* *"Nothing about visibility is ever taken from the request. Not a header, not
|
|
45
|
+
* a query parameter, not a field on the body, not the reviewer's own claim
|
|
46
|
+
* about themselves."* `thread:private` and `thread:assign` decide whether a
|
|
47
|
+
* session sees team-only threads, so the only route by which a widget session
|
|
48
|
+
* gets them is a review link whose stored `permissions` already contain them —
|
|
49
|
+
* the "team link" of §3.4.
|
|
50
|
+
*/
|
|
51
|
+
function permissionsFor(kind, link) {
|
|
52
|
+
if (kind === "guest")
|
|
53
|
+
return CLIENT_PERMISSIONS.filter((p) => p !== "thread:resolve");
|
|
54
|
+
if (kind === "link" && link) {
|
|
55
|
+
const stored = link.permissions.filter((p) => ["thread:create", "comment:create", "thread:resolve", "thread:delete:own", "thread:private", "thread:assign"].includes(p));
|
|
56
|
+
return stored.length ? stored : CLIENT_PERMISSIONS;
|
|
57
|
+
}
|
|
58
|
+
return CLIENT_PERMISSIONS;
|
|
59
|
+
}
|
|
60
|
+
export function sessionRoutes(deps) {
|
|
61
|
+
const app = new Hono();
|
|
62
|
+
const { sql, keys, projects, ttls } = deps;
|
|
63
|
+
const now = deps.now ?? (() => new Date());
|
|
64
|
+
app.post("/api/v1/projects/:uid/sessions", async (c) => {
|
|
65
|
+
const uid = c.req.param("uid");
|
|
66
|
+
let project;
|
|
67
|
+
try {
|
|
68
|
+
project = await projects.get(uid);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// Fail closed. A stale cache entry is a stale session_epoch, and that
|
|
72
|
+
// means a revoked reviewer keeps working (projectCache.ts).
|
|
73
|
+
if (e instanceof ProjectUnavailable) {
|
|
74
|
+
c.header("Retry-After", "5");
|
|
75
|
+
return fail(c, 503, "database_unavailable", "The server cannot reach its database right now.");
|
|
76
|
+
}
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
79
|
+
if (!project)
|
|
80
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
81
|
+
// §6.9's codes, not §4.5.1's blanket 401: a paused project is 403 and a
|
|
82
|
+
// disabled one 410 everywhere else in this API, and a client branches on
|
|
83
|
+
// the status class. "Your token is wrong" and "this project is switched
|
|
84
|
+
// off" are different problems with different next steps.
|
|
85
|
+
if (project.status === "paused") {
|
|
86
|
+
return fail(c, 403, "project_paused", "This project is paused. Its owner can resume it.");
|
|
87
|
+
}
|
|
88
|
+
if (project.status === "disabled" || project.status === "deleting") {
|
|
89
|
+
return fail(c, 410, "project_disabled", "This project is no longer accepting comments.");
|
|
90
|
+
}
|
|
91
|
+
// Computed once, up front, from the row the cache just gave us — see
|
|
92
|
+
// `effectiveWriteMode`'s own comment for why this can't wait for a reaper.
|
|
93
|
+
const writeMode = effectiveWriteMode(project, now());
|
|
94
|
+
const body = (await c.req.json().catch(() => null));
|
|
95
|
+
if (!body)
|
|
96
|
+
return fail(c, 400, "validation_failed", "A JSON body is required.");
|
|
97
|
+
// §4.4, and A31's shape of rule: a request that tries to grant itself
|
|
98
|
+
// visibility is refused rather than ignored, because silently dropping the
|
|
99
|
+
// field teaches the caller it worked.
|
|
100
|
+
for (const forbidden of ["role", "isTeam", "includePrivate", "permissions", "perm"]) {
|
|
101
|
+
if (forbidden in body || (body.identity && forbidden in body.identity)) {
|
|
102
|
+
return fail(c, 422, "field_not_permitted", `"${forbidden}" is not accepted here. What a session may see is decided by its credential, never by the request.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const bearer = /^Bearer (.+)$/i.exec(c.req.header("Authorization") ?? "")?.[1];
|
|
106
|
+
const identity = body.identity;
|
|
107
|
+
// ── REFRESH: a bearer and no identity (§4.4). Silent, and the whole reason
|
|
108
|
+
// a reviewer is not asked to re-open their link four times an afternoon.
|
|
109
|
+
if (bearer && !identity) {
|
|
110
|
+
const v = await verifySession(keys, bearer, {
|
|
111
|
+
projectUid: uid,
|
|
112
|
+
expectedEpoch: project.session_epoch,
|
|
113
|
+
allowExpired: true,
|
|
114
|
+
absoluteTtlS: ttls.absoluteS,
|
|
115
|
+
now,
|
|
116
|
+
});
|
|
117
|
+
if (!v.ok) {
|
|
118
|
+
const code = v.reason === "session_revoked" ? "session_revoked" : "unauthorized";
|
|
119
|
+
return fail(c, 401, code, "That session is no longer valid. Re-open your review link to continue.");
|
|
120
|
+
}
|
|
121
|
+
// A link session re-checks the link on refresh (§4.4): revocation is
|
|
122
|
+
// instant at exchange AND on every refresh, which is what bounds how long
|
|
123
|
+
// a revoked link keeps working to one sliding window.
|
|
124
|
+
let linkExpiresAt;
|
|
125
|
+
if (v.claims.lnk) {
|
|
126
|
+
const [link] = await sql `
|
|
127
|
+
SELECT expires_at, revoked_at FROM review_links WHERE id = ${v.claims.lnk}
|
|
128
|
+
`;
|
|
129
|
+
if (!link || link.revoked_at || link.expires_at <= now()) {
|
|
130
|
+
return fail(c, 401, "token_revoked", "This review link has been turned off.");
|
|
131
|
+
}
|
|
132
|
+
linkExpiresAt = Math.floor(link.expires_at.getTime() / 1000);
|
|
133
|
+
}
|
|
134
|
+
if (!withinRefreshWindow(v.claims, ttls, linkExpiresAt, now)) {
|
|
135
|
+
return fail(c, 401, "session_expired", "This review session has reached its limit. Re-open your review link.");
|
|
136
|
+
}
|
|
137
|
+
const [reviewer] = await sql `
|
|
138
|
+
SELECT identity_valid_after FROM reviewers WHERE id = ${v.claims.sub}
|
|
139
|
+
`;
|
|
140
|
+
if (reviewer?.identity_valid_after && v.claims.iat * 1000 < reviewer.identity_valid_after.getTime()) {
|
|
141
|
+
return fail(c, 401, "session_revoked", "This reviewer has been signed out.");
|
|
142
|
+
}
|
|
143
|
+
const minted = await mintSession(keys, {
|
|
144
|
+
projectUid: uid,
|
|
145
|
+
reviewerId: v.claims.sub,
|
|
146
|
+
environmentKey: v.claims.env,
|
|
147
|
+
kind: v.claims.kind,
|
|
148
|
+
reviewLinkId: v.claims.lnk,
|
|
149
|
+
permissions: v.claims.perm,
|
|
150
|
+
sessionEpoch: project.session_epoch,
|
|
151
|
+
// Carried forward — the cap does not slide (session.ts).
|
|
152
|
+
sid: v.claims.sid,
|
|
153
|
+
iat: v.claims.iat,
|
|
154
|
+
...(v.claims.iex ? { iex: v.claims.iex } : {}),
|
|
155
|
+
...(linkExpiresAt ? { linkExpiresAt } : {}),
|
|
156
|
+
}, ttls, now);
|
|
157
|
+
return c.json({
|
|
158
|
+
sessionToken: minted.token,
|
|
159
|
+
expiresAt: minted.expiresAt.toISOString(),
|
|
160
|
+
refreshableUntil: minted.refreshableUntil.toISOString(),
|
|
161
|
+
serverTime: now().toISOString(),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!identity || typeof identity.kind !== "string") {
|
|
165
|
+
return fail(c, 400, "validation_failed", "identity.kind is required (guest or link).");
|
|
166
|
+
}
|
|
167
|
+
if (identity.kind === "verified") {
|
|
168
|
+
// §4.6's HMAC handshake is M2-19. Saying so beats a 500.
|
|
169
|
+
return fail(c, 501, "not_implemented", "Verified identity is not available on this build yet.");
|
|
170
|
+
}
|
|
171
|
+
if (identity.kind !== "guest" && identity.kind !== "link") {
|
|
172
|
+
return fail(c, 400, "validation_failed", `Unknown identity kind "${identity.kind}".`);
|
|
173
|
+
}
|
|
174
|
+
// ── §4.2's write-mode gate. This is what closed the hole: a project past
|
|
175
|
+
// `open`'s grace window (or minted `link-required`/`verified-only`)
|
|
176
|
+
// used to mint a guest session anyway, because nothing here ever read
|
|
177
|
+
// `write_mode`. `verified-only` is checked FIRST: neither a `guest`
|
|
178
|
+
// identity (no credential at all) nor a `link` identity is ever
|
|
179
|
+
// `verified`, so both must be refused before the guest/link check
|
|
180
|
+
// below gets a chance to mislabel a verified-only rejection as
|
|
181
|
+
// "get a link" (specs/05-api-data.md §6.9 registers the two codes
|
|
182
|
+
// this way, and §4.2's table maps mode -> code the same way; a
|
|
183
|
+
// guest-first ordering here made a verified-only project answer
|
|
184
|
+
// guests with 403 link_required instead of verified_identity_required
|
|
185
|
+
// — a link would not have helped them). Only once verified-only is
|
|
186
|
+
// out of the way does a guest identity fall through to link_required
|
|
187
|
+
// for anything short of `open`.
|
|
188
|
+
if (writeMode === "verified-only") {
|
|
189
|
+
return fail(c, 403, "verified_identity_required", "This project requires a verified identity to comment.");
|
|
190
|
+
}
|
|
191
|
+
if (identity.kind === "guest" && writeMode !== "open") {
|
|
192
|
+
return fail(c, 403, "link_required", "This project needs a review link to comment. Ask the studio for one.");
|
|
193
|
+
}
|
|
194
|
+
// ── The environment. Absent means the project's default (the widget only
|
|
195
|
+
// knows a name if the host put one on the script tag).
|
|
196
|
+
const envKey = typeof body.environment === "string" && body.environment ? body.environment : null;
|
|
197
|
+
const [environment] = await sql `
|
|
198
|
+
SELECT id, key, label FROM environments
|
|
199
|
+
WHERE project_id = ${project.id} AND deleted_at IS NULL
|
|
200
|
+
AND ${envKey ? sql `key = ${envKey}` : sql `is_default`}
|
|
201
|
+
LIMIT 1
|
|
202
|
+
`;
|
|
203
|
+
if (!environment) {
|
|
204
|
+
return fail(c, 400, "environment_not_found", envKey ? `This project has no environment called "${envKey}".` : "This project has no default environment.");
|
|
205
|
+
}
|
|
206
|
+
// ── §4.5.1's validation table, IN ORDER.
|
|
207
|
+
let link = null;
|
|
208
|
+
if (identity.kind === "link") {
|
|
209
|
+
if (typeof identity.token !== "string" || !identity.token) {
|
|
210
|
+
return fail(c, 401, "token_invalid", "That review link is not valid.");
|
|
211
|
+
}
|
|
212
|
+
// Looked up BY HASH, so an unknown token and a wrong one take the same
|
|
213
|
+
// path — there is no row to compare against and therefore nothing to
|
|
214
|
+
// compare in variable time.
|
|
215
|
+
// T8: LEFT JOIN rounds so a round-pinned link can report the round
|
|
216
|
+
// NUMBER (what a reviewer and the widget both mean by "round"), not
|
|
217
|
+
// just its opaque id -- rounds.id never left the server before this.
|
|
218
|
+
const [found] = await sql `
|
|
219
|
+
SELECT rl.id, rl.round_id, rd.number AS round_number, rl.permissions, rl.environments,
|
|
220
|
+
rl.allow_name_override, rl.name, rl.email,
|
|
221
|
+
rl.max_uses, rl.use_count, rl.expires_at, rl.revoked_at
|
|
222
|
+
FROM review_links rl
|
|
223
|
+
LEFT JOIN rounds rd ON rd.id = rl.round_id
|
|
224
|
+
WHERE rl.project_id = ${project.id} AND rl.token_hash = ${sha256(identity.token)}
|
|
225
|
+
`;
|
|
226
|
+
if (!found)
|
|
227
|
+
return fail(c, 401, "token_invalid", "That review link is not valid.");
|
|
228
|
+
if (found.revoked_at)
|
|
229
|
+
return fail(c, 401, "token_revoked", "This review link has been turned off.");
|
|
230
|
+
if (found.expires_at <= now()) {
|
|
231
|
+
return fail(c, 401, "token_expired", "This review link has expired. Ask for a new one.");
|
|
232
|
+
}
|
|
233
|
+
if (!found.environments.includes("*") && !found.environments.includes(environment.key)) {
|
|
234
|
+
return fail(c, 401, "token_wrong_environment", "This review link is not for this environment.");
|
|
235
|
+
}
|
|
236
|
+
link = found;
|
|
237
|
+
}
|
|
238
|
+
const kind = identity.kind;
|
|
239
|
+
const permissions = permissionsFor(kind, link);
|
|
240
|
+
// ── The reviewer. §4.4.1: the SERVER mints the holder secret and returns
|
|
241
|
+
// it once; later exchanges present it back.
|
|
242
|
+
const presented = typeof identity.holderSecret === "string" ? identity.holderSecret : null;
|
|
243
|
+
const presentedId = rowId(identity.holderId, ID_PREFIX.reviewer);
|
|
244
|
+
let holderSecret = null;
|
|
245
|
+
let reviewerId = null;
|
|
246
|
+
let reviewerName;
|
|
247
|
+
// `allow_name_override = false` ⇒ the link's own name is authoritative and
|
|
248
|
+
// a typed one is silently IGNORED, never a 422: the reviewer then sees the
|
|
249
|
+
// name they will be posting under rather than the one they asked for.
|
|
250
|
+
const typedName = typeof identity.name === "string" ? identity.name.trim().slice(0, 80) : "";
|
|
251
|
+
reviewerName = link && !link.allow_name_override ? link.name : typedName || link?.name || "Anonymous";
|
|
252
|
+
if (presented && presentedId) {
|
|
253
|
+
const [existing] = await sql `
|
|
254
|
+
SELECT id, name FROM reviewers
|
|
255
|
+
WHERE id = ${presentedId} AND project_id = ${project.id}
|
|
256
|
+
AND holder_secret_hash = ${sha256(presented)}
|
|
257
|
+
AND blocked_at IS NULL AND erased_at IS NULL
|
|
258
|
+
`;
|
|
259
|
+
// §4.4.1: "a mismatch does NOT error — it silently mints a new guest
|
|
260
|
+
// identity, because a stale storage entry must never lock a reviewer
|
|
261
|
+
// out." So this is an `if`, not a guard clause.
|
|
262
|
+
if (existing) {
|
|
263
|
+
reviewerId = existing.id;
|
|
264
|
+
if (typedName && (!link || link.allow_name_override)) {
|
|
265
|
+
await sql `UPDATE reviewers SET name = ${reviewerName}, last_seen_at = now() WHERE id = ${reviewerId}`;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
await sql `UPDATE reviewers SET last_seen_at = now() WHERE id = ${reviewerId}`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!reviewerId) {
|
|
273
|
+
// §4.5.1's `max_uses` check happens HERE, not above, because it counts
|
|
274
|
+
// PEOPLE: a holder who already has a row is readmitted even on an
|
|
275
|
+
// exhausted link, and only a genuinely new person is refused.
|
|
276
|
+
if (link && link.max_uses !== null && link.use_count >= link.max_uses) {
|
|
277
|
+
return fail(c, 401, "token_exhausted", "This review link has reached its limit.");
|
|
278
|
+
}
|
|
279
|
+
holderSecret = mintSecret();
|
|
280
|
+
const id = randomUUID();
|
|
281
|
+
const inserted = await sql.begin(async (tx) => {
|
|
282
|
+
await tx `SELECT set_config('app.org_id', ${project.org_id}, true)`;
|
|
283
|
+
const rows = await tx `
|
|
284
|
+
INSERT INTO reviewers (id, org_id, project_id, kind, name, email, review_link_id, holder_secret_hash)
|
|
285
|
+
VALUES (${id}, ${project.org_id}, ${project.id}, ${kind}, ${reviewerName},
|
|
286
|
+
${link?.email ?? null}, ${link?.id ?? null}, ${sha256(holderSecret)})
|
|
287
|
+
ON CONFLICT (project_id, review_link_id, holder_secret_hash) WHERE kind = 'link'
|
|
288
|
+
DO NOTHING
|
|
289
|
+
RETURNING id
|
|
290
|
+
`;
|
|
291
|
+
// §4.5: use_count increments ONLY on a returned id — that is what makes
|
|
292
|
+
// it count people rather than exchanges. `last_used_at` rides along, so
|
|
293
|
+
// an exchange is one extra statement rather than two.
|
|
294
|
+
if (rows[0] && link) {
|
|
295
|
+
await tx `
|
|
296
|
+
UPDATE review_links SET use_count = use_count + 1, last_used_at = now() WHERE id = ${link.id}
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
else if (link) {
|
|
300
|
+
await tx `UPDATE review_links SET last_used_at = now() WHERE id = ${link.id}`;
|
|
301
|
+
}
|
|
302
|
+
return rows[0]?.id ?? null;
|
|
303
|
+
});
|
|
304
|
+
reviewerId = inserted;
|
|
305
|
+
if (!reviewerId) {
|
|
306
|
+
// The conflict fired: this holder already has a row on this link.
|
|
307
|
+
const [again] = await sql `
|
|
308
|
+
SELECT id FROM reviewers
|
|
309
|
+
WHERE project_id = ${project.id} AND review_link_id = ${link?.id ?? null}
|
|
310
|
+
AND holder_secret_hash = ${sha256(holderSecret)} AND kind = 'link'
|
|
311
|
+
`;
|
|
312
|
+
reviewerId = again?.id ?? null;
|
|
313
|
+
holderSecret = null;
|
|
314
|
+
}
|
|
315
|
+
if (!reviewerId)
|
|
316
|
+
return fail(c, 500, "internal_error", "Could not create a reviewer.");
|
|
317
|
+
}
|
|
318
|
+
const linkExpiresAt = link ? Math.floor(link.expires_at.getTime() / 1000) : undefined;
|
|
319
|
+
const minted = await mintSession(keys, {
|
|
320
|
+
projectUid: uid,
|
|
321
|
+
reviewerId,
|
|
322
|
+
environmentKey: environment.key,
|
|
323
|
+
kind,
|
|
324
|
+
reviewLinkId: link?.id ?? null,
|
|
325
|
+
permissions,
|
|
326
|
+
sessionEpoch: project.session_epoch,
|
|
327
|
+
...(linkExpiresAt ? { linkExpiresAt } : {}),
|
|
328
|
+
}, ttls, now);
|
|
329
|
+
return c.json({
|
|
330
|
+
sessionToken: minted.token,
|
|
331
|
+
expiresAt: minted.expiresAt.toISOString(),
|
|
332
|
+
refreshableUntil: minted.refreshableUntil.toISOString(),
|
|
333
|
+
reviewer: { id: ID_PREFIX.reviewer + reviewerId, name: reviewerName, kind },
|
|
334
|
+
// Returned ONCE, on first creation of this person (§4.4.1). Absent on
|
|
335
|
+
// every later exchange, which is how the client knows to keep the one
|
|
336
|
+
// it has.
|
|
337
|
+
...(holderSecret ? { holderSecret } : {}),
|
|
338
|
+
environment,
|
|
339
|
+
permissions,
|
|
340
|
+
// T8: `number` alongside `id` -- the widget stores the NUMBER beside
|
|
341
|
+
// the bearer and passes `?round=<number>` on `GET …/threads`; the
|
|
342
|
+
// internal `rounds.id` is never a query param anywhere in 05.
|
|
343
|
+
...(link?.round_id ? { round: { id: link.round_id, number: link.round_number } } : {}),
|
|
344
|
+
serverTime: now().toISOString(),
|
|
345
|
+
}, 201);
|
|
346
|
+
});
|
|
347
|
+
return app;
|
|
348
|
+
}
|
|
349
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/routes/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { createProject, validateOrigins } from "../projects.js";
|
|
3
|
+
/**
|
|
4
|
+
* NO CACHE INVALIDATION HOOK, deliberately.
|
|
5
|
+
*
|
|
6
|
+
* The obvious next line here is "bust the project cache so the new site is live
|
|
7
|
+
* at once", and it would be dead code. A new project was never cached — the
|
|
8
|
+
* cache is a miss and loads it. And `cors.ts`'s origin cache is a self-expiring
|
|
9
|
+
* 10 s window whose own comment already made this call: *"a NEWLY allowed
|
|
10
|
+
* origin waits up to ten seconds to work, which reads as 'it took a moment'"*.
|
|
11
|
+
* A hook that fires and changes nothing is worse than none, because the next
|
|
12
|
+
* person reading it believes invalidation is handled.
|
|
13
|
+
*/
|
|
14
|
+
function fail(c, status, code, message) {
|
|
15
|
+
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
16
|
+
}
|
|
17
|
+
export function siteRoutes(deps) {
|
|
18
|
+
const app = new Hono();
|
|
19
|
+
const { sql } = deps;
|
|
20
|
+
/**
|
|
21
|
+
* `POST /api/v1/projects` — add a site.
|
|
22
|
+
*
|
|
23
|
+
* Unscoped, because there is no project to scope it to yet; the ORG comes
|
|
24
|
+
* from the owner cookie and never from the body. A body-supplied `orgId`
|
|
25
|
+
* would be the whole authorisation model, handed to the caller.
|
|
26
|
+
*/
|
|
27
|
+
app.post("/api/v1/projects", async (c) => {
|
|
28
|
+
const owner = c.get("owner");
|
|
29
|
+
if (!owner)
|
|
30
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
31
|
+
const body = (await c.req.json().catch(() => null));
|
|
32
|
+
const name = typeof body?.["name"] === "string" ? body["name"].trim() : "";
|
|
33
|
+
if (!name)
|
|
34
|
+
return fail(c, 400, "validation_failed", "A site needs a name.");
|
|
35
|
+
if (name.length > 120) {
|
|
36
|
+
return fail(c, 400, "validation_failed", "That name is too long — 120 characters at most.");
|
|
37
|
+
}
|
|
38
|
+
// Accepts an array or the comma-separated string a text input produces, so
|
|
39
|
+
// the dashboard does not have to split it and the CLI does not have to
|
|
40
|
+
// build an array. One shape on the wire is not worth a parsing rule in two
|
|
41
|
+
// clients.
|
|
42
|
+
const raw = body?.["origins"];
|
|
43
|
+
const origins = Array.isArray(raw)
|
|
44
|
+
? raw.map(String)
|
|
45
|
+
: typeof raw === "string"
|
|
46
|
+
? raw.split(",")
|
|
47
|
+
: [];
|
|
48
|
+
try {
|
|
49
|
+
validateOrigins(origins);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return fail(c, 400, "validation_failed", e instanceof Error ? e.message : String(e));
|
|
53
|
+
}
|
|
54
|
+
// A site with no allowed origin cannot be talked to by anything — CORS
|
|
55
|
+
// refuses every request from the tag the operator is about to paste. It is
|
|
56
|
+
// ALLOWED (an origin can be added later, and `snagset allow-origin`
|
|
57
|
+
// exists) but the response says so, because the alternative is a tag that
|
|
58
|
+
// is installed correctly and silently does nothing.
|
|
59
|
+
const created = await sql.begin(async (tx) => {
|
|
60
|
+
await tx `SELECT set_config('app.org_id', ${owner.orgId}, true)`;
|
|
61
|
+
return createProject(tx, {
|
|
62
|
+
orgId: owner.orgId,
|
|
63
|
+
name,
|
|
64
|
+
createdBy: owner.userId,
|
|
65
|
+
origins,
|
|
66
|
+
graceHours: deps.graceHours,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return c.json({
|
|
70
|
+
uid: created.uid,
|
|
71
|
+
name: created.name,
|
|
72
|
+
origins: validateOrigins(origins),
|
|
73
|
+
...(origins.length
|
|
74
|
+
? {}
|
|
75
|
+
: {
|
|
76
|
+
warning: "No origins allowed yet, so no site can talk to this project. " +
|
|
77
|
+
"Add one before pasting the tag.",
|
|
78
|
+
}),
|
|
79
|
+
}, 201);
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* `POST /api/v1/projects/:uid/origins` — allow another origin.
|
|
83
|
+
*
|
|
84
|
+
* Here rather than in a route file of its own because it is the other half
|
|
85
|
+
* of adding a site: you get it wrong, or you add staging first and
|
|
86
|
+
* production later, and the alternative is the CLI on a machine the person
|
|
87
|
+
* who noticed does not have.
|
|
88
|
+
*/
|
|
89
|
+
app.post("/api/v1/projects/:uid/origins", async (c) => {
|
|
90
|
+
const owner = c.get("owner");
|
|
91
|
+
if (!owner)
|
|
92
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
93
|
+
const [project] = await sql `
|
|
94
|
+
SELECT id, org_id FROM projects WHERE uid = ${c.req.param("uid")} AND deleted_at IS NULL
|
|
95
|
+
`;
|
|
96
|
+
// A project in another org is reported as absent, not as forbidden — the
|
|
97
|
+
// same rule the comment route follows, so a uid's validity leaks nothing
|
|
98
|
+
// about somebody else's org.
|
|
99
|
+
if (!project || project.org_id !== owner.orgId) {
|
|
100
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
101
|
+
}
|
|
102
|
+
const body = (await c.req.json().catch(() => null));
|
|
103
|
+
const raw = body?.["origin"] ?? body?.["origins"];
|
|
104
|
+
let origins;
|
|
105
|
+
try {
|
|
106
|
+
origins = validateOrigins(Array.isArray(raw) ? raw.map(String) : typeof raw === "string" ? raw.split(",") : []);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
return fail(c, 400, "validation_failed", e instanceof Error ? e.message : String(e));
|
|
110
|
+
}
|
|
111
|
+
if (!origins.length)
|
|
112
|
+
return fail(c, 400, "validation_failed", "An origin is required.");
|
|
113
|
+
await sql.begin(async (tx) => {
|
|
114
|
+
await tx `SELECT set_config('app.org_id', ${owner.orgId}, true)`;
|
|
115
|
+
for (const origin of origins) {
|
|
116
|
+
// Adding an origin that is already allowed is a no-op, not an error:
|
|
117
|
+
// the operator's intent is satisfied either way, and a 409 here would
|
|
118
|
+
// make "make sure staging is allowed" a two-step operation.
|
|
119
|
+
await tx `
|
|
120
|
+
INSERT INTO project_origins (id, org_id, project_id, pattern, is_wildcard)
|
|
121
|
+
VALUES (gen_random_uuid(), ${owner.orgId}, ${project.id}, ${origin}, ${origin.includes("*.")})
|
|
122
|
+
ON CONFLICT (project_id, pattern) DO NOTHING
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const all = await sql `
|
|
127
|
+
SELECT pattern FROM project_origins WHERE project_id = ${project.id} ORDER BY pattern
|
|
128
|
+
`;
|
|
129
|
+
return c.json({ origins: all.map((r) => r.pattern) }, 201);
|
|
130
|
+
});
|
|
131
|
+
/**
|
|
132
|
+
* `PATCH /api/v1/projects/:uid` — who sees the toolbar. D66.
|
|
133
|
+
*
|
|
134
|
+
* ONE knob, and it is the knob that already existed: `write_mode`. Who can
|
|
135
|
+
* WRITE is who the toolbar is for — showing a comment bar to somebody whose
|
|
136
|
+
* comments would be refused is a form the product fills in and then tears
|
|
137
|
+
* up. `open` = everyone on the allow-listed addresses sees it and may
|
|
138
|
+
* comment; `link-required` = only somebody who opened a review link (and,
|
|
139
|
+
* thereafter, whenever they come back).
|
|
140
|
+
*
|
|
141
|
+
* Choosing `open` here CLEARS the D1 grace timer. The 24-hour downgrade
|
|
142
|
+
* exists so a site somebody set up and forgot does not stay a public comment
|
|
143
|
+
* box; an owner choosing "everyone" in the dashboard is the opposite of
|
|
144
|
+
* forgetting, and having the toolbar silently vanish a day later would be
|
|
145
|
+
* this product's most mysterious bug report.
|
|
146
|
+
*/
|
|
147
|
+
app.patch("/api/v1/projects/:uid", async (c) => {
|
|
148
|
+
const owner = c.get("owner");
|
|
149
|
+
if (!owner)
|
|
150
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
151
|
+
const [project] = await sql `
|
|
152
|
+
SELECT id, org_id FROM projects WHERE uid = ${c.req.param("uid")} AND deleted_at IS NULL
|
|
153
|
+
`;
|
|
154
|
+
if (!project || project.org_id !== owner.orgId) {
|
|
155
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
156
|
+
}
|
|
157
|
+
const body = (await c.req.json().catch(() => null));
|
|
158
|
+
const mode = body?.["writeMode"];
|
|
159
|
+
if (mode !== "open" && mode !== "link-required" && mode !== "verified-only") {
|
|
160
|
+
return fail(c, 400, "validation_failed", "writeMode must be open, link-required or verified-only.");
|
|
161
|
+
}
|
|
162
|
+
await sql.begin(async (tx) => {
|
|
163
|
+
await tx `SELECT set_config('app.org_id', ${owner.orgId}, true)`;
|
|
164
|
+
await tx `
|
|
165
|
+
UPDATE projects
|
|
166
|
+
SET write_mode = ${mode}::snag_write_mode,
|
|
167
|
+
write_mode_downgrade_at = ${mode === "open" ? null : tx `write_mode_downgrade_at`},
|
|
168
|
+
config_version = config_version + 1
|
|
169
|
+
WHERE id = ${project.id}::uuid
|
|
170
|
+
`;
|
|
171
|
+
});
|
|
172
|
+
return c.json({ uid: c.req.param("uid"), writeMode: mode });
|
|
173
|
+
});
|
|
174
|
+
/** The origins on a site — what the dashboard shows beside the script tag. */
|
|
175
|
+
app.get("/api/v1/projects/:uid/origins", async (c) => {
|
|
176
|
+
const owner = c.get("owner");
|
|
177
|
+
if (!owner)
|
|
178
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
179
|
+
const [project] = await sql `
|
|
180
|
+
SELECT id, org_id FROM projects WHERE uid = ${c.req.param("uid")} AND deleted_at IS NULL
|
|
181
|
+
`;
|
|
182
|
+
if (!project || project.org_id !== owner.orgId) {
|
|
183
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
184
|
+
}
|
|
185
|
+
const all = await sql `
|
|
186
|
+
SELECT pattern FROM project_origins WHERE project_id = ${project.id} ORDER BY pattern
|
|
187
|
+
`;
|
|
188
|
+
return c.json({ origins: all.map((r) => r.pattern) });
|
|
189
|
+
});
|
|
190
|
+
return app;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=sites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sites.js","sourceRoot":"","sources":["../../src/routes/sites.ts"],"names":[],"mappings":"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"}
|