@snagset/server 0.0.4 → 0.1.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 +4 -3
- package/dist/app.js +82 -9
- package/dist/app.js.map +1 -1
- package/dist/bin.js +85 -11
- package/dist/bin.js.map +1 -1
- package/dist/boot.js +67 -51
- package/dist/boot.js.map +1 -1
- package/dist/config.generated.js +5 -2
- package/dist/config.generated.js.map +1 -1
- package/dist/db/connect.js +7 -0
- package/dist/db/connect.js.map +1 -1
- package/dist/db/migrations/0005_studio_reviewers.sql +45 -0
- package/dist/db/migrations/0006_instance_settings.sql +69 -0
- package/dist/db/migrations/0007_jira_provider.no-txn.sql +13 -0
- package/dist/domain.js +129 -0
- package/dist/domain.js.map +1 -0
- package/dist/instance/settings.js +54 -0
- package/dist/instance/settings.js.map +1 -0
- package/dist/integrations/github.js +112 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/inbound.js +88 -0
- package/dist/integrations/inbound.js.map +1 -0
- package/dist/integrations/jira.js +129 -0
- package/dist/integrations/jira.js.map +1 -0
- package/dist/integrations/linear.js +111 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/outbox.js +305 -0
- package/dist/integrations/outbox.js.map +1 -0
- package/dist/integrations/slack.js +135 -0
- package/dist/integrations/slack.js.map +1 -0
- package/dist/integrations/store.js +101 -0
- package/dist/integrations/store.js.map +1 -0
- package/dist/listen.js +58 -0
- package/dist/listen.js.map +1 -0
- package/dist/mail/index.js +21 -11
- package/dist/mail/index.js.map +1 -1
- package/dist/mail/resolve.js +80 -0
- package/dist/mail/resolve.js.map +1 -0
- package/dist/platform.js +27 -0
- package/dist/platform.js.map +1 -0
- package/dist/projectCache.js +3 -2
- package/dist/projectCache.js.map +1 -1
- package/dist/projects.js +8 -2
- package/dist/projects.js.map +1 -1
- package/dist/routes/account.js +59 -0
- package/dist/routes/account.js.map +1 -0
- package/dist/routes/activity.js +111 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/assignees.js +139 -0
- package/dist/routes/assignees.js.map +1 -0
- package/dist/routes/attachments.js +2 -2
- package/dist/routes/attachments.js.map +1 -1
- package/dist/routes/comments.js +27 -17
- package/dist/routes/comments.js.map +1 -1
- package/dist/routes/cors.js +10 -1
- package/dist/routes/cors.js.map +1 -1
- package/dist/routes/instance.js +164 -0
- package/dist/routes/instance.js.map +1 -0
- package/dist/routes/integrations.js +610 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/jobs.js +15 -3
- package/dist/routes/jobs.js.map +1 -1
- package/dist/routes/mcpTokens.js +119 -0
- package/dist/routes/mcpTokens.js.map +1 -0
- package/dist/routes/members.js +177 -0
- package/dist/routes/members.js.map +1 -0
- package/dist/routes/owner.js +64 -7
- package/dist/routes/owner.js.map +1 -1
- package/dist/routes/sites.js +181 -8
- package/dist/routes/sites.js.map +1 -1
- package/dist/routes/threads.js +276 -9
- package/dist/routes/threads.js.map +1 -1
- package/dist/serverless.js +41 -6
- package/dist/serverless.js.map +1 -1
- package/dist/studioReviewers.js +67 -0
- package/dist/studioReviewers.js.map +1 -0
- package/dist/vendor/adapters/outbound/sanitise.js +18 -3
- package/dist/vendor/types/brief.js +121 -0
- package/package.json +1 -1
- package/static/dashboard/assets/CompositeItem-DnLNS57f.js +1 -0
- package/static/dashboard/assets/ConfirmDialog-CCAJDc_P.js +1 -0
- package/static/dashboard/assets/CopyButton-exuJDdXj.js +1 -0
- package/static/dashboard/assets/FieldItemContext-jf0Is3j7.js +1 -0
- package/static/dashboard/assets/InstallTag-CkeGLk1a.js +1 -0
- package/static/dashboard/assets/SaveState-BsrfwjoH.js +6 -0
- package/static/dashboard/assets/_app-CiDKmy_a.js +1 -0
- package/static/dashboard/assets/_app-xxJ7UG34.js +56 -0
- package/static/dashboard/assets/activity-mwHb7ir7.js +1 -0
- package/static/dashboard/assets/arrow-left-D08BSlJU.js +6 -0
- package/static/dashboard/assets/avatar-BITl2vTh.js +1 -0
- package/static/dashboard/assets/badge-CGEswzew.js +1 -0
- package/static/dashboard/assets/card-8T0wnIiU.js +1 -0
- package/static/dashboard/assets/check-CiLHTeII.js +6 -0
- package/static/dashboard/assets/chevron-right-BokKbPOn.js +6 -0
- package/static/dashboard/assets/copy-CKgsZbj1.js +6 -0
- package/static/dashboard/assets/createBaseUIEventDetails-CEmaecLi.js +1 -0
- package/static/dashboard/assets/createLucideIcon-llnWWh0E.js +36 -0
- package/static/dashboard/assets/dialog-DrrnCoT5.js +6 -0
- package/static/dashboard/assets/dropdown-menu-DIE2xmL5.js +11 -0
- package/static/dashboard/assets/image-off-CgUC8krg.js +6 -0
- package/static/dashboard/assets/index-BRTBtV91.css +1 -0
- package/static/dashboard/assets/index-C87aqO18.js +129 -0
- package/static/dashboard/assets/label-BfMHjFA7.js +1 -0
- package/static/dashboard/assets/labels--UUyt9uB.js +1 -0
- package/static/dashboard/assets/layout-grid-B7mn7n3K.js +6 -0
- package/static/dashboard/assets/me-D1prnPpY.js +1 -0
- package/static/dashboard/assets/parts-B-HQayFH.js +1 -0
- package/static/dashboard/assets/plus-CuMhvZUZ.js +6 -0
- package/static/dashboard/assets/s._site-Ccc7pzT-.js +1 -0
- package/static/dashboard/assets/s._site.comments-nJZLYDQh.js +39 -0
- package/static/dashboard/assets/s._site.reviewers-BmRPiC0a.js +1 -0
- package/static/dashboard/assets/s._site.settings-DNv5ovrJ.js +1 -0
- package/static/dashboard/assets/setup-B_VsIyK2.js +1 -0
- package/static/dashboard/assets/sign-in-sWUNd3Lp.js +1 -0
- package/static/dashboard/assets/sites-CSzUAHV6.js +1 -0
- package/static/dashboard/assets/studio-DVFn025O.js +1 -0
- package/static/dashboard/assets/studio-DzBapfQK.js +1 -0
- package/static/dashboard/assets/studio.index-Cp7lBb8S.js +1 -0
- package/static/dashboard/assets/studio.integrations-DD2EWaL-.js +1 -0
- package/static/dashboard/assets/studio.members-CVu-eP6p.js +1 -0
- package/static/dashboard/assets/sun-DTI4O-GM.js +16 -0
- package/static/dashboard/assets/table-DkqScFZb.js +1 -0
- package/static/dashboard/assets/update-duXzIzFS.js +1 -0
- package/static/dashboard/assets/useBaseQuery-0OqUJMfc.js +1 -0
- package/static/dashboard/assets/useQuery-COJ09EkU.js +1 -0
- package/static/dashboard/assets/useRender-dtgBGlgC.js +1 -0
- package/static/dashboard/assets/users-O11swHBQ.js +6 -0
- package/static/dashboard/assets/welcome-CT6oH0I8.js +11 -0
- package/static/dashboard/assets/you-_yIPqa_9.js +1 -0
- package/static/dashboard/assets/zod-iC-UhR5u.js +1 -0
- package/static/dashboard/index.html +17 -2
- package/static/widget/snag.capture.js +1 -1
- package/static/widget/snag.dock.js +1 -1
- package/static/widget/snag.panels.js +1 -1
- package/static/widget/snag.runtime.js +2 -2
- package/static/widget/snag.text.js +1 -1
- package/vercel.json +4 -0
- package/static/dashboard/assets/index-CATItBzT.js +0 -1
- package/static/dashboard/assets/index-DsUJ96yc.css +0 -1
package/dist/listen.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a failed `listen` should say — one sentence, naming the fix.
|
|
3
|
+
*
|
|
4
|
+
* Found 2026-09-07 by starting a second instance beside the dogfood: Node's
|
|
5
|
+
* own report is an unhandled 'error' event and a stack trace, which reads
|
|
6
|
+
* as a crash in our code when the whole story is "8080 is taken". Every
|
|
7
|
+
* other boot failure here is a sentence with the variable to change; this
|
|
8
|
+
* was the one that was not. `null` means the error is not one we know how
|
|
9
|
+
* to explain, and the caller rethrows it.
|
|
10
|
+
*/
|
|
11
|
+
export function listenFailure(err, port, hostname) {
|
|
12
|
+
const code = err?.code;
|
|
13
|
+
if (code === "EADDRINUSE") {
|
|
14
|
+
return (`✗ port ${port} is already in use on ${hostname} — another Snagset, or something else, is listening there. ` +
|
|
15
|
+
`Stop it, or set SNAGSET_PORT to a free port (and SNAGSET_BASE_URL to match, since review links are built from it).`);
|
|
16
|
+
}
|
|
17
|
+
if (code === "EACCES") {
|
|
18
|
+
return `✗ this user may not listen on port ${port} — ports below 1024 need a privileged user. Set SNAGSET_PORT to a port above 1023.`;
|
|
19
|
+
}
|
|
20
|
+
if (code === "EADDRNOTAVAIL") {
|
|
21
|
+
return `✗ ${hostname} is not an address on this machine — check SNAGSET_BIND_ADDRESS.`;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Roam to the next free port — but only when nothing pinned one. D104.
|
|
27
|
+
*
|
|
28
|
+
* Eurico, 2026-09-07, after a second instance died on 8080: *"can't it just
|
|
29
|
+
* ask if another can be used, like Next does?"* It can, and for a laptop
|
|
30
|
+
* that is the right default. What stops it being the default everywhere is
|
|
31
|
+
* that review links and the claim URL are built from `SNAGSET_BASE_URL`, and
|
|
32
|
+
* a deploy's reverse proxy points at the port the operator chose: a server
|
|
33
|
+
* that quietly moved to 8081 there would report success while the site was
|
|
34
|
+
* down. So the rule is Next's rule. Set neither the port nor the address and
|
|
35
|
+
* the server finds a free port and moves its address to match; set either,
|
|
36
|
+
* and a taken port is the sentence from `listenFailure`, because you chose.
|
|
37
|
+
*/
|
|
38
|
+
export function pinned(env) {
|
|
39
|
+
// `PORT` is the platform's (Railway, Render, Fly) and 08 §4.2 honours it as
|
|
40
|
+
// ours; a platform that set it routes traffic to it, so roaming away from
|
|
41
|
+
// it would be the worst possible answer.
|
|
42
|
+
return !!(env.SNAGSET_PORT || env.PORT || env.SNAGSET_BASE_URL || env.SNAGSET_PUBLIC_URL);
|
|
43
|
+
}
|
|
44
|
+
/** The first port at or after `start` that can be bound on `hostname`, or null after `tries`. */
|
|
45
|
+
export async function freePort(start, hostname, tries = 10) {
|
|
46
|
+
const { createServer } = await import("node:net");
|
|
47
|
+
for (let port = start; port < start + tries; port++) {
|
|
48
|
+
const free = await new Promise((resolve) => {
|
|
49
|
+
const probe = createServer();
|
|
50
|
+
probe.once("error", () => resolve(false));
|
|
51
|
+
probe.listen(port, hostname, () => probe.close(() => resolve(true)));
|
|
52
|
+
});
|
|
53
|
+
if (free)
|
|
54
|
+
return port;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=listen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listen.js","sourceRoot":"","sources":["../src/listen.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,GAAY,EAAE,IAAY,EAAE,QAAgB;IACxE,MAAM,IAAI,GAAI,GAAgC,EAAE,IAAI,CAAC;IACrD,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;QAC1B,OAAO,CACL,UAAU,IAAI,yBAAyB,QAAQ,6DAA6D;YAC5G,oHAAoH,CACrH,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtB,OAAO,sCAAsC,IAAI,oFAAoF,CAAC;IACxI,CAAC;IACD,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;QAC7B,OAAO,KAAK,QAAQ,kEAAkE,CAAC;IACzF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,MAAM,CAAC,GAAuC;IAC5D,4EAA4E;IAC5E,0EAA0E;IAC1E,yCAAyC;IACzC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,gBAAgB,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAC;AAC5F,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAa,EAAE,QAAgB,EAAE,KAAK,GAAG,EAAE;IACxE,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;IAClD,KAAK,IAAI,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,KAAK,GAAG,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,EAAE;YAClD,MAAM,KAAK,GAAG,YAAY,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YAC1C,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvE,CAAC,CAAC,CAAC;QACH,IAAI,IAAI;YAAE,OAAO,IAAI,CAAC;IACxB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/mail/index.js
CHANGED
|
@@ -46,25 +46,35 @@ export function selectMailer(cfg) {
|
|
|
46
46
|
"smtp://user:pass@host:587, together with SNAGSET_MAIL_FROM.",
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
if (!from) {
|
|
50
|
+
throw new Error("SNAGSET_MAIL_URL is set but SNAGSET_MAIL_FROM is not. Every message needs a From " +
|
|
51
|
+
"address, so this would fail on the first send rather than here.");
|
|
52
|
+
}
|
|
53
|
+
return mailerFromUrl(raw, from);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A transport URL and a From address → a mailer, or a sentence. The ONE parse
|
|
57
|
+
* — the environment (`selectMailer`) and the dashboard's stored transport
|
|
58
|
+
* (`resolve.ts`, D89) both come through here, so an unsupported scheme is
|
|
59
|
+
* refused with the same words wherever it was typed.
|
|
60
|
+
*
|
|
61
|
+
* Parsed by hand rather than with `new URL`: a Resend key is the AUTHORITY
|
|
62
|
+
* component of `resend://re_abc123`, and WHATWG URL lower-cases hostnames.
|
|
63
|
+
* An API key is case-sensitive, so `new URL().hostname` would silently
|
|
64
|
+
* corrupt it and produce a 401 that looks like a bad key rather than a bad
|
|
65
|
+
* parser.
|
|
66
|
+
*/
|
|
67
|
+
export function mailerFromUrl(raw, from) {
|
|
54
68
|
const at = raw.indexOf("://");
|
|
55
69
|
const scheme = at === -1 ? "" : raw.slice(0, at).toLowerCase();
|
|
56
70
|
const rest = at === -1 ? "" : raw.slice(at + 3);
|
|
57
71
|
if (!MAIL_SCHEMES.includes(scheme)) {
|
|
58
|
-
throw new Error(`
|
|
72
|
+
throw new Error(`the mail transport has an unsupported scheme ${JSON.stringify(scheme || raw.slice(0, 12))}. ` +
|
|
59
73
|
`This build speaks: ${MAIL_SCHEMES.map((s) => `${s}://`).join(", ")}.`);
|
|
60
74
|
}
|
|
61
|
-
if (!from) {
|
|
62
|
-
throw new Error("SNAGSET_MAIL_URL is set but SNAGSET_MAIL_FROM is not. Every message needs a From " +
|
|
63
|
-
"address, so this would fail on the first send rather than here.");
|
|
64
|
-
}
|
|
65
75
|
if (scheme === "resend") {
|
|
66
76
|
if (!rest)
|
|
67
|
-
throw new Error("
|
|
77
|
+
throw new Error("the mail transport is resend:// with no API key after it.");
|
|
68
78
|
// A trailing slash is what a browser or a copy-paste adds; it is not part
|
|
69
79
|
// of the key, and a key with one on the end fails authentication in a way
|
|
70
80
|
// that reads as "wrong key".
|
package/dist/mail/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mail/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAE1C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,MAAM,EACJ,kFAAkF;gBAClF,yEAAyE;gBACzE,6DAA6D;SAChE,CAAC;IACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mail/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAG7C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAU,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAE1C,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,MAAM,EACJ,kFAAkF;gBAClF,yEAAyE;gBACzE,6DAA6D;SAChE,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,iEAAiE,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,IAAY;IACrD,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/D,MAAM,IAAI,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAEhD,IAAI,CAAE,YAAkC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CACb,gDAAgD,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI;YAC5F,sBAAsB,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACzE,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;QACxF,0EAA0E;QAC1E,0EAA0E;QAC1E,6BAA6B;QAC7B,OAAO,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;IACD,OAAO,gBAAgB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { firstOrgId, readSetting } from "../instance/settings.js";
|
|
2
|
+
import { MAIL_SCHEMES, mailerFromUrl, selectMailer } from "./index.js";
|
|
3
|
+
const TTL_MS = 10_000;
|
|
4
|
+
let cache = null;
|
|
5
|
+
export function invalidateMailer() {
|
|
6
|
+
cache = null;
|
|
7
|
+
}
|
|
8
|
+
/** `resend://<key>` → `{ scheme: 'resend' }`; `smtp://u:p@host:587` → `{ scheme: 'smtp', host: 'host:587' }`. Never the credentials. */
|
|
9
|
+
export function describeMailUrl(raw) {
|
|
10
|
+
const at = raw.indexOf("://");
|
|
11
|
+
const scheme = at === -1 ? "" : raw.slice(0, at).toLowerCase();
|
|
12
|
+
if (scheme === "resend" || at === -1)
|
|
13
|
+
return { scheme };
|
|
14
|
+
const rest = raw.slice(at + 3);
|
|
15
|
+
const hostPart = rest.includes("@") ? rest.slice(rest.lastIndexOf("@") + 1) : rest;
|
|
16
|
+
return { scheme, host: hostPart.split("/")[0] ?? "" };
|
|
17
|
+
}
|
|
18
|
+
export async function resolveMailer(sql, keys, cfg) {
|
|
19
|
+
if (cfg.get("SNAGSET_MAIL_URL")) {
|
|
20
|
+
// The environment: no cache needed, `selectMailer` is a pure parse. It
|
|
21
|
+
// THROWS on a half-set or malformed environment, as boot always has.
|
|
22
|
+
const status = selectMailer(cfg);
|
|
23
|
+
return {
|
|
24
|
+
status,
|
|
25
|
+
source: "env",
|
|
26
|
+
...describeMailUrl(cfg.get("SNAGSET_MAIL_URL")),
|
|
27
|
+
...(cfg.get("SNAGSET_MAIL_FROM") ? { from: cfg.get("SNAGSET_MAIL_FROM") } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (cache && Date.now() - cache.at < TTL_MS)
|
|
31
|
+
return cache.value;
|
|
32
|
+
const none = (reason) => ({ status: { kind: "none", reason }, source: "none" });
|
|
33
|
+
const orgId = await firstOrgId(sql);
|
|
34
|
+
let value;
|
|
35
|
+
if (!orgId) {
|
|
36
|
+
value = none("this instance is not claimed yet, so there is nobody to mail.");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const stored = await readSetting(sql, keys, orgId, "mail");
|
|
40
|
+
if (!stored) {
|
|
41
|
+
value = none("no mail transport is configured, so notifications are off. Connect one on Studio › General, " +
|
|
42
|
+
"or set SNAGSET_MAIL_URL and SNAGSET_MAIL_FROM in the environment.");
|
|
43
|
+
}
|
|
44
|
+
else if (stored.unreadable || !stored.secret) {
|
|
45
|
+
value = {
|
|
46
|
+
status: {
|
|
47
|
+
kind: "none",
|
|
48
|
+
reason: "a mail transport is stored but its secret will not decrypt — almost always a changed SNAGSET_SECRET_KEY (08 §4.7). Connect it again.",
|
|
49
|
+
},
|
|
50
|
+
source: "stored",
|
|
51
|
+
scheme: stored.value.scheme,
|
|
52
|
+
...(stored.value.host ? { host: stored.value.host } : {}),
|
|
53
|
+
from: stored.value.from,
|
|
54
|
+
updatedAt: stored.updatedAt,
|
|
55
|
+
unreadable: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
let status;
|
|
60
|
+
try {
|
|
61
|
+
status = mailerFromUrl(stored.secret, stored.value.from);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
status = { kind: "none", reason: e instanceof Error ? e.message : String(e) };
|
|
65
|
+
}
|
|
66
|
+
value = {
|
|
67
|
+
status,
|
|
68
|
+
source: "stored",
|
|
69
|
+
scheme: stored.value.scheme,
|
|
70
|
+
...(stored.value.host ? { host: stored.value.host } : {}),
|
|
71
|
+
from: stored.value.from,
|
|
72
|
+
updatedAt: stored.updatedAt,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
cache = { at: Date.now(), value };
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
export { MAIL_SCHEMES };
|
|
80
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/mail/resolve.ts"],"names":[],"mappings":"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"}
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const asHttps = (host) => (/^https?:\/\//.test(host) ? host : `https://${host}`).replace(/\/+$/, "");
|
|
2
|
+
export function applyPlatformDefaults(env) {
|
|
3
|
+
const out = { port: null, baseUrl: null };
|
|
4
|
+
if (!env.SNAGSET_PORT && env.PORT && /^\d+$/.test(env.PORT)) {
|
|
5
|
+
env.SNAGSET_PORT = env.PORT;
|
|
6
|
+
out.port = env.PORT;
|
|
7
|
+
}
|
|
8
|
+
if (!env.SNAGSET_BASE_URL && !env.SNAGSET_PUBLIC_URL) {
|
|
9
|
+
const candidates = [
|
|
10
|
+
["VERCEL_PROJECT_PRODUCTION_URL", env.VERCEL_PROJECT_PRODUCTION_URL],
|
|
11
|
+
["VERCEL_URL", env.VERCEL_URL],
|
|
12
|
+
["RAILWAY_PUBLIC_DOMAIN", env.RAILWAY_PUBLIC_DOMAIN],
|
|
13
|
+
["RENDER_EXTERNAL_URL", env.RENDER_EXTERNAL_URL],
|
|
14
|
+
["FLY_APP_NAME", env.FLY_APP_NAME ? `${env.FLY_APP_NAME}.fly.dev` : undefined],
|
|
15
|
+
];
|
|
16
|
+
for (const [from, host] of candidates) {
|
|
17
|
+
if (host && host.trim()) {
|
|
18
|
+
const value = asHttps(host.trim());
|
|
19
|
+
env.SNAGSET_BASE_URL = value;
|
|
20
|
+
out.baseUrl = { value, from };
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AA0BA,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAErH,MAAM,UAAU,qBAAqB,CAAC,GAAuC;IAC3E,MAAM,GAAG,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC5D,IAAI,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5D,GAAG,CAAC,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC;QAC5B,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;IACtB,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,gBAAgB,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;QACrD,MAAM,UAAU,GAAmC;YACjD,CAAC,+BAA+B,EAAE,GAAG,CAAC,6BAA6B,CAAC;YACpE,CAAC,YAAY,EAAE,GAAG,CAAC,UAAU,CAAC;YAC9B,CAAC,uBAAuB,EAAE,GAAG,CAAC,qBAAqB,CAAC;YACpD,CAAC,qBAAqB,EAAE,GAAG,CAAC,mBAAmB,CAAC;YAChD,CAAC,cAAc,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;SAC/E,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,UAAU,EAAE,CAAC;YACtC,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBACxB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;gBACnC,GAAG,CAAC,gBAAgB,GAAG,KAAK,CAAC;gBAC7B,GAAG,CAAC,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;gBAC9B,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/dist/projectCache.js
CHANGED
|
@@ -14,8 +14,9 @@ export function createProjectCache(sql, opts = {}) {
|
|
|
14
14
|
const inflight = new Map();
|
|
15
15
|
async function load(uid) {
|
|
16
16
|
const rows = await sql `
|
|
17
|
-
SELECT id, org_id, uid, status, write_mode, write_mode_downgrade_at, read_mode,
|
|
18
|
-
session_epoch, config_version, config_etag, key_prefix, ip_salt
|
|
17
|
+
SELECT id, org_id, uid, name, status, write_mode, write_mode_downgrade_at, read_mode,
|
|
18
|
+
session_epoch, config_version, config_etag, key_prefix, ip_salt,
|
|
19
|
+
settings->>'domain' AS domain
|
|
19
20
|
FROM projects WHERE uid = ${uid} AND deleted_at IS NULL
|
|
20
21
|
`;
|
|
21
22
|
return rows[0] ?? null;
|
package/dist/projectCache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projectCache.js","sourceRoot":"","sources":["../src/projectCache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"projectCache.js","sourceRoot":"","sources":["../src/projectCache.ts"],"names":[],"mappings":"AA4CA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,KAAa;QACvB,KAAK,CAAC,wCAAwC,KAAK,EAAE,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAgBD,MAAM,UAAU,kBAAkB,CAChC,GAAiB,EACjB,OAA+C,EAAE;IAEjD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiB,CAAC;IACzC,4EAA4E;IAC5E,wEAAwE;IACxE,2BAA2B;IAC3B,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAsC,CAAC;IAE/D,KAAK,UAAU,IAAI,CAAC,GAAW;QAC7B,MAAM,IAAI,GAAG,MAAM,GAAG,CAAc;;;;kCAIN,GAAG;KAChC,CAAC;QACF,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IACzB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC7B,IAAI,GAAG,IAAI,GAAG,EAAE,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG;gBAAE,OAAO,GAAG,CAAC,GAAG,CAAC;YAEhD,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC;qBAChB,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;oBACZ,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;oBACrC,OAAO,GAAG,CAAC;gBACb,CAAC,CAAC;qBACD,KAAK,CAAC,CAAC,CAAU,EAAE,EAAE;oBACpB,iDAAiD;oBACjD,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;oBACpB,MAAM,IAAI,kBAAkB,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC3E,CAAC,CAAC;qBACD,OAAO,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;gBACvC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC7B,CAAC;YACD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,UAAU,CAAC,GAAG;YACZ,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/projects.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { ensureStudioReviewers } from "./studioReviewers.js";
|
|
2
3
|
import { ID_PREFIX } from "./vendor/types/names.js";
|
|
3
4
|
/**
|
|
4
5
|
* `prj_` + 12 Crockford base32 characters — foundations §2.10, and §3.3's
|
|
@@ -68,10 +69,11 @@ export async function createProject(tx, input) {
|
|
|
68
69
|
const name = input.name.trim() || "Untitled site";
|
|
69
70
|
const origins = validateOrigins(input.origins ?? []);
|
|
70
71
|
await tx `
|
|
71
|
-
INSERT INTO projects (id, org_id, uid, name, ip_salt, config_etag, write_mode_downgrade_at)
|
|
72
|
+
INSERT INTO projects (id, org_id, uid, name, ip_salt, config_etag, write_mode_downgrade_at, settings)
|
|
72
73
|
VALUES (${id}, ${input.orgId}, ${uid}, ${name},
|
|
73
74
|
${randomBytes(32)}, ${randomBytes(8).toString("hex")},
|
|
74
|
-
now() + ${input.graceHours}::int * interval '1 hour'
|
|
75
|
+
now() + ${input.graceHours}::int * interval '1 hour',
|
|
76
|
+
${tx.json(input.domain ? { domain: input.domain } : {})}::jsonb)
|
|
75
77
|
`;
|
|
76
78
|
// 05 §6.10 step 2. `threads.environment_id` is NOT NULL.
|
|
77
79
|
for (const [key, label, isDefault] of [
|
|
@@ -90,6 +92,10 @@ export async function createProject(tx, input) {
|
|
|
90
92
|
INSERT INTO rounds (id, org_id, project_id, number, name, created_by)
|
|
91
93
|
VALUES (${randomUUID()}, ${input.orgId}, ${id}, 1, 'Round 1', ${input.createdBy})
|
|
92
94
|
`;
|
|
95
|
+
// 05 §6.4b: studio people are reviewer rows in every project — made here
|
|
96
|
+
// for the members that exist, and on membership create for the projects
|
|
97
|
+
// that do. The assignee pool is a pure read because of these two lines.
|
|
98
|
+
await ensureStudioReviewers(tx, { orgId: input.orgId, projectId: id });
|
|
93
99
|
for (const origin of origins) {
|
|
94
100
|
await tx `
|
|
95
101
|
INSERT INTO project_origins (id, org_id, project_id, pattern, is_wildcard)
|
package/dist/projects.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { hashPassword, verifyPassword } from "./owner.js";
|
|
3
|
+
function fail(c, status, code, message) {
|
|
4
|
+
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
5
|
+
}
|
|
6
|
+
const MIN = 12; // 05 §4.7 — length is the rule, no composition theatre
|
|
7
|
+
export function accountRoutes(deps) {
|
|
8
|
+
const { sql } = deps;
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
app.patch("/api/v1/org", async (c) => {
|
|
11
|
+
const owner = c.get("owner");
|
|
12
|
+
if (!owner)
|
|
13
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
14
|
+
if (owner.role !== "owner")
|
|
15
|
+
return fail(c, 403, "forbidden", "Only an owner can rename the studio.");
|
|
16
|
+
const body = (await c.req.json().catch(() => null));
|
|
17
|
+
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
|
18
|
+
if (!name)
|
|
19
|
+
return fail(c, 400, "validation_failed", "The studio needs a name.");
|
|
20
|
+
if (name.length > 120)
|
|
21
|
+
return fail(c, 400, "validation_failed", "That name is too long — 120 characters at most.");
|
|
22
|
+
// The slug stays. It is the studio's stable handle in URLs and mail; a
|
|
23
|
+
// rename is a label changing, not an identity.
|
|
24
|
+
await sql `UPDATE orgs SET name = ${name} WHERE id = ${owner.orgId}`;
|
|
25
|
+
return c.json({ org: { name } });
|
|
26
|
+
});
|
|
27
|
+
app.patch("/api/v1/me", 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().slice(0, 120) : "";
|
|
33
|
+
await sql `UPDATE users SET name = ${name}, updated_at = now() WHERE id = ${owner.userId}`;
|
|
34
|
+
return c.json({ user: { email: owner.email, name, role: owner.role } });
|
|
35
|
+
});
|
|
36
|
+
app.post("/api/v1/me/password", async (c) => {
|
|
37
|
+
const owner = c.get("owner");
|
|
38
|
+
if (!owner)
|
|
39
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
40
|
+
const body = (await c.req.json().catch(() => null));
|
|
41
|
+
const current = typeof body?.current === "string" ? body.current : "";
|
|
42
|
+
const next = typeof body?.next === "string" ? body.next : "";
|
|
43
|
+
if (next.length < MIN) {
|
|
44
|
+
return fail(c, 400, "validation_failed", `The new password needs at least ${MIN} characters. A short phrase of real words is fine.`);
|
|
45
|
+
}
|
|
46
|
+
const [row] = await sql `SELECT password_hash FROM users WHERE id = ${owner.userId}`;
|
|
47
|
+
if (!verifyPassword(current, row?.password_hash ?? null)) {
|
|
48
|
+
// Said plainly. There is no enumeration risk on your own account.
|
|
49
|
+
return fail(c, 400, "wrong_password", "That is not your current password.");
|
|
50
|
+
}
|
|
51
|
+
await sql.begin(async (tx) => {
|
|
52
|
+
await tx `UPDATE users SET password_hash = ${hashPassword(next)}, updated_at = now() WHERE id = ${owner.userId}`;
|
|
53
|
+
await tx `UPDATE user_sessions SET revoked_at = now() WHERE user_id = ${owner.userId} AND id <> ${owner.sessionId} AND revoked_at IS NULL`;
|
|
54
|
+
});
|
|
55
|
+
return c.json({ ok: true });
|
|
56
|
+
});
|
|
57
|
+
return app;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../../src/routes/account.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { ID_PREFIX } from "../vendor/types/names.js";
|
|
3
|
+
const STATUSES = ["open", "progress", "review", "resolved"];
|
|
4
|
+
const isStatus = (s) => STATUSES.includes(s);
|
|
5
|
+
const MAX = 100;
|
|
6
|
+
const DEFAULT_LIMIT = 50;
|
|
7
|
+
function fail(c, status, code, message) {
|
|
8
|
+
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
9
|
+
}
|
|
10
|
+
export function activityRoutes(sql) {
|
|
11
|
+
const app = new Hono();
|
|
12
|
+
app.get("/api/v1/activity", async (c) => {
|
|
13
|
+
const owner = c.get("owner");
|
|
14
|
+
if (!owner)
|
|
15
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
16
|
+
const statusRaw = c.req.query("status")?.trim();
|
|
17
|
+
if (statusRaw && statusRaw !== "all" && statusRaw !== "unresolved" && !isStatus(statusRaw)) {
|
|
18
|
+
return fail(c, 400, "validation_failed", `status must be one of ${STATUSES.join(", ")}, or the word unresolved or all.`);
|
|
19
|
+
}
|
|
20
|
+
const site = c.req.query("site")?.trim();
|
|
21
|
+
/**
|
|
22
|
+
* `?q=` — the same three columns the per-site list searches (key, the
|
|
23
|
+
* anchor's label, the comment bodies), so a studio-wide search and a
|
|
24
|
+
* site's own search can never disagree about what matches. It is what
|
|
25
|
+
* makes ⌘K able to find a comment when you remember the words and not
|
|
26
|
+
* the site (D103).
|
|
27
|
+
*/
|
|
28
|
+
const q = (c.req.query("q") ?? "").trim();
|
|
29
|
+
const limit = Math.min(MAX, Math.max(1, Number(c.req.query("limit")) || DEFAULT_LIMIT));
|
|
30
|
+
/**
|
|
31
|
+
* The cursor is the last row's `updated_at` and id, not an offset: rows
|
|
32
|
+
* arrive while the reader is paging — that is the entire point of this
|
|
33
|
+
* screen — and an offset would hand them a row twice or skip one.
|
|
34
|
+
*/
|
|
35
|
+
const cursorRaw = c.req.query("cursor");
|
|
36
|
+
let cursor = null;
|
|
37
|
+
if (cursorRaw) {
|
|
38
|
+
const [at, id] = cursorRaw.split("|");
|
|
39
|
+
const parsed = new Date(at ?? "");
|
|
40
|
+
if (Number.isNaN(parsed.getTime()) || !id) {
|
|
41
|
+
return fail(c, 400, "validation_failed", "cursor must be the `nextCursor` from a previous answer.");
|
|
42
|
+
}
|
|
43
|
+
cursor = { at: parsed, id };
|
|
44
|
+
}
|
|
45
|
+
const rows = await sql.begin(async (tx) => {
|
|
46
|
+
await tx `SELECT set_config('app.org_id', ${owner.orgId}, true)`;
|
|
47
|
+
return tx `
|
|
48
|
+
SELECT t.id, t.key, t.status, t.route, t.updated_at, t.created_at, t.comment_count_public,
|
|
49
|
+
p.uid AS project_uid, p.name AS project_name, e.key AS environment_key,
|
|
50
|
+
r.name AS created_by_name, r.kind AS created_by_kind,
|
|
51
|
+
ar.name AS assignee_name,
|
|
52
|
+
lc.body AS latest_body, lc.created_at AS latest_at, lcr.name AS latest_author
|
|
53
|
+
FROM threads t
|
|
54
|
+
JOIN projects p ON p.id = t.project_id AND p.deleted_at IS NULL
|
|
55
|
+
JOIN environments e ON e.id = t.environment_id
|
|
56
|
+
JOIN reviewers r ON r.id = t.created_by
|
|
57
|
+
LEFT JOIN reviewers ar ON ar.id = t.assignee_id
|
|
58
|
+
-- The newest PUBLIC comment: an activity feed says what was said,
|
|
59
|
+
-- and a team-only reply is the studio talking about the client.
|
|
60
|
+
-- It is still the studio reading this screen, but a feed that
|
|
61
|
+
-- quotes an internal note beside a client's words is how one gets
|
|
62
|
+
-- pasted into the wrong window.
|
|
63
|
+
LEFT JOIN LATERAL (
|
|
64
|
+
SELECT c.body, c.created_at, c.author_id
|
|
65
|
+
FROM comments c
|
|
66
|
+
WHERE c.thread_id = t.id AND c.deleted_at IS NULL AND c.private = false
|
|
67
|
+
ORDER BY c.seq DESC
|
|
68
|
+
LIMIT 1
|
|
69
|
+
) lc ON true
|
|
70
|
+
LEFT JOIN reviewers lcr ON lcr.id = lc.author_id
|
|
71
|
+
WHERE t.org_id = ${owner.orgId}::uuid
|
|
72
|
+
AND t.deleted_at IS NULL
|
|
73
|
+
AND t.private = false
|
|
74
|
+
${site ? tx `AND p.uid = ${site}` : tx ``}
|
|
75
|
+
${statusRaw === "unresolved" ? tx `AND t.status <> 'resolved'` : statusRaw && statusRaw !== "all" ? tx `AND t.status = ${statusRaw}::snag_thread_status` : tx ``}
|
|
76
|
+
${q
|
|
77
|
+
? tx `AND (t.key ILIKE ${"%" + q + "%"}
|
|
78
|
+
OR t.anchor_label ILIKE ${"%" + q + "%"}
|
|
79
|
+
OR EXISTS (SELECT 1 FROM comments cq
|
|
80
|
+
WHERE cq.thread_id = t.id AND cq.deleted_at IS NULL AND cq.private = false
|
|
81
|
+
AND cq.body ILIKE ${"%" + q + "%"}))`
|
|
82
|
+
: tx ``}
|
|
83
|
+
${cursor ? tx `AND (t.updated_at, t.id) < (${cursor.at}, ${cursor.id}::uuid)` : tx ``}
|
|
84
|
+
ORDER BY t.updated_at DESC, t.id DESC
|
|
85
|
+
LIMIT ${limit + 1}
|
|
86
|
+
`;
|
|
87
|
+
});
|
|
88
|
+
const page = rows.slice(0, limit);
|
|
89
|
+
const last = page[page.length - 1];
|
|
90
|
+
return c.json({
|
|
91
|
+
data: page.map((r) => ({
|
|
92
|
+
id: `${ID_PREFIX.thread}${r.id}`,
|
|
93
|
+
key: r.key,
|
|
94
|
+
status: r.status,
|
|
95
|
+
route: r.route ?? "/",
|
|
96
|
+
site: { uid: r.project_uid, name: r.project_name },
|
|
97
|
+
environment: r.environment_key,
|
|
98
|
+
createdBy: { name: r.created_by_name, kind: r.created_by_kind },
|
|
99
|
+
assignee: r.assignee_name,
|
|
100
|
+
commentCount: r.comment_count_public,
|
|
101
|
+
latest: r.latest_body === null ? null : { body: r.latest_body, at: r.latest_at?.toISOString() ?? null, author: r.latest_author },
|
|
102
|
+
createdAt: r.created_at.toISOString(),
|
|
103
|
+
updatedAt: r.updated_at.toISOString(),
|
|
104
|
+
})),
|
|
105
|
+
nextCursor: rows.length > limit && last ? `${last.updated_at.toISOString()}|${last.id}` : null,
|
|
106
|
+
serverTime: new Date().toISOString(),
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return app;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.js","sourceRoot":"","sources":["../../src/routes/activity.ts"],"names":[],"mappings":"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"}
|