@snagset/server 0.1.10 → 0.1.12
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/dist/app.js +23 -1
- package/dist/app.js.map +1 -1
- package/dist/config.generated.js +2 -1
- package/dist/config.generated.js.map +1 -1
- package/dist/db/migrate.js +44 -0
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/migrations/0008_outbox_org_rows.sql +29 -0
- package/dist/db/migrations/0009_member_invites.sql +84 -0
- package/dist/jobs/set.js +15 -4
- package/dist/jobs/set.js.map +1 -1
- package/dist/mail/digest.js +291 -0
- package/dist/mail/digest.js.map +1 -0
- package/dist/mail/links.js +41 -0
- package/dist/mail/links.js.map +1 -0
- package/dist/mail/notify.js +126 -25
- package/dist/mail/notify.js.map +1 -1
- package/dist/net/publicFetch.js +202 -0
- package/dist/net/publicFetch.js.map +1 -0
- package/dist/routes/commentEdit.js +20 -5
- package/dist/routes/commentEdit.js.map +1 -1
- package/dist/routes/imageProxy.js +77 -0
- package/dist/routes/imageProxy.js.map +1 -0
- package/dist/routes/invites.js +417 -0
- package/dist/routes/invites.js.map +1 -0
- package/dist/routes/members.js +36 -68
- package/dist/routes/members.js.map +1 -1
- package/dist/routes/owner.js +5 -0
- package/dist/routes/owner.js.map +1 -1
- package/dist/routes/threads.js +182 -42
- package/dist/routes/threads.js.map +1 -1
- package/package.json +3 -3
- package/static/dashboard/assets/CompositeItem-CYCgjQYA.js +1 -0
- package/static/dashboard/assets/{ConfirmDialog-eCzw7vjK.js → ConfirmDialog-ChIOOj7I.js} +1 -1
- package/static/dashboard/assets/{CopyButton-lcjux1TA.js → CopyButton-V7OJiMLP.js} +1 -1
- package/static/dashboard/assets/{FieldItemContext-i5zdyQYl.js → FieldItemContext-DVtdzu3C.js} +1 -1
- package/static/dashboard/assets/{InstallTag-Eqs3RM9e.js → InstallTag-OD-dW_hC.js} +1 -1
- package/static/dashboard/assets/{SaveState-Cf_bnJmg.js → SaveState-JVgANqCr.js} +1 -1
- package/static/dashboard/assets/{_app-CuKLSKZ2.js → _app-B0TJyHg-.js} +1 -1
- package/static/dashboard/assets/_app-Bwss1pXe.js +56 -0
- package/static/dashboard/assets/activity-CbcgWM3L.js +1 -0
- package/static/dashboard/assets/{arrow-left-CQUHEFUP.js → arrow-left-BrmSwEeI.js} +1 -1
- package/static/dashboard/assets/{avatar-CWT37Ivf.js → avatar-CwD1_B-n.js} +1 -1
- package/static/dashboard/assets/{badge-thtKAJ0h.js → badge-DM_IoUbC.js} +1 -1
- package/static/dashboard/assets/{card-CsXE7vKD.js → card-DpmJQuox.js} +1 -1
- package/static/dashboard/assets/{check-Bc_1fmPx.js → check-DNKwULUa.js} +1 -1
- package/static/dashboard/assets/{chevron-right-neQQkaOt.js → chevron-right-ob3VZye2.js} +1 -1
- package/static/dashboard/assets/{copy-O3USBatC.js → copy-CyBHfvzN.js} +1 -1
- package/static/dashboard/assets/{dialog-MC-TOvQV.js → dialog-Bys-LJLw.js} +2 -2
- package/static/dashboard/assets/{dropdown-menu-C2ZhmONu.js → dropdown-menu-Cqas_wS3.js} +2 -2
- package/static/dashboard/assets/{image-off-Bu6l8gP5.js → image-off-D_65_17F.js} +1 -1
- package/static/dashboard/assets/index-Cxnlb88o.js +169 -0
- package/static/dashboard/assets/index-Ddi7bl87.css +1 -0
- package/static/dashboard/assets/invite-Cc7l9fdY.js +1 -0
- package/static/dashboard/assets/label-CwqSv8L3.js +1 -0
- package/static/dashboard/assets/{layout-grid-rYWIGaIJ.js → layout-grid-PTAyhqU8.js} +1 -1
- package/static/dashboard/assets/me-kMczOn0k.js +1 -0
- package/static/dashboard/assets/{parts-kPNZVO9H.js → parts-CErghVAB.js} +1 -1
- package/static/dashboard/assets/{plus-CcJ1Zwa7.js → plus-BE-gOo3v.js} +1 -1
- package/static/dashboard/assets/s._site-BBRI74gv.js +1 -0
- package/static/dashboard/assets/s._site.comments-CbfjF9-s.js +54 -0
- package/static/dashboard/assets/{s._site.reviewers-Djq2t6Dx.js → s._site.reviewers-BMiqVuRm.js} +1 -1
- package/static/dashboard/assets/s._site.settings-CEvRwMQt.js +1 -0
- package/static/dashboard/assets/{setup-D748SEIm.js → setup-CkNn_rwp.js} +1 -1
- package/static/dashboard/assets/{sign-in-CKSiZWCo.js → sign-in-Bz6iRxcc.js} +1 -1
- package/static/dashboard/assets/{sign-up-BX9j_Ya2.js → sign-up-QcstGuaV.js} +1 -1
- package/static/dashboard/assets/sites-DJjGT-f2.js +1 -0
- package/static/dashboard/assets/studio-mKw0u2SL.js +1 -0
- package/static/dashboard/assets/studio-qqUvuQo5.js +1 -0
- package/static/dashboard/assets/studio.index-zjJeqf8B.js +1 -0
- package/static/dashboard/assets/studio.integrations-C9-R6CQZ.js +1 -0
- package/static/dashboard/assets/studio.members-BYd5gcvp.js +6 -0
- package/static/dashboard/assets/{sun-CWPmS6_O.js → sun-DTQiNbTt.js} +1 -1
- package/static/dashboard/assets/switch-Cqqz6oQN.js +1 -0
- package/static/dashboard/assets/{table-CiZO2uuW.js → table-D_36h1Oq.js} +1 -1
- package/static/dashboard/assets/{useAriaLabelledBy-oZFqC809.js → useAriaLabelledBy-D-dab2TD.js} +1 -1
- package/static/dashboard/assets/{useBaseQuery-CHgLQD-M.js → useBaseQuery-DmDP3Pxx.js} +1 -1
- package/static/dashboard/assets/useQuery-CKn14AcE.js +1 -0
- package/static/dashboard/assets/useRender-B_JrSXOO.js +1 -0
- package/static/dashboard/assets/{useValueChanged-Dvr7PvAV.js → useValueChanged--Isnj6ZH.js} +1 -1
- package/static/dashboard/assets/{users-Ci-ogubz.js → users-CJVqlkra.js} +1 -1
- package/static/dashboard/assets/{welcome-BDWw5Qlv.js → welcome-BMr4vd5V.js} +2 -2
- package/static/dashboard/assets/{you-DAUNHAFI.js → you-Bqw_r3i7.js} +1 -1
- package/static/dashboard/assets/{zod-CtjFmGX5.js → zod-BMw0fS-7.js} +1 -1
- package/static/dashboard/index.html +2 -2
- package/static/widget/snag.capture.js +1 -1
- package/static/widget/snag.dock.js +1 -1
- package/static/widget/snag.runtime.js +2 -2
- package/static/widget/snag.screenshot.js +1 -1
- package/static/dashboard/assets/CompositeItem-Db_8iHSG.js +0 -1
- package/static/dashboard/assets/_app-qNVG5lsX.js +0 -56
- package/static/dashboard/assets/activity-CQLBFIV4.js +0 -1
- package/static/dashboard/assets/index-BR4Mnw-o.css +0 -1
- package/static/dashboard/assets/index-C6gyyLd5.js +0 -169
- package/static/dashboard/assets/label-BTdAT39L.js +0 -1
- package/static/dashboard/assets/me-CI9giaT9.js +0 -1
- package/static/dashboard/assets/s._site-D0HRtia-.js +0 -1
- package/static/dashboard/assets/s._site.comments-D3ACsr3a.js +0 -39
- package/static/dashboard/assets/s._site.settings-D0b6vJci.js +0 -1
- package/static/dashboard/assets/sites-QwJy87Y8.js +0 -1
- package/static/dashboard/assets/studio-B_ma5JGA.js +0 -1
- package/static/dashboard/assets/studio-HH_J6PPv.js +0 -1
- package/static/dashboard/assets/studio.index-DrtJF7DT.js +0 -1
- package/static/dashboard/assets/studio.integrations-Cv34ukzT.js +0 -1
- package/static/dashboard/assets/studio.members-DTbKcrEI.js +0 -1
- package/static/dashboard/assets/switch-CzijGUed.js +0 -1
- package/static/dashboard/assets/useQuery-C6FyjMS-.js +0 -1
- package/static/dashboard/assets/useRender-D5jZ1wns.js +0 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { randomUUID, randomBytes, createHash, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { ensureStudioReviewers } from "../studioReviewers.js";
|
|
4
|
+
import { hashPassword, setSessionCookies, verifyPassword } from "./owner.js";
|
|
5
|
+
import { LimitReached, planLimitReached, usage } from "../plan.js";
|
|
6
|
+
import { openSession, SESSION_S } from "../orgs.js";
|
|
7
|
+
import { inOrg, maintenance } from "../db/scope.js";
|
|
8
|
+
import { isConfigured } from "../mail/index.js";
|
|
9
|
+
/** Seven days, as the request asked. A constant rather than a variable: nobody has asked to tune it, and a setting nobody tunes is a row in 08 §4 for nothing. */
|
|
10
|
+
export const INVITE_TTL_DAYS = 7;
|
|
11
|
+
const DAY_MS = 86_400_000;
|
|
12
|
+
/**
|
|
13
|
+
* The public routes' budget: twenty an hour per address, lookups and accepts
|
|
14
|
+
* together — enough for a person who mistypes a password a few times and
|
|
15
|
+
* reloads the page, nowhere near enough to farm scrypt verifications. In
|
|
16
|
+
* process memory, as `/auth/register`'s ten-an-hour is (orgs.ts says why that
|
|
17
|
+
* is a weaker limit on serverless rather than a missing one).
|
|
18
|
+
*/
|
|
19
|
+
const WINDOW_MS = 60 * 60 * 1000;
|
|
20
|
+
const PER_WINDOW = 20;
|
|
21
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
22
|
+
/** Bare uuids on the wire, as the agent tokens' (mcpTokens.ts): foundations §2.10's prefixes are for ids that travel in webhooks and issue markers, and an invite never leaves the dashboard. */
|
|
23
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
24
|
+
const sha256 = (s) => createHash("sha256").update(s, "utf8").digest();
|
|
25
|
+
function fail(c, status, code, message, details) {
|
|
26
|
+
return c.json({ error: { code, message, ...(details ? { details } : {}), requestId: c.get("requestId") } }, status);
|
|
27
|
+
}
|
|
28
|
+
/** The link. One function, so the dialog and the mail cannot point at different places. */
|
|
29
|
+
export function inviteUrl(base, token) {
|
|
30
|
+
return `${base.replace(/\/+$/, "")}/invite#${token}`;
|
|
31
|
+
}
|
|
32
|
+
/** D85's rule, said the same way members.ts says it. */
|
|
33
|
+
function parseRole(v) {
|
|
34
|
+
if (v !== "owner" && v !== "admin") {
|
|
35
|
+
return v === "member" || v === "viewer"
|
|
36
|
+
? `"${v}" is not available yet — a read-only seat needs visibility rules that are not built. Use owner or admin.`
|
|
37
|
+
: "role must be owner or admin.";
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
/** Never the token, never its hash. */
|
|
42
|
+
const shape = (r) => ({
|
|
43
|
+
id: r.id,
|
|
44
|
+
role: r.role,
|
|
45
|
+
email: r.email,
|
|
46
|
+
createdAt: r.created_at.toISOString(),
|
|
47
|
+
expiresAt: r.expires_at.toISOString(),
|
|
48
|
+
createdBy: r.creator_email ? { name: r.creator_name ?? "", email: r.creator_email } : null,
|
|
49
|
+
});
|
|
50
|
+
function gone(r, at) {
|
|
51
|
+
if (r.accepted_at)
|
|
52
|
+
return "invite_used";
|
|
53
|
+
if (r.revoked_at)
|
|
54
|
+
return "invite_revoked";
|
|
55
|
+
if (r.expires_at <= at)
|
|
56
|
+
return "invite_expired";
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const SENTENCE = {
|
|
60
|
+
invite_not_found: () => "This invite link is not valid. Check that you copied all of it, or ask for a new one.",
|
|
61
|
+
invite_used: () => "This invite has already been used. If that was you, sign in instead.",
|
|
62
|
+
invite_revoked: () => "This invite was withdrawn. Ask whoever sent it for a new one.",
|
|
63
|
+
invite_expired: (r) => `This invite expired${r ? ` on ${r.expires_at.toISOString().slice(0, 10)}` : ""}. Ask whoever sent it for a new one.`,
|
|
64
|
+
};
|
|
65
|
+
/** 404 for a token nobody minted; 410 for one that existed and no longer works — the client can tell "typo" from "too late". */
|
|
66
|
+
function refuse(c, why, r) {
|
|
67
|
+
return fail(c, why === "invite_not_found" ? 404 : 410, why, SENTENCE[why](r));
|
|
68
|
+
}
|
|
69
|
+
/** Raised inside the accept transaction; everything before it rolls back with it. */
|
|
70
|
+
class Refused extends Error {
|
|
71
|
+
status;
|
|
72
|
+
code;
|
|
73
|
+
constructor(status, code, message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.status = status;
|
|
76
|
+
this.code = code;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function inviteRoutes(deps) {
|
|
80
|
+
const { sql } = deps;
|
|
81
|
+
const now = deps.now ?? (() => Date.now());
|
|
82
|
+
const app = new Hono();
|
|
83
|
+
const attempts = new Map();
|
|
84
|
+
/** orgs.ts's limiter, the same shape: counted before any work, so a flood costs a Map lookup. */
|
|
85
|
+
const overLimit = (c) => {
|
|
86
|
+
const ip = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip")?.trim() || "local";
|
|
87
|
+
const t = now();
|
|
88
|
+
const recent = (attempts.get(ip) ?? []).filter((at) => t - at < WINDOW_MS);
|
|
89
|
+
recent.push(t);
|
|
90
|
+
attempts.set(ip, recent);
|
|
91
|
+
if (attempts.size > 10_000)
|
|
92
|
+
attempts.clear();
|
|
93
|
+
return recent.length > PER_WINDOW;
|
|
94
|
+
};
|
|
95
|
+
/** `/auth/login`'s origin rule: an accept signs a browser in, so a cross-site page must not be able to make one. */
|
|
96
|
+
const foreign = (c) => {
|
|
97
|
+
const origin = c.req.header("Origin");
|
|
98
|
+
const host = c.req.header("Host");
|
|
99
|
+
const ours = new Set([deps.origin, host ? `http://${host}` : "", host ? `https://${host}` : ""]);
|
|
100
|
+
return !!origin && !ours.has(origin);
|
|
101
|
+
};
|
|
102
|
+
const mustBeOwner = (c) => {
|
|
103
|
+
const owner = c.get("owner");
|
|
104
|
+
if (!owner)
|
|
105
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
106
|
+
if (owner.role !== "owner")
|
|
107
|
+
return fail(c, 403, "forbidden", "Only an owner can change who is in the studio.");
|
|
108
|
+
return owner;
|
|
109
|
+
};
|
|
110
|
+
const canMail = async () => isConfigured(await deps.mailer().catch(() => ({ kind: "none", reason: "" })));
|
|
111
|
+
const pending = (orgId) => inOrg(sql, orgId, (tx) => tx `
|
|
112
|
+
SELECT i.id, i.role, i.email, i.created_at, i.expires_at, u.name AS creator_name, u.email AS creator_email
|
|
113
|
+
FROM member_invites i
|
|
114
|
+
LEFT JOIN users u ON u.id = i.created_by
|
|
115
|
+
WHERE i.org_id = ${orgId}
|
|
116
|
+
AND i.accepted_at IS NULL AND i.revoked_at IS NULL AND i.expires_at > ${new Date(now())}
|
|
117
|
+
ORDER BY i.created_at DESC
|
|
118
|
+
`);
|
|
119
|
+
// ── The studio's side ───────────────────────────────────────────────────
|
|
120
|
+
app.get("/api/v1/members/invites", async (c) => {
|
|
121
|
+
const owner = c.get("owner");
|
|
122
|
+
if (!owner)
|
|
123
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
124
|
+
// `mail` rides along so the dialog knows whether to offer sending before
|
|
125
|
+
// it is asked — the same answer Studio › General's mail row gives.
|
|
126
|
+
return c.json({ data: (await pending(owner.orgId)).map(shape), mail: await canMail() });
|
|
127
|
+
});
|
|
128
|
+
app.post("/api/v1/members/invites", async (c) => {
|
|
129
|
+
const owner = mustBeOwner(c);
|
|
130
|
+
if (owner instanceof Response)
|
|
131
|
+
return owner;
|
|
132
|
+
const body = (await c.req.json().catch(() => null));
|
|
133
|
+
const role = parseRole(body?.role);
|
|
134
|
+
if (role !== "owner" && role !== "admin")
|
|
135
|
+
return fail(c, 400, "validation_failed", role);
|
|
136
|
+
const raw = typeof body?.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
137
|
+
if (raw && !EMAIL.test(raw))
|
|
138
|
+
return fail(c, 400, "validation_failed", "That does not look like an email address.", { field: "email" });
|
|
139
|
+
const email = raw || null;
|
|
140
|
+
// A full studio is told now, not after the invitee has typed a password.
|
|
141
|
+
// The accept counts again inside its own transaction, which is the check
|
|
142
|
+
// that holds; this one only saves somebody a wasted link.
|
|
143
|
+
const cap = (await deps.limits(owner.orgId)).members;
|
|
144
|
+
const already = await inOrg(sql, owner.orgId, async (tx) => {
|
|
145
|
+
if (cap !== null) {
|
|
146
|
+
const used = (await usage(tx, owner.orgId)).members;
|
|
147
|
+
if (used >= cap)
|
|
148
|
+
throw new LimitReached("members", cap, used);
|
|
149
|
+
}
|
|
150
|
+
if (!email)
|
|
151
|
+
return false;
|
|
152
|
+
const [hit] = await tx `
|
|
153
|
+
SELECT count(*)::int AS n FROM memberships m JOIN users u ON u.id = m.user_id AND u.deleted_at IS NULL
|
|
154
|
+
WHERE m.org_id = ${owner.orgId} AND u.email = ${email}
|
|
155
|
+
`;
|
|
156
|
+
return (hit?.n ?? 0) > 0;
|
|
157
|
+
}).catch((e) => {
|
|
158
|
+
if (e instanceof LimitReached)
|
|
159
|
+
return e;
|
|
160
|
+
throw e;
|
|
161
|
+
});
|
|
162
|
+
if (already instanceof LimitReached)
|
|
163
|
+
return planLimitReached(c, already);
|
|
164
|
+
if (already)
|
|
165
|
+
return fail(c, 409, "already_a_member", "Somebody with that email is already in the studio.", { field: "email" });
|
|
166
|
+
const token = randomBytes(32).toString("base64url");
|
|
167
|
+
const id = randomUUID();
|
|
168
|
+
const expires = new Date(now() + INVITE_TTL_DAYS * DAY_MS);
|
|
169
|
+
await inOrg(sql, owner.orgId, (tx) => tx `
|
|
170
|
+
INSERT INTO member_invites (id, org_id, token_hash, role, email, created_by, expires_at)
|
|
171
|
+
VALUES (${id}, ${owner.orgId}, ${sha256(token)}, ${role}::snag_member_role, ${email}, ${owner.userId}, ${expires})
|
|
172
|
+
`);
|
|
173
|
+
return c.json({
|
|
174
|
+
invite: shape({ id, role, email, created_at: new Date(now()), expires_at: expires, creator_name: owner.name, creator_email: owner.email }),
|
|
175
|
+
// The one and only time, as with a review link.
|
|
176
|
+
token,
|
|
177
|
+
url: inviteUrl(await deps.address(), token),
|
|
178
|
+
mail: await canMail(),
|
|
179
|
+
}, 201);
|
|
180
|
+
});
|
|
181
|
+
app.delete("/api/v1/members/invites/:id", async (c) => {
|
|
182
|
+
const owner = mustBeOwner(c);
|
|
183
|
+
if (owner instanceof Response)
|
|
184
|
+
return owner;
|
|
185
|
+
const id = c.req.param("id");
|
|
186
|
+
if (!UUID.test(id))
|
|
187
|
+
return fail(c, 404, "not_found", "No pending invite with that id.");
|
|
188
|
+
// Org-scoped in the WHERE as well as by the claim: on a role that
|
|
189
|
+
// bypasses RLS (every dev machine) the claim alone filters nothing.
|
|
190
|
+
const [hit] = await inOrg(sql, owner.orgId, (tx) => tx `
|
|
191
|
+
UPDATE member_invites SET revoked_at = now()
|
|
192
|
+
WHERE id = ${id} AND org_id = ${owner.orgId} AND accepted_at IS NULL AND revoked_at IS NULL
|
|
193
|
+
RETURNING id
|
|
194
|
+
`);
|
|
195
|
+
if (!hit)
|
|
196
|
+
return fail(c, 404, "not_found", "No pending invite with that id.");
|
|
197
|
+
return c.json({ data: (await pending(owner.orgId)).map(shape) });
|
|
198
|
+
});
|
|
199
|
+
/**
|
|
200
|
+
* Mail the link. SENT HERE, NOT QUEUED, and it has to be: an outbox row
|
|
201
|
+
* would hold the raw link in `outbox.event` until it was delivered and
|
|
202
|
+
* after (done rows stay), which is the copy of the token at rest this
|
|
203
|
+
* whole table was shaped to avoid. So it goes the way Studio › General's
|
|
204
|
+
* test message goes — inline, to a person who is watching, with the
|
|
205
|
+
* provider's own sentence when it fails — and the link in the dialog works
|
|
206
|
+
* whether or not it did.
|
|
207
|
+
*
|
|
208
|
+
* The caller presents the token it was shown, and it must hash to this
|
|
209
|
+
* row: the server cannot rebuild a link it never kept, and taking a token
|
|
210
|
+
* on trust would let an owner mail a link to an invite it does not name.
|
|
211
|
+
*/
|
|
212
|
+
app.post("/api/v1/members/invites/:id/send", async (c) => {
|
|
213
|
+
const owner = mustBeOwner(c);
|
|
214
|
+
if (owner instanceof Response)
|
|
215
|
+
return owner;
|
|
216
|
+
const id = c.req.param("id");
|
|
217
|
+
const body = (await c.req.json().catch(() => null));
|
|
218
|
+
const token = typeof body?.token === "string" ? body.token : "";
|
|
219
|
+
if (!UUID.test(id))
|
|
220
|
+
return fail(c, 404, "not_found", "No pending invite with that id.");
|
|
221
|
+
const [row] = await inOrg(sql, owner.orgId, (tx) => tx `
|
|
222
|
+
SELECT i.token_hash, i.email, i.role, i.expires_at, i.accepted_at, i.revoked_at, o.name AS org_name
|
|
223
|
+
FROM member_invites i JOIN orgs o ON o.id = i.org_id
|
|
224
|
+
WHERE i.id = ${id} AND i.org_id = ${owner.orgId}
|
|
225
|
+
`);
|
|
226
|
+
if (!row || gone(row, new Date(now())))
|
|
227
|
+
return fail(c, 404, "not_found", "No pending invite with that id.");
|
|
228
|
+
const want = sha256(token);
|
|
229
|
+
if (!token || !timingSafeEqual(want, row.token_hash)) {
|
|
230
|
+
return fail(c, 403, "forbidden", "That is not this invite's link. Copy it from the invite you just made.");
|
|
231
|
+
}
|
|
232
|
+
if (!row.email)
|
|
233
|
+
return fail(c, 400, "validation_failed", "This invite has no email address to send it to. Copy the link instead.");
|
|
234
|
+
const mailer = await deps.mailer();
|
|
235
|
+
if (!isConfigured(mailer))
|
|
236
|
+
return fail(c, 409, "integration_not_configured", "No mail transport is connected yet.");
|
|
237
|
+
const url = inviteUrl(await deps.address(), token);
|
|
238
|
+
const who = owner.name || owner.email;
|
|
239
|
+
const studio = row.org_name || "a studio";
|
|
240
|
+
try {
|
|
241
|
+
await mailer.send({
|
|
242
|
+
to: row.email,
|
|
243
|
+
subject: `${who} invited you to ${studio}`,
|
|
244
|
+
text: `${who} has invited you to join ${studio} as ${row.role === "owner" ? "an owner" : "an admin"}.\n\n` +
|
|
245
|
+
`Open this link to choose your password and sign in:\n${url}\n\n` +
|
|
246
|
+
`It works once and expires on ${row.expires_at.toISOString().slice(0, 10)}. ` +
|
|
247
|
+
`If you were not expecting it, you can ignore this email.\n`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
return fail(c, 502, "mail_send_failed", e instanceof Error ? e.message : String(e));
|
|
252
|
+
}
|
|
253
|
+
return c.json({ to: row.email });
|
|
254
|
+
});
|
|
255
|
+
// ── The invitee's side — public, rate-limited, bodies never logged ──────
|
|
256
|
+
app.use("/auth/invites/*", async (c, next) => {
|
|
257
|
+
c.set("noBodyLogging", true);
|
|
258
|
+
await next();
|
|
259
|
+
});
|
|
260
|
+
/** Find a live invite by its token. The maintenance claim: the org is what this is finding out (D56). */
|
|
261
|
+
const byToken = (token) => maintenance(sql, (tx) => tx `
|
|
262
|
+
SELECT i.id, i.org_id, i.role, i.email, i.expires_at, i.accepted_at, i.revoked_at,
|
|
263
|
+
o.name AS org_name, o.slug AS org_slug, u.name AS creator_name, u.email AS creator_email
|
|
264
|
+
FROM member_invites i
|
|
265
|
+
JOIN orgs o ON o.id = i.org_id AND o.deleted_at IS NULL
|
|
266
|
+
LEFT JOIN users u ON u.id = i.created_by
|
|
267
|
+
WHERE i.token_hash = ${sha256(token)}
|
|
268
|
+
`).then((r) => r[0] ?? null);
|
|
269
|
+
app.post("/auth/invites/lookup", async (c) => {
|
|
270
|
+
if (overLimit(c))
|
|
271
|
+
return fail(c, 429, "quota_exceeded", "Too many attempts from this address. Try again in an hour.");
|
|
272
|
+
const body = (await c.req.json().catch(() => null));
|
|
273
|
+
const token = typeof body?.token === "string" ? body.token.trim() : "";
|
|
274
|
+
const inv = token ? await byToken(token) : null;
|
|
275
|
+
if (!inv)
|
|
276
|
+
return refuse(c, "invite_not_found");
|
|
277
|
+
const why = gone(inv, new Date(now()));
|
|
278
|
+
if (why)
|
|
279
|
+
return refuse(c, why, inv);
|
|
280
|
+
return c.json({
|
|
281
|
+
org: { name: inv.org_name },
|
|
282
|
+
role: inv.role,
|
|
283
|
+
// Present only when the invite fixed it; the page then shows it rather than asking.
|
|
284
|
+
email: inv.email,
|
|
285
|
+
invitedBy: inv.creator_email ? inv.creator_name || inv.creator_email : null,
|
|
286
|
+
expiresAt: inv.expires_at.toISOString(),
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
app.post("/auth/invites/accept", async (c) => {
|
|
290
|
+
if (foreign(c))
|
|
291
|
+
return fail(c, 403, "forbidden", "That request came from another site.");
|
|
292
|
+
if (overLimit(c))
|
|
293
|
+
return fail(c, 429, "quota_exceeded", "Too many attempts from this address. Try again in an hour.");
|
|
294
|
+
const body = (await c.req.json().catch(() => null));
|
|
295
|
+
const str = (k) => (typeof body?.[k] === "string" ? body[k].trim() : "");
|
|
296
|
+
const token = str("token");
|
|
297
|
+
const name = str("name").slice(0, 120);
|
|
298
|
+
const password = typeof body?.password === "string" ? body.password : "";
|
|
299
|
+
const inv = token ? await byToken(token) : null;
|
|
300
|
+
if (!inv)
|
|
301
|
+
return refuse(c, "invite_not_found");
|
|
302
|
+
const early = gone(inv, new Date(now()));
|
|
303
|
+
if (early)
|
|
304
|
+
return refuse(c, early, inv);
|
|
305
|
+
const typed = str("email").toLowerCase();
|
|
306
|
+
if (inv.email && typed && typed !== inv.email) {
|
|
307
|
+
return fail(c, 403, "invite_email_mismatch", `This invite is for ${inv.email}. Join with that address, or ask for an invite to yours.`, { field: "email" });
|
|
308
|
+
}
|
|
309
|
+
const email = inv.email ?? typed;
|
|
310
|
+
if (!EMAIL.test(email))
|
|
311
|
+
return fail(c, 400, "validation_failed", "That does not look like an email address.", { field: "email" });
|
|
312
|
+
// Who this is, read before the transaction so the expensive half of the
|
|
313
|
+
// work — scrypt, a tenth of a second by design — is not done holding a
|
|
314
|
+
// row lock that a racing accept is waiting on.
|
|
315
|
+
const [existing] = await maintenance(sql, (tx) => tx `
|
|
316
|
+
SELECT u.id, u.name, u.password_hash,
|
|
317
|
+
EXISTS (SELECT 1 FROM memberships m WHERE m.user_id = u.id AND m.org_id = ${inv.org_id}) AS member
|
|
318
|
+
FROM users u WHERE u.email = ${email} AND u.deleted_at IS NULL
|
|
319
|
+
`);
|
|
320
|
+
if (existing?.member) {
|
|
321
|
+
return fail(c, 409, "already_a_member", `${email} is already in ${inv.org_name || "this studio"}. Sign in instead.`, { field: "email" });
|
|
322
|
+
}
|
|
323
|
+
let passwordHash = null;
|
|
324
|
+
if (existing) {
|
|
325
|
+
// An account on this instance already: the invite adds a studio to it,
|
|
326
|
+
// and its own password is what proves the holder is its owner.
|
|
327
|
+
if (!verifyPassword(password, existing.password_hash)) {
|
|
328
|
+
return fail(c, 409, "account_exists", `There is already an account for ${email} on this instance. Enter its password to join ${inv.org_name || "the studio"} with it.`, { field: "password" });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// 05 §4.7: twelve, no composition rules — said as setup and sign-up say it.
|
|
333
|
+
if (password.length < 12)
|
|
334
|
+
return fail(c, 400, "validation_failed", "The password must be at least 12 characters. A short phrase of real words is fine.", { field: "password" });
|
|
335
|
+
passwordHash = hashPassword(password);
|
|
336
|
+
}
|
|
337
|
+
const cap = (await deps.limits(inv.org_id)).members;
|
|
338
|
+
let out;
|
|
339
|
+
try {
|
|
340
|
+
out = await sql.begin(async (tx) => {
|
|
341
|
+
// The maintenance claim for the consume (found by hash, cross-tenant
|
|
342
|
+
// by nature) and for a new users row, which no membership makes
|
|
343
|
+
// visible yet — the reason setup and members.ts took it too.
|
|
344
|
+
await tx `SET LOCAL app.maintenance = 'on'`;
|
|
345
|
+
await tx `SELECT set_config('app.org_id', ${inv.org_id}, true)`;
|
|
346
|
+
// THE consume. One statement on the whole pending predicate: a racing
|
|
347
|
+
// accept blocks on this row's lock, re-reads it once we commit, and
|
|
348
|
+
// finds accepted_at set.
|
|
349
|
+
const [won] = await tx `
|
|
350
|
+
UPDATE member_invites SET accepted_at = now()
|
|
351
|
+
WHERE id = ${inv.id} AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > now()
|
|
352
|
+
RETURNING id
|
|
353
|
+
`;
|
|
354
|
+
if (!won) {
|
|
355
|
+
const [again] = await tx `
|
|
356
|
+
SELECT accepted_at, revoked_at, expires_at FROM member_invites WHERE id = ${inv.id}
|
|
357
|
+
`;
|
|
358
|
+
const why = again ? gone(again, new Date()) ?? "invite_used" : "invite_used";
|
|
359
|
+
throw new Refused(410, why, SENTENCE[why](again));
|
|
360
|
+
}
|
|
361
|
+
if (cap !== null) {
|
|
362
|
+
const used = (await usage(tx, inv.org_id)).members;
|
|
363
|
+
if (used >= cap)
|
|
364
|
+
throw new LimitReached("members", cap, used);
|
|
365
|
+
}
|
|
366
|
+
const userId = existing?.id ?? randomUUID();
|
|
367
|
+
if (!existing) {
|
|
368
|
+
// The invite's org is the HOME org (§3.0). email_verified only when
|
|
369
|
+
// the owner named the address: an open invite's address is the
|
|
370
|
+
// invitee's own say-so, which is all the column has ever meant.
|
|
371
|
+
await tx `
|
|
372
|
+
INSERT INTO users (id, org_id, email, name, password_hash, email_verified)
|
|
373
|
+
VALUES (${userId}, ${inv.org_id}, ${email}, ${name}, ${passwordHash}, ${inv.email !== null})
|
|
374
|
+
`;
|
|
375
|
+
}
|
|
376
|
+
await tx `INSERT INTO memberships (org_id, user_id, role) VALUES (${inv.org_id}, ${userId}, ${inv.role}::snag_member_role)`;
|
|
377
|
+
await tx `UPDATE member_invites SET accepted_by = ${userId} WHERE id = ${inv.id}`;
|
|
378
|
+
// 05 §6.4b: assignable in every site from the moment they join. A
|
|
379
|
+
// person who was in this studio before and was removed kept their
|
|
380
|
+
// reviewer rows, blocked (members.ts); coming back unblocks them.
|
|
381
|
+
await ensureStudioReviewers(tx, { orgId: inv.org_id, userId });
|
|
382
|
+
if (existing) {
|
|
383
|
+
await tx `UPDATE reviewers SET blocked_at = NULL WHERE org_id = ${inv.org_id} AND user_id = ${userId} AND blocked_at IS NOT NULL`;
|
|
384
|
+
}
|
|
385
|
+
// Signed in exactly as `/auth/login` signs in: a user_sessions row is
|
|
386
|
+
// the authority, the raw token goes into the cookie once.
|
|
387
|
+
const session = await openSession(tx, { orgId: inv.org_id, userId, userAgent: c.req.header("User-Agent") ?? "" });
|
|
388
|
+
return { userId, userName: existing?.name ?? name, session };
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
if (e instanceof Refused)
|
|
393
|
+
return fail(c, e.status, e.code, e.message);
|
|
394
|
+
if (e instanceof LimitReached)
|
|
395
|
+
return planLimitReached(c, e);
|
|
396
|
+
const pg = e;
|
|
397
|
+
if (pg?.code === "23505") {
|
|
398
|
+
// Lost a race to the same address: a sign-up, another invite, or
|
|
399
|
+
// this one's twin with a different token. Said as the sentence the
|
|
400
|
+
// slower request would have got had it arrived a moment later.
|
|
401
|
+
return pg.constraint_name === "memberships_pkey"
|
|
402
|
+
? fail(c, 409, "already_a_member", `${email} is already in ${inv.org_name || "this studio"}. Sign in instead.`, { field: "email" })
|
|
403
|
+
: fail(c, 409, "account_exists", `There is already an account for ${email} on this instance. Enter its password to join with it.`, { field: "password" });
|
|
404
|
+
}
|
|
405
|
+
throw e;
|
|
406
|
+
}
|
|
407
|
+
setSessionCookies(c, out.session.token, out.session.csrf, deps.secure, SESSION_S);
|
|
408
|
+
return c.json({
|
|
409
|
+
org: { name: inv.org_name, slug: inv.org_slug },
|
|
410
|
+
user: { email, name: out.userName },
|
|
411
|
+
role: inv.role,
|
|
412
|
+
csrfToken: out.session.csrf,
|
|
413
|
+
}, 201);
|
|
414
|
+
});
|
|
415
|
+
return app;
|
|
416
|
+
}
|
|
417
|
+
//# sourceMappingURL=invites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invites.js","sourceRoot":"","sources":["../../src/routes/invites.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/members.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
3
|
-
import { ensureStudioReviewers } from "../studioReviewers.js";
|
|
4
|
-
import { hashPassword } from "./owner.js";
|
|
5
|
-
import { LimitReached, planLimitReached, usage } from "../plan.js";
|
|
2
|
+
import { maintenance } from "../db/scope.js";
|
|
6
3
|
function fail(c, status, code, message) {
|
|
7
4
|
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
8
5
|
}
|
|
@@ -51,63 +48,12 @@ export function memberRoutes(deps) {
|
|
|
51
48
|
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
52
49
|
return c.json({ data: await list(owner.orgId, owner.userId) });
|
|
53
50
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const name = typeof body?.name === "string" ? body.name.trim().slice(0, 120) : "";
|
|
61
|
-
const password = typeof body?.password === "string" ? body.password : "";
|
|
62
|
-
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
|
63
|
-
return fail(c, 400, "validation_failed", "That does not look like an email address.");
|
|
64
|
-
const role = parseRole(body?.role);
|
|
65
|
-
if (!WIRED.includes(role))
|
|
66
|
-
return fail(c, 400, "validation_failed", role);
|
|
67
|
-
// 05 §4.7: minimum 12, no composition rules — the same rule setup and
|
|
68
|
-
// `passwd` apply, stated here before the server has to refuse it.
|
|
69
|
-
if (password.length < 12)
|
|
70
|
-
return fail(c, 400, "validation_failed", "The initial password needs at least 12 characters. A short phrase of real words is fine.");
|
|
71
|
-
const userId = randomUUID();
|
|
72
|
-
// The plan's number (plan/09), read before the transaction; counted inside it.
|
|
73
|
-
const cap = (await deps.limits(owner.orgId)).members;
|
|
74
|
-
try {
|
|
75
|
-
await sql.begin(async (tx) => {
|
|
76
|
-
if (cap !== null) {
|
|
77
|
-
await tx `SELECT set_config('app.org_id', ${owner.orgId}, true)`;
|
|
78
|
-
const used = (await usage(tx, owner.orgId)).members;
|
|
79
|
-
if (used >= cap)
|
|
80
|
-
throw new LimitReached("members", cap, used);
|
|
81
|
-
}
|
|
82
|
-
// The maintenance claim (0002, D56) for the users INSERT: a user with
|
|
83
|
-
// no membership row yet is invisible to users_org, so under a role
|
|
84
|
-
// that honours RLS the insert would fail its own WITH CHECK. Setup
|
|
85
|
-
// and boot take the same claim for the same row. The membership then
|
|
86
|
-
// goes in under the org scope like any other write.
|
|
87
|
-
await tx `SET LOCAL app.maintenance = 'on'`;
|
|
88
|
-
await tx `SELECT set_config('app.org_id', ${owner.orgId}, true)`;
|
|
89
|
-
await tx `
|
|
90
|
-
INSERT INTO users (id, org_id, email, name, password_hash, email_verified)
|
|
91
|
-
VALUES (${userId}, ${owner.orgId}, ${email}, ${name}, ${hashPassword(password)}, true)
|
|
92
|
-
`;
|
|
93
|
-
await tx `INSERT INTO memberships (org_id, user_id, role) VALUES (${owner.orgId}, ${userId}, ${role}::snag_member_role)`;
|
|
94
|
-
// 05 §6.4b: a member is assignable in every site from the moment
|
|
95
|
-
// they join — the other half of what createProject does.
|
|
96
|
-
await ensureStudioReviewers(tx, { orgId: owner.orgId, userId });
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
catch (e) {
|
|
100
|
-
if (e instanceof LimitReached)
|
|
101
|
-
return planLimitReached(c, e);
|
|
102
|
-
// users_email_uniq — the one collision this route can have, said as a
|
|
103
|
-
// sentence rather than a 500.
|
|
104
|
-
if (e?.code === "23505") {
|
|
105
|
-
return fail(c, 409, "already_a_member", "Somebody with that email is already in the studio.");
|
|
106
|
-
}
|
|
107
|
-
throw e;
|
|
108
|
-
}
|
|
109
|
-
return c.json({ data: await list(owner.orgId, owner.userId) }, 201);
|
|
110
|
-
});
|
|
51
|
+
/**
|
|
52
|
+
* Gone, said rather than 404'd: a script written against D85's
|
|
53
|
+
* create-with-password reads where members come from now, instead of
|
|
54
|
+
* "no route", which reads as a typo.
|
|
55
|
+
*/
|
|
56
|
+
app.post("/api/v1/members", (c) => fail(c, 404, "not_found", "Members join through an invite now: POST /api/v1/members/invites, then send them the link."));
|
|
111
57
|
app.patch("/api/v1/members/:email", async (c) => {
|
|
112
58
|
const owner = mustBeOwner(c);
|
|
113
59
|
if (owner instanceof Response)
|
|
@@ -163,18 +109,40 @@ export function memberRoutes(deps) {
|
|
|
163
109
|
if ((owners?.n ?? 0) <= 1)
|
|
164
110
|
return "last_owner";
|
|
165
111
|
}
|
|
166
|
-
// Soft on the user, hard on the membership: users_email_uniq is partial
|
|
167
|
-
// on deleted_at IS NULL, so the same email can be added again later;
|
|
168
|
-
// every session goes with it, because the session middleware requires
|
|
169
|
-
// a membership row and this is the row.
|
|
170
112
|
// 05 §6.4b: they leave the assignee pool and keep what was already
|
|
171
113
|
// assigned to them — blocked, which is the one mechanism the spec has
|
|
172
114
|
// for that, and never un-assigned, which would rewrite history on
|
|
173
115
|
// threads somebody is mid-conversation on.
|
|
174
116
|
await tx `UPDATE reviewers SET blocked_at = now() WHERE org_id = ${owner.orgId} AND user_id = ${target.user_id} AND blocked_at IS NULL`;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
117
|
+
// THIS STUDIO'S sessions, and the person only if this was their last
|
|
118
|
+
// studio (D121). Before invites could add an existing account to a
|
|
119
|
+
// second studio, every user here had one membership and "remove from
|
|
120
|
+
// the studio" and "delete the account" were the same act; now a person
|
|
121
|
+
// in two studios removed from one keeps the other, and a soft-delete
|
|
122
|
+
// here would have signed them out of a studio that never removed them.
|
|
123
|
+
await tx `UPDATE user_sessions SET revoked_at = now() WHERE user_id = ${target.user_id} AND org_id = ${owner.orgId} AND revoked_at IS NULL`;
|
|
124
|
+
const [other] = await maintenance(tx, (m) => m `
|
|
125
|
+
SELECT org_id FROM memberships WHERE user_id = ${target.user_id} AND org_id <> ${owner.orgId}
|
|
126
|
+
ORDER BY created_at LIMIT 1
|
|
127
|
+
`);
|
|
128
|
+
if (other) {
|
|
129
|
+
// Still somebody's member: re-home them if this was the home org
|
|
130
|
+
// (§3.0), because /auth/login opens a session in the HOME org and a
|
|
131
|
+
// session in a studio with no membership row is refused on arrival —
|
|
132
|
+
// they would be unable to sign in at all. Done before the membership
|
|
133
|
+
// goes, while users_org can still see the row.
|
|
134
|
+
await tx `UPDATE users SET org_id = ${other.org_id}, updated_at = now() WHERE id = ${target.user_id} AND org_id = ${owner.orgId}`;
|
|
135
|
+
await tx `DELETE FROM memberships WHERE org_id = ${owner.orgId} AND user_id = ${target.user_id}`;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Their last studio: soft on the user, hard on the membership, as
|
|
139
|
+
// before. users_email_uniq is partial on deleted_at IS NULL, so the
|
|
140
|
+
// same email can be invited again later; every session goes with it,
|
|
141
|
+
// because the session middleware requires a membership row.
|
|
142
|
+
await tx `UPDATE user_sessions SET revoked_at = now() WHERE user_id = ${target.user_id} AND revoked_at IS NULL`;
|
|
143
|
+
await tx `DELETE FROM memberships WHERE org_id = ${owner.orgId} AND user_id = ${target.user_id}`;
|
|
144
|
+
await tx `UPDATE users SET deleted_at = now(), updated_at = now() WHERE id = ${target.user_id}`;
|
|
145
|
+
}
|
|
178
146
|
return "ok";
|
|
179
147
|
});
|
|
180
148
|
if (result === "missing")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"members.js","sourceRoot":"","sources":["../../src/routes/members.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"members.js","sourceRoot":"","sources":["../../src/routes/members.ts"],"names":[],"mappings":"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"}
|
package/dist/routes/owner.js
CHANGED
|
@@ -5,6 +5,7 @@ import { usage } from "../plan.js";
|
|
|
5
5
|
import { studiosOf } from "../orgs.js";
|
|
6
6
|
import { inOrg, maintenance } from "../db/scope.js";
|
|
7
7
|
import { effectiveWriteMode } from "./sessions.js";
|
|
8
|
+
import { pendingMigrations } from "../db/migrate.js";
|
|
8
9
|
/** 05 §4.7: 30-day absolute, 7-day idle, rolling refresh on use. */
|
|
9
10
|
const ABSOLUTE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
10
11
|
const IDLE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -344,6 +345,10 @@ export function ownerRoutes(deps) {
|
|
|
344
345
|
org: { name: org?.name ?? "", slug: org?.slug ?? "" },
|
|
345
346
|
orgs: studios.map((s) => ({ slug: s.slug, name: s.name, role: s.role, current: s.id === owner.orgId })),
|
|
346
347
|
signupOpen: deps.signupOpen?.() ?? false,
|
|
348
|
+
// The migrations this build ships that the database has not run
|
|
349
|
+
// (db/migrate.ts says why a serverless deploy can be behind). Empty
|
|
350
|
+
// when current; the dashboard shows a banner otherwise.
|
|
351
|
+
pendingMigrations: await pendingMigrations(sql),
|
|
347
352
|
projects: projects.map((p) => ({
|
|
348
353
|
uid: p.uid,
|
|
349
354
|
name: p.name,
|