@snagset/server 0.0.3 → 0.1.0

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 +167 -30
  3. package/dist/app.js.map +1 -1
  4. package/dist/bin.js +46 -9
  5. package/dist/bin.js.map +1 -1
  6. package/dist/boot.js +105 -54
  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/mail/index.js +21 -11
  34. package/dist/mail/index.js.map +1 -1
  35. package/dist/mail/resolve.js +80 -0
  36. package/dist/mail/resolve.js.map +1 -0
  37. package/dist/projectCache.js +3 -2
  38. package/dist/projectCache.js.map +1 -1
  39. package/dist/projects.js +8 -2
  40. package/dist/projects.js.map +1 -1
  41. package/dist/routes/account.js +59 -0
  42. package/dist/routes/account.js.map +1 -0
  43. package/dist/routes/activity.js +111 -0
  44. package/dist/routes/activity.js.map +1 -0
  45. package/dist/routes/anchorReports.js +196 -0
  46. package/dist/routes/anchorReports.js.map +1 -0
  47. package/dist/routes/assignees.js +139 -0
  48. package/dist/routes/assignees.js.map +1 -0
  49. package/dist/routes/attachments.js +2 -2
  50. package/dist/routes/attachments.js.map +1 -1
  51. package/dist/routes/bodyLimit.js +60 -0
  52. package/dist/routes/bodyLimit.js.map +1 -0
  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 +39 -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-CoPuxIvr.js +1 -0
  81. package/static/dashboard/assets/ConfirmDialog-DkCcJwll.js +1 -0
  82. package/static/dashboard/assets/CopyButton-BNa2pLLv.js +1 -0
  83. package/static/dashboard/assets/FieldItemContext-DQXjpFtW.js +1 -0
  84. package/static/dashboard/assets/InstallTag-SOlWl7Hx.js +1 -0
  85. package/static/dashboard/assets/SaveState-CmxB4XyX.js +6 -0
  86. package/static/dashboard/assets/_app-DNkv3HBi.js +1 -0
  87. package/static/dashboard/assets/_app-xVQe811c.js +56 -0
  88. package/static/dashboard/assets/activity-DsMa5Ja6.js +1 -0
  89. package/static/dashboard/assets/arrow-left-vQV4xhAD.js +6 -0
  90. package/static/dashboard/assets/avatar-DuPL6QD_.js +1 -0
  91. package/static/dashboard/assets/badge-BWjiIc8T.js +1 -0
  92. package/static/dashboard/assets/card-BIBfCno8.js +1 -0
  93. package/static/dashboard/assets/check-Dpx6GTdT.js +6 -0
  94. package/static/dashboard/assets/chevron-right-DKIlBUGN.js +6 -0
  95. package/static/dashboard/assets/copy-DCbhEe2L.js +6 -0
  96. package/static/dashboard/assets/createBaseUIEventDetails-C_ZVkrgp.js +1 -0
  97. package/static/dashboard/assets/createLucideIcon-C_IGE4WD.js +36 -0
  98. package/static/dashboard/assets/dialog-BT2uWf4e.js +6 -0
  99. package/static/dashboard/assets/dropdown-menu-DSXzPAwB.js +11 -0
  100. package/static/dashboard/assets/image-off-DGWejLDG.js +6 -0
  101. package/static/dashboard/assets/index-BRTBtV91.css +1 -0
  102. package/static/dashboard/assets/index-D6FWZoJT.js +129 -0
  103. package/static/dashboard/assets/label-9QsSpLMA.js +1 -0
  104. package/static/dashboard/assets/labels--UUyt9uB.js +1 -0
  105. package/static/dashboard/assets/layout-grid-UPGEmEw-.js +6 -0
  106. package/static/dashboard/assets/me-0HQsBXLU.js +1 -0
  107. package/static/dashboard/assets/parts-C29uZ5zo.js +1 -0
  108. package/static/dashboard/assets/plus-BFNS5LcY.js +6 -0
  109. package/static/dashboard/assets/s._site-D6Ep2tdc.js +1 -0
  110. package/static/dashboard/assets/s._site.comments-BRVI5gpJ.js +39 -0
  111. package/static/dashboard/assets/s._site.reviewers-CiEkZnnp.js +1 -0
  112. package/static/dashboard/assets/s._site.settings-BqLGyfEd.js +1 -0
  113. package/static/dashboard/assets/setup-dUMCPWdf.js +1 -0
  114. package/static/dashboard/assets/sign-in-CdRa-bio.js +1 -0
  115. package/static/dashboard/assets/sites-CmbtFKoo.js +1 -0
  116. package/static/dashboard/assets/studio-BHuVXWZh.js +1 -0
  117. package/static/dashboard/assets/studio-BjeTOKda.js +1 -0
  118. package/static/dashboard/assets/studio.index--kuzOs7w.js +1 -0
  119. package/static/dashboard/assets/studio.integrations-Bq6HP80a.js +1 -0
  120. package/static/dashboard/assets/studio.members-8AB1KYsw.js +1 -0
  121. package/static/dashboard/assets/sun-BCiTcgnh.js +16 -0
  122. package/static/dashboard/assets/table-BXmeI_yA.js +1 -0
  123. package/static/dashboard/assets/update-duXzIzFS.js +1 -0
  124. package/static/dashboard/assets/useBaseQuery-Cp9wivVR.js +1 -0
  125. package/static/dashboard/assets/useQuery-BZXZlOqO.js +1 -0
  126. package/static/dashboard/assets/useRender-CO8aPW06.js +1 -0
  127. package/static/dashboard/assets/users-Dvw2FCKM.js +6 -0
  128. package/static/dashboard/assets/welcome-BHKjw20b.js +11 -0
  129. package/static/dashboard/assets/you-RBpAfUMU.js +1 -0
  130. package/static/dashboard/assets/zod-BpyjQIvb.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-DsUJ96yc.css +0 -1
  139. package/static/dashboard/assets/index-DultqpS7.js +0 -1
