@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.
Files changed (139) hide show
  1. package/README.md +4 -3
  2. package/dist/app.js +82 -9
  3. package/dist/app.js.map +1 -1
  4. package/dist/bin.js +85 -11
  5. package/dist/bin.js.map +1 -1
  6. package/dist/boot.js +67 -51
  7. package/dist/boot.js.map +1 -1
  8. package/dist/config.generated.js +5 -2
  9. package/dist/config.generated.js.map +1 -1
  10. package/dist/db/connect.js +7 -0
  11. package/dist/db/connect.js.map +1 -1
  12. package/dist/db/migrations/0005_studio_reviewers.sql +45 -0
  13. package/dist/db/migrations/0006_instance_settings.sql +69 -0
  14. package/dist/db/migrations/0007_jira_provider.no-txn.sql +13 -0
  15. package/dist/domain.js +129 -0
  16. package/dist/domain.js.map +1 -0
  17. package/dist/instance/settings.js +54 -0
  18. package/dist/instance/settings.js.map +1 -0
  19. package/dist/integrations/github.js +112 -0
  20. package/dist/integrations/github.js.map +1 -0
  21. package/dist/integrations/inbound.js +88 -0
  22. package/dist/integrations/inbound.js.map +1 -0
  23. package/dist/integrations/jira.js +129 -0
  24. package/dist/integrations/jira.js.map +1 -0
  25. package/dist/integrations/linear.js +111 -0
  26. package/dist/integrations/linear.js.map +1 -0
  27. package/dist/integrations/outbox.js +305 -0
  28. package/dist/integrations/outbox.js.map +1 -0
  29. package/dist/integrations/slack.js +135 -0
  30. package/dist/integrations/slack.js.map +1 -0
  31. package/dist/integrations/store.js +101 -0
  32. package/dist/integrations/store.js.map +1 -0
  33. package/dist/listen.js +58 -0
  34. package/dist/listen.js.map +1 -0
  35. package/dist/mail/index.js +21 -11
  36. package/dist/mail/index.js.map +1 -1
  37. package/dist/mail/resolve.js +80 -0
  38. package/dist/mail/resolve.js.map +1 -0
  39. package/dist/platform.js +27 -0
  40. package/dist/platform.js.map +1 -0
  41. package/dist/projectCache.js +3 -2
  42. package/dist/projectCache.js.map +1 -1
  43. package/dist/projects.js +8 -2
  44. package/dist/projects.js.map +1 -1
  45. package/dist/routes/account.js +59 -0
  46. package/dist/routes/account.js.map +1 -0
  47. package/dist/routes/activity.js +111 -0
  48. package/dist/routes/activity.js.map +1 -0
  49. package/dist/routes/assignees.js +139 -0
  50. package/dist/routes/assignees.js.map +1 -0
  51. package/dist/routes/attachments.js +2 -2
  52. package/dist/routes/attachments.js.map +1 -1
  53. package/dist/routes/comments.js +27 -17
  54. package/dist/routes/comments.js.map +1 -1
  55. package/dist/routes/cors.js +10 -1
  56. package/dist/routes/cors.js.map +1 -1
  57. package/dist/routes/instance.js +164 -0
  58. package/dist/routes/instance.js.map +1 -0
  59. package/dist/routes/integrations.js +610 -0
  60. package/dist/routes/integrations.js.map +1 -0
  61. package/dist/routes/jobs.js +15 -3
  62. package/dist/routes/jobs.js.map +1 -1
  63. package/dist/routes/mcpTokens.js +119 -0
  64. package/dist/routes/mcpTokens.js.map +1 -0
  65. package/dist/routes/members.js +177 -0
  66. package/dist/routes/members.js.map +1 -0
  67. package/dist/routes/owner.js +64 -7
  68. package/dist/routes/owner.js.map +1 -1
  69. package/dist/routes/sites.js +181 -8
  70. package/dist/routes/sites.js.map +1 -1
  71. package/dist/routes/threads.js +276 -9
  72. package/dist/routes/threads.js.map +1 -1
  73. package/dist/serverless.js +41 -6
  74. package/dist/serverless.js.map +1 -1
  75. package/dist/studioReviewers.js +67 -0
  76. package/dist/studioReviewers.js.map +1 -0
  77. package/dist/vendor/adapters/outbound/sanitise.js +18 -3
  78. package/dist/vendor/types/brief.js +121 -0
  79. package/package.json +1 -1
  80. package/static/dashboard/assets/CompositeItem-DnLNS57f.js +1 -0
  81. package/static/dashboard/assets/ConfirmDialog-CCAJDc_P.js +1 -0
  82. package/static/dashboard/assets/CopyButton-exuJDdXj.js +1 -0
  83. package/static/dashboard/assets/FieldItemContext-jf0Is3j7.js +1 -0
  84. package/static/dashboard/assets/InstallTag-CkeGLk1a.js +1 -0
  85. package/static/dashboard/assets/SaveState-BsrfwjoH.js +6 -0
  86. package/static/dashboard/assets/_app-CiDKmy_a.js +1 -0
  87. package/static/dashboard/assets/_app-xxJ7UG34.js +56 -0
  88. package/static/dashboard/assets/activity-mwHb7ir7.js +1 -0
  89. package/static/dashboard/assets/arrow-left-D08BSlJU.js +6 -0
  90. package/static/dashboard/assets/avatar-BITl2vTh.js +1 -0
  91. package/static/dashboard/assets/badge-CGEswzew.js +1 -0
  92. package/static/dashboard/assets/card-8T0wnIiU.js +1 -0
  93. package/static/dashboard/assets/check-CiLHTeII.js +6 -0
  94. package/static/dashboard/assets/chevron-right-BokKbPOn.js +6 -0
  95. package/static/dashboard/assets/copy-CKgsZbj1.js +6 -0
  96. package/static/dashboard/assets/createBaseUIEventDetails-CEmaecLi.js +1 -0
  97. package/static/dashboard/assets/createLucideIcon-llnWWh0E.js +36 -0
  98. package/static/dashboard/assets/dialog-DrrnCoT5.js +6 -0
  99. package/static/dashboard/assets/dropdown-menu-DIE2xmL5.js +11 -0
  100. package/static/dashboard/assets/image-off-CgUC8krg.js +6 -0
  101. package/static/dashboard/assets/index-BRTBtV91.css +1 -0
  102. package/static/dashboard/assets/index-C87aqO18.js +129 -0
  103. package/static/dashboard/assets/label-BfMHjFA7.js +1 -0
  104. package/static/dashboard/assets/labels--UUyt9uB.js +1 -0
  105. package/static/dashboard/assets/layout-grid-B7mn7n3K.js +6 -0
  106. package/static/dashboard/assets/me-D1prnPpY.js +1 -0
  107. package/static/dashboard/assets/parts-B-HQayFH.js +1 -0
  108. package/static/dashboard/assets/plus-CuMhvZUZ.js +6 -0
  109. package/static/dashboard/assets/s._site-Ccc7pzT-.js +1 -0
  110. package/static/dashboard/assets/s._site.comments-nJZLYDQh.js +39 -0
  111. package/static/dashboard/assets/s._site.reviewers-BmRPiC0a.js +1 -0
  112. package/static/dashboard/assets/s._site.settings-DNv5ovrJ.js +1 -0
  113. package/static/dashboard/assets/setup-B_VsIyK2.js +1 -0
  114. package/static/dashboard/assets/sign-in-sWUNd3Lp.js +1 -0
  115. package/static/dashboard/assets/sites-CSzUAHV6.js +1 -0
  116. package/static/dashboard/assets/studio-DVFn025O.js +1 -0
  117. package/static/dashboard/assets/studio-DzBapfQK.js +1 -0
  118. package/static/dashboard/assets/studio.index-Cp7lBb8S.js +1 -0
  119. package/static/dashboard/assets/studio.integrations-DD2EWaL-.js +1 -0
  120. package/static/dashboard/assets/studio.members-CVu-eP6p.js +1 -0
  121. package/static/dashboard/assets/sun-DTI4O-GM.js +16 -0
  122. package/static/dashboard/assets/table-DkqScFZb.js +1 -0
  123. package/static/dashboard/assets/update-duXzIzFS.js +1 -0
  124. package/static/dashboard/assets/useBaseQuery-0OqUJMfc.js +1 -0
  125. package/static/dashboard/assets/useQuery-COJ09EkU.js +1 -0
  126. package/static/dashboard/assets/useRender-dtgBGlgC.js +1 -0
  127. package/static/dashboard/assets/users-O11swHBQ.js +6 -0
  128. package/static/dashboard/assets/welcome-CT6oH0I8.js +11 -0
  129. package/static/dashboard/assets/you-_yIPqa_9.js +1 -0
  130. package/static/dashboard/assets/zod-iC-UhR5u.js +1 -0
  131. package/static/dashboard/index.html +17 -2
  132. package/static/widget/snag.capture.js +1 -1
  133. package/static/widget/snag.dock.js +1 -1
  134. package/static/widget/snag.panels.js +1 -1
  135. package/static/widget/snag.runtime.js +2 -2
  136. package/static/widget/snag.text.js +1 -1
  137. package/vercel.json +4 -0
  138. package/static/dashboard/assets/index-CATItBzT.js +0 -1
  139. 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"}
@@ -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
- // Parsed by hand rather than with `new URL`: a Resend key is the AUTHORITY
50
- // component of `resend://re_abc123`, and WHATWG URL lower-cases hostnames.
51
- // An API key is case-sensitive, so `new URL().hostname` would silently
52
- // corrupt it and produce a 401 that looks like a bad key rather than a bad
53
- // parser.
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(`SNAGSET_MAIL_URL has an unsupported scheme ${JSON.stringify(scheme || raw.slice(0, 12))}. ` +
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("SNAGSET_MAIL_URL is resend:// with no API key after it.");
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".
@@ -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;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,uEAAuE;IACvE,2EAA2E;IAC3E,UAAU;IACV,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,8CAA8C,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI;YAC1F,sBAAsB,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACzE,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,iEAAiE,CACpE,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QACtF,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"}
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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"}
@@ -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"}
@@ -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;
@@ -1 +1 @@
1
- {"version":3,"file":"projectCache.js","sourceRoot":"","sources":["../src/projectCache.ts"],"names":[],"mappings":"AAyCA,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;;;kCAGN,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"}
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)
@@ -1 +1 @@
1
- {"version":3,"file":"projects.js","sourceRoot":"","sources":["../src/projects.ts"],"names":[],"mappings":"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"}
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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"}