@@ -0,0 +1,101 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { open, seal } from "../crypto.js";
3
+ /** The providers a site can be connected to today; the enum has more. */
4
+ export const TRACKERS = ["github", "linear", "jira", "slack"];
5
+ export const isTracker = (s) => TRACKERS.includes(s);
6
+ /** What a thread's mirror is called in each tracker — `external_refs.kind`. */
7
+ export const REF_KIND = { github: "issue", linear: "issue", jira: "issue", slack: "message" };
8
+ const SELECT = (tx) => tx `
9
+ id, org_id, project_id, provider, config, credentials, credentials_iv, enabled, install_state, last_error, updated_at`;
10
+ function shapeRow(keys, r) {
11
+ const sealed = r.credentials && r.credentials_iv ? { ciphertext: r.credentials, iv: r.credentials_iv } : null;
12
+ const secret = sealed ? open(keys, sealed) : null;
13
+ return {
14
+ id: r.id,
15
+ orgId: r.org_id,
16
+ projectId: r.project_id,
17
+ provider: r.provider,
18
+ config: r.config,
19
+ secret,
20
+ unreadable: !!sealed && secret === null,
21
+ enabled: r.enabled,
22
+ installState: r.install_state,
23
+ lastError: r.last_error,
24
+ updatedAt: r.updated_at,
25
+ };
26
+ }
27
+ export async function getIntegration(sql, keys, orgId, projectId, provider) {
28
+ const [row] = await sql.begin(async (tx) => {
29
+ await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
30
+ return tx `
31
+ SELECT ${SELECT(tx)} FROM integrations
32
+ WHERE project_id = ${projectId}::uuid AND provider = ${provider}::snag_provider AND deleted_at IS NULL
33
+ `;
34
+ });
35
+ return row ? shapeRow(keys, row) : null;
36
+ }
37
+ /** Every live connection in the studio, for the Integrations screen. */
38
+ export async function listIntegrations(sql, keys, orgId) {
39
+ const rows = await sql.begin(async (tx) => {
40
+ await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
41
+ return tx `
42
+ SELECT ${SELECT(tx)} FROM integrations
43
+ WHERE org_id = ${orgId}::uuid AND deleted_at IS NULL ORDER BY updated_at DESC
44
+ `;
45
+ });
46
+ return rows.map((r) => shapeRow(keys, r));
47
+ }
48
+ export async function putIntegration(sql, keys, input) {
49
+ const sealed = input.secret === null ? null : seal(keys, input.secret);
50
+ await sql.begin(async (tx) => {
51
+ await tx `SELECT set_config('app.org_id', ${input.orgId}, true)`;
52
+ // A soft-deleted row for the same pair is revived rather than duplicated:
53
+ // `UNIQUE (project_id, provider)` counts the tombstone too.
54
+ await tx `
55
+ INSERT INTO integrations (id, org_id, project_id, provider, config, credentials, credentials_iv, enabled, install_state, last_error)
56
+ VALUES (${randomUUID()}, ${input.orgId}::uuid, ${input.projectId}::uuid, ${input.provider}::snag_provider,
57
+ ${tx.json(input.config)}, ${sealed?.ciphertext ?? null}, ${sealed?.iv ?? null}, true, 'ok', NULL)
58
+ ON CONFLICT (project_id, provider) DO UPDATE
59
+ SET config = EXCLUDED.config,
60
+ credentials = COALESCE(EXCLUDED.credentials, integrations.credentials),
61
+ credentials_iv = COALESCE(EXCLUDED.credentials_iv, integrations.credentials_iv),
62
+ enabled = true, install_state = 'ok', last_error = NULL, deleted_at = NULL, updated_at = now()
63
+ `;
64
+ });
65
+ }
66
+ export async function removeIntegration(sql, orgId, projectId, provider) {
67
+ await sql.begin(async (tx) => {
68
+ await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
69
+ await tx `
70
+ UPDATE integrations SET deleted_at = now(), enabled = false, credentials = NULL, credentials_iv = NULL, updated_at = now()
71
+ WHERE project_id = ${projectId}::uuid AND provider = ${provider}::snag_provider AND deleted_at IS NULL
72
+ `;
73
+ });
74
+ }
75
+ /** Health is a column: the worker writes what it saw, the card shows it. */
76
+ export async function markIntegration(sql, orgId, id, state, error) {
77
+ await sql.begin(async (tx) => {
78
+ await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
79
+ await tx `UPDATE integrations SET install_state = ${state}, last_error = ${error}, updated_at = now() WHERE id = ${id}::uuid`;
80
+ });
81
+ }
82
+ export const trackerSecrets = (secret) => githubSecrets(secret);
83
+ export function githubSecrets(secret) {
84
+ if (!secret)
85
+ return { token: null, webhookSecret: null };
86
+ if (secret.startsWith("{")) {
87
+ try {
88
+ const j = JSON.parse(secret);
89
+ return {
90
+ token: typeof j.token === "string" && j.token ? j.token : null,
91
+ webhookSecret: typeof j.webhookSecret === "string" && j.webhookSecret ? j.webhookSecret : null,
92
+ };
93
+ }
94
+ catch {
95
+ /* a token that happens to start with a brace — fall through */
96
+ }
97
+ }
98
+ return { token: secret, webhookSecret: null };
99
+ }
100
+ export const mintWebhookSecret = () => randomBytes(24).toString("base64url");
101
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/integrations/store.ts"],"names":[],"mappings":"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"}
@@ -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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"}
@@ -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":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AA4CjD;;;;;;;;;GASG;AACH,MAAM,SAAS,GAAG,kCAAkC,CAAC;AACrD,MAAM,UAAU,MAAM,CAAC,MAAc,EAAE,MAAM,GAAG,EAAE;IAChD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,OAAO,GAAG,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;YACvC,IAAI,IAAI,IAAI,GAAG,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC;gBAAE,SAAS;YACvC,GAAG,IAAI,SAAS,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;YAC5B,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,MAAM;QACnC,CAAC;IACH,CAAC;IACD,OAAO,MAAM,GAAG,GAAG,CAAC;AACtB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAAC,OAA0B;IACxD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,IAAI,MAAW,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,kEAAkE;gBACzF,uBAAuB,CAC1B,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,+BAA+B,CAAC,CAAC;QAC5E,CAAC;QACD,IAAI,MAAM,CAAC,QAAQ,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,kEAAkE,CAC5F,CAAC;QACJ,CAAC;QACD,0EAA0E;QAC1E,wEAAwE;QACxE,wEAAwE;QACxE,qDAAqD;QACrD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,EAA2B,EAC3B,KAAiB;IAEjB,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IACxB,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,eAAe,CAAC;IAClD,MAAM,OAAO,GAAG,eAAe,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAErD,MAAM,EAAE,CAAA;;cAEI,EAAE,KAAK,KAAK,CAAC,KAAK,KAAK,GAAG,KAAK,IAAI;cACnC,WAAW,CAAC,EAAE,CAAC,KAAK,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;sBAC1C,KAAK,CAAC,UAAU;GACnC,CAAC;IAEF,yDAAyD;IACzD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI;QACpC,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC;QAClC,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,CAAC;KACrB,EAAE,CAAC;QACX,MAAM,EAAE,CAAA;;gBAEI,UAAU,EAAE,KAAK,KAAK,CAAC,KAAK,KAAK,EAAE,KAAK,GAAG,KAAK,KAAK,KAAK,SAAS;KAC9E,CAAC;IACJ,CAAC;IAED,iEAAiE;IACjE,yEAAyE;IACzE,8CAA8C;IAC9C,MAAM,EAAE,CAAA;;cAEI,UAAU,EAAE,KAAK,KAAK,CAAC,KAAK,KAAK,EAAE,mBAAmB,KAAK,CAAC,SAAS;GAChF,CAAC;IAEF,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,EAAE,CAAA;;gBAEI,UAAU,EAAE,KAAK,KAAK,CAAC,KAAK,KAAK,EAAE,KAAK,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;KACnF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;AAC3B,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":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAE1C,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AA6BjD,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAU,CAAC;AAErE,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAe,EAAE,CAAE,QAA8B,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEzF,MAAM,GAAG,GAAG,GAAG,CAAC;AAChB,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,SAAS,IAAI,CAAC,CAAM,EAAE,MAAc,EAAE,IAAY,EAAE,OAAe;IACjE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAa,CAAC,CAAC;AAC5F,CAAC;AAqBD,MAAM,UAAU,cAAc,CAAC,GAAiB;IAC9C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAO,CAAC;IAE5B,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACtC,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,iCAAiC,CAAC,CAAC;QAEnF,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,CAAC;QAChD,IAAI,SAAS,IAAI,SAAS,KAAK,KAAK,IAAI,SAAS,KAAK,YAAY,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3F,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,yBAAyB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QAC3H,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;QACzC;;;;;;WAMG;QACH,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;QAExF;;;;WAIG;QACH,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,MAAM,GAAoC,IAAI,CAAC;QACnD,IAAI,SAAS,EAAE,CAAC;YACd,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACtC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;YAClC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;gBAC1C,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,yDAAyD,CAAC,CAAC;YACtG,CAAC;YACD,MAAM,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACxC,MAAM,EAAE,CAAA,mCAAmC,KAAK,CAAC,KAAK,SAAS,CAAC;YAChE,OAAO,EAAE,CAAO;;;;;;;;;;;;;;;;;;;;;;;;4BAwBM,KAAK,CAAC,KAAK;;;aAG1B,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA,eAAe,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA,EAAE;aACrC,SAAS,KAAK,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA,4BAA4B,CAAC,CAAC,CAAC,SAAS,IAAI,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA,kBAAkB,SAAS,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAA,EAAE;aAE3J,CAAC;gBACC,CAAC,CAAC,EAAE,CAAA,oBAAoB,GAAG,GAAG,CAAC,GAAG,GAAG;gDACJ,GAAG,GAAG,CAAC,GAAG,GAAG;;;uDAGN,GAAG,GAAG,CAAC,GAAG,GAAG,IAAI;gBACzD,CAAC,CAAC,EAAE,CAAA,EACR;aACE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAA,+BAA+B,MAAM,CAAC,EAAE,KAAK,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAA,EAAE;;iBAE7E,KAAK,GAAG,CAAC;OACnB,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACnC,OAAO,CAAC,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACrB,EAAE,EAAE,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE;gBAChC,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,GAAG;gBACrB,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,EAAE;gBAClD,WAAW,EAAE,CAAC,CAAC,eAAe;gBAC9B,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC,eAAe,EAAE;gBAC/D,QAAQ,EAAE,CAAC,CAAC,aAAa;gBACzB,YAAY,EAAE,CAAC,CAAC,oBAAoB;gBACpC,MAAM,EAAE,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,aAAa,EAAE;gBAChI,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE;gBACrC,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE;aACtC,CAAC,CAAC;YACH,UAAU,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI;YAC9F,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}