@snagset/server 0.0.4 → 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 (135) 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 +46 -9
  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/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/assignees.js +139 -0
  46. package/dist/routes/assignees.js.map +1 -0
  47. package/dist/routes/attachments.js +2 -2
  48. package/dist/routes/attachments.js.map +1 -1
  49. package/dist/routes/comments.js +27 -17
  50. package/dist/routes/comments.js.map +1 -1
  51. package/dist/routes/cors.js +10 -1
  52. package/dist/routes/cors.js.map +1 -1
  53. package/dist/routes/instance.js +164 -0
  54. package/dist/routes/instance.js.map +1 -0
  55. package/dist/routes/integrations.js +610 -0
  56. package/dist/routes/integrations.js.map +1 -0
  57. package/dist/routes/jobs.js +15 -3
  58. package/dist/routes/jobs.js.map +1 -1
  59. package/dist/routes/mcpTokens.js +119 -0
  60. package/dist/routes/mcpTokens.js.map +1 -0
  61. package/dist/routes/members.js +177 -0
  62. package/dist/routes/members.js.map +1 -0
  63. package/dist/routes/owner.js +64 -7
  64. package/dist/routes/owner.js.map +1 -1
  65. package/dist/routes/sites.js +181 -8
  66. package/dist/routes/sites.js.map +1 -1
  67. package/dist/routes/threads.js +276 -9
  68. package/dist/routes/threads.js.map +1 -1
  69. package/dist/serverless.js +39 -6
  70. package/dist/serverless.js.map +1 -1
  71. package/dist/studioReviewers.js +67 -0
  72. package/dist/studioReviewers.js.map +1 -0
  73. package/dist/vendor/adapters/outbound/sanitise.js +18 -3
  74. package/dist/vendor/types/brief.js +121 -0
  75. package/package.json +1 -1
  76. package/static/dashboard/assets/CompositeItem-CoPuxIvr.js +1 -0
  77. package/static/dashboard/assets/ConfirmDialog-DkCcJwll.js +1 -0
  78. package/static/dashboard/assets/CopyButton-BNa2pLLv.js +1 -0
  79. package/static/dashboard/assets/FieldItemContext-DQXjpFtW.js +1 -0
  80. package/static/dashboard/assets/InstallTag-SOlWl7Hx.js +1 -0
  81. package/static/dashboard/assets/SaveState-CmxB4XyX.js +6 -0
  82. package/static/dashboard/assets/_app-DNkv3HBi.js +1 -0
  83. package/static/dashboard/assets/_app-xVQe811c.js +56 -0
  84. package/static/dashboard/assets/activity-DsMa5Ja6.js +1 -0
  85. package/static/dashboard/assets/arrow-left-vQV4xhAD.js +6 -0
  86. package/static/dashboard/assets/avatar-DuPL6QD_.js +1 -0
  87. package/static/dashboard/assets/badge-BWjiIc8T.js +1 -0
  88. package/static/dashboard/assets/card-BIBfCno8.js +1 -0
  89. package/static/dashboard/assets/check-Dpx6GTdT.js +6 -0
  90. package/static/dashboard/assets/chevron-right-DKIlBUGN.js +6 -0
  91. package/static/dashboard/assets/copy-DCbhEe2L.js +6 -0
  92. package/static/dashboard/assets/createBaseUIEventDetails-C_ZVkrgp.js +1 -0
  93. package/static/dashboard/assets/createLucideIcon-C_IGE4WD.js +36 -0
  94. package/static/dashboard/assets/dialog-BT2uWf4e.js +6 -0
  95. package/static/dashboard/assets/dropdown-menu-DSXzPAwB.js +11 -0
  96. package/static/dashboard/assets/image-off-DGWejLDG.js +6 -0
  97. package/static/dashboard/assets/index-BRTBtV91.css +1 -0
  98. package/static/dashboard/assets/index-D6FWZoJT.js +129 -0
  99. package/static/dashboard/assets/label-9QsSpLMA.js +1 -0
  100. package/static/dashboard/assets/labels--UUyt9uB.js +1 -0
  101. package/static/dashboard/assets/layout-grid-UPGEmEw-.js +6 -0
  102. package/static/dashboard/assets/me-0HQsBXLU.js +1 -0
  103. package/static/dashboard/assets/parts-C29uZ5zo.js +1 -0
  104. package/static/dashboard/assets/plus-BFNS5LcY.js +6 -0
  105. package/static/dashboard/assets/s._site-D6Ep2tdc.js +1 -0
  106. package/static/dashboard/assets/s._site.comments-BRVI5gpJ.js +39 -0
  107. package/static/dashboard/assets/s._site.reviewers-CiEkZnnp.js +1 -0
  108. package/static/dashboard/assets/s._site.settings-BqLGyfEd.js +1 -0
  109. package/static/dashboard/assets/setup-dUMCPWdf.js +1 -0
  110. package/static/dashboard/assets/sign-in-CdRa-bio.js +1 -0
  111. package/static/dashboard/assets/sites-CmbtFKoo.js +1 -0
  112. package/static/dashboard/assets/studio-BHuVXWZh.js +1 -0
  113. package/static/dashboard/assets/studio-BjeTOKda.js +1 -0
  114. package/static/dashboard/assets/studio.index--kuzOs7w.js +1 -0
  115. package/static/dashboard/assets/studio.integrations-Bq6HP80a.js +1 -0
  116. package/static/dashboard/assets/studio.members-8AB1KYsw.js +1 -0
  117. package/static/dashboard/assets/sun-BCiTcgnh.js +16 -0
  118. package/static/dashboard/assets/table-BXmeI_yA.js +1 -0
  119. package/static/dashboard/assets/update-duXzIzFS.js +1 -0
  120. package/static/dashboard/assets/useBaseQuery-Cp9wivVR.js +1 -0
  121. package/static/dashboard/assets/useQuery-BZXZlOqO.js +1 -0
  122. package/static/dashboard/assets/useRender-CO8aPW06.js +1 -0
  123. package/static/dashboard/assets/users-Dvw2FCKM.js +6 -0
  124. package/static/dashboard/assets/welcome-BHKjw20b.js +11 -0
  125. package/static/dashboard/assets/you-RBpAfUMU.js +1 -0
  126. package/static/dashboard/assets/zod-BpyjQIvb.js +1 -0
  127. package/static/dashboard/index.html +17 -2
  128. package/static/widget/snag.capture.js +1 -1
  129. package/static/widget/snag.dock.js +1 -1
  130. package/static/widget/snag.panels.js +1 -1
  131. package/static/widget/snag.runtime.js +2 -2
  132. package/static/widget/snag.text.js +1 -1
  133. package/vercel.json +4 -0
  134. package/static/dashboard/assets/index-CATItBzT.js +0 -1
  135. package/static/dashboard/assets/index-DsUJ96yc.css +0 -1
@@ -0,0 +1,54 @@
1
+ import { open, seal } from "../crypto.js";
2
+ export async function readSetting(sql, keys, orgId, key) {
3
+ const [row] = await sql.begin(async (tx) => {
4
+ await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
5
+ return tx `
6
+ SELECT value, secret, secret_iv, updated_at, updated_by
7
+ FROM instance_settings WHERE org_id = ${orgId}::uuid AND key = ${key}
8
+ `;
9
+ });
10
+ if (!row)
11
+ return null;
12
+ const sealed = row.secret && row.secret_iv ? { ciphertext: row.secret, iv: row.secret_iv } : null;
13
+ const secret = sealed ? open(keys, sealed) : null;
14
+ return {
15
+ value: row.value,
16
+ secret,
17
+ unreadable: !!sealed && secret === null,
18
+ updatedAt: row.updated_at,
19
+ updatedBy: row.updated_by,
20
+ };
21
+ }
22
+ export async function writeSetting(sql, keys, input) {
23
+ const sealed = input.secret === null ? null : seal(keys, input.secret);
24
+ await sql.begin(async (tx) => {
25
+ await tx `SELECT set_config('app.org_id', ${input.orgId}, true)`;
26
+ await tx `
27
+ INSERT INTO instance_settings (org_id, key, value, secret, secret_iv, updated_by, updated_at)
28
+ VALUES (${input.orgId}::uuid, ${input.key}, ${tx.json(input.value)}, ${sealed?.ciphertext ?? null},
29
+ ${sealed?.iv ?? null}, ${input.updatedBy}, now())
30
+ ON CONFLICT (org_id, key) DO UPDATE
31
+ SET value = EXCLUDED.value, secret = EXCLUDED.secret, secret_iv = EXCLUDED.secret_iv,
32
+ updated_by = EXCLUDED.updated_by, updated_at = now()
33
+ `;
34
+ });
35
+ }
36
+ export async function clearSetting(sql, orgId, key) {
37
+ await sql.begin(async (tx) => {
38
+ await tx `SELECT set_config('app.org_id', ${orgId}, true)`;
39
+ await tx `DELETE FROM instance_settings WHERE org_id = ${orgId}::uuid AND key = ${key}`;
40
+ });
41
+ }
42
+ /**
43
+ * The one org a v1 self-host has — the storage module's own convention,
44
+ * under the maintenance claim (0002, D56) because the callers that need it
45
+ * (the outbox job, boot) hold no org yet.
46
+ */
47
+ export async function firstOrgId(sql) {
48
+ const [row] = await sql.begin(async (tx) => {
49
+ await tx `SET LOCAL app.maintenance = 'on'`;
50
+ return tx `SELECT id FROM orgs WHERE deleted_at IS NULL ORDER BY created_at LIMIT 1`;
51
+ });
52
+ return row?.id ?? null;
53
+ }
54
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/instance/settings.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAsC1C,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAiB,EACjB,IAAa,EACb,KAAa,EACb,GAAW;IAEX,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACzC,MAAM,EAAE,CAAA,mCAAmC,KAAK,SAAS,CAAC;QAC1D,OAAO,EAAE,CAAO;;8CAE0B,KAAK,oBAAoB,GAAG;KACrE,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAClG,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClD,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,KAAU;QACrB,MAAM;QACN,UAAU,EAAE,CAAC,CAAC,MAAM,IAAI,MAAM,KAAK,IAAI;QACvC,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,GAAiB,EACjB,IAAa,EACb,KAAsH;IAEtH,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvE,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QAC3B,MAAM,EAAE,CAAA,mCAAmC,KAAK,CAAC,KAAK,SAAS,CAAC;QAChE,MAAM,EAAE,CAAA;;gBAEI,KAAK,CAAC,KAAK,WAAW,KAAK,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAA2B,CAAC,KAAK,MAAM,EAAE,UAAU,IAAI,IAAI;gBAC7G,MAAM,EAAE,EAAE,IAAI,IAAI,KAAK,KAAK,CAAC,SAAS;;;;KAIjD,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAiB,EAAE,KAAa,EAAE,GAAW;IAC9E,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QAC3B,MAAM,EAAE,CAAA,mCAAmC,KAAK,SAAS,CAAC;QAC1D,MAAM,EAAE,CAAA,gDAAgD,KAAK,oBAAoB,GAAG,EAAE,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,GAAiB;IAChD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACzC,MAAM,EAAE,CAAA,kCAAkC,CAAC;QAC3C,OAAO,EAAE,CAAkB,0EAA0E,CAAC;IACxG,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC;AACzB,CAAC"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * GitHub, spoken with a fine-grained token — D88/D95, plan/08 §5.
3
+ *
4
+ * 07 §3.1 decided a GitHub App for the cloud; a self-host will not register
5
+ * one, so the connection here is a token scoped to one repository (Issues:
6
+ * read and write, Metadata: read). Three calls are all this needs: create an
7
+ * issue, move its state, and prove the token can see the repo. Every call
8
+ * carries the API version header, and every failure comes back as a sentence
9
+ * with GitHub's own message in it, because "422" tells nobody what to fix.
10
+ */
11
+ export class GitHubError extends Error {
12
+ status;
13
+ retryable;
14
+ constructor(status, message) {
15
+ super(message);
16
+ this.status = status;
17
+ // 5xx and the two rate-limit answers are "not now"; 4xx are "not like this".
18
+ this.retryable = status >= 500 || status === 429 || status === 403 && /rate limit/i.test(message);
19
+ }
20
+ }
21
+ export function githubClient(opts) {
22
+ const base = (opts.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
23
+ const f = opts.fetch ?? globalThis.fetch;
24
+ const headers = {
25
+ authorization: `Bearer ${opts.token}`,
26
+ accept: "application/vnd.github+json",
27
+ "x-github-api-version": "2022-11-28",
28
+ "user-agent": "snagset",
29
+ };
30
+ async function call(method, path, body) {
31
+ let res;
32
+ try {
33
+ res = await f(`${base}${path}`, {
34
+ method,
35
+ headers: body === undefined ? headers : { ...headers, "content-type": "application/json" },
36
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
37
+ });
38
+ }
39
+ catch (e) {
40
+ throw new GitHubError(0, `could not reach GitHub: ${e instanceof Error ? e.message : String(e)}`);
41
+ }
42
+ if (!res.ok) {
43
+ let detail = "";
44
+ try {
45
+ const j = (await res.json());
46
+ detail = [j.message, ...(j.errors ?? []).map((x) => x.message ?? x.field)].filter(Boolean).join("; ");
47
+ }
48
+ catch {
49
+ /* no body */
50
+ }
51
+ const what = res.status === 401
52
+ ? "the token was refused (revoked, expired, or not a GitHub token)"
53
+ : res.status === 404
54
+ ? "the repository was not found, or the token cannot see it"
55
+ : res.status === 403
56
+ ? "the token is not allowed to do that on this repository"
57
+ : `GitHub answered ${res.status}`;
58
+ throw new GitHubError(res.status, detail ? `${what}: ${detail}` : what);
59
+ }
60
+ return (await res.json());
61
+ }
62
+ return {
63
+ /** Proves the token can see the repo and may write issues. */
64
+ async probe(r) {
65
+ try {
66
+ const repo = await call("GET", `/repos/${enc(r.owner)}/${enc(r.repo)}`);
67
+ return { ok: true, fullName: repo.full_name, canWrite: !!(repo.permissions?.push || repo.permissions?.admin) };
68
+ }
69
+ catch (e) {
70
+ return { ok: false, detail: e instanceof Error ? e.message : String(e) };
71
+ }
72
+ },
73
+ async createIssue(r, issue) {
74
+ const made = await call("POST", `/repos/${enc(r.owner)}/${enc(r.repo)}/issues`, {
75
+ title: issue.title,
76
+ body: issue.body,
77
+ ...(issue.labels?.length ? { labels: issue.labels } : {}),
78
+ });
79
+ return { number: made.number, url: made.html_url };
80
+ },
81
+ /** `closed` with a reason, or `open`. GitHub's own vocabulary; the mapping from ours is the worker's. */
82
+ async setIssueState(r, number, state, reason) {
83
+ await call("PATCH", `/repos/${enc(r.owner)}/${enc(r.repo)}/issues/${number}`, {
84
+ state,
85
+ ...(state === "closed" && reason ? { state_reason: reason } : {}),
86
+ });
87
+ },
88
+ async comment(r, number, body) {
89
+ await call("POST", `/repos/${enc(r.owner)}/${enc(r.repo)}/issues/${number}/comments`, { body });
90
+ },
91
+ };
92
+ }
93
+ const enc = encodeURIComponent;
94
+ /** The issue title — 07 §3.4's algorithm on an already-escaped first line. */
95
+ export function issueTitle(input) {
96
+ let summary = "";
97
+ const raw = (input.firstBody ?? "")
98
+ .replace(/```[\s\S]*?```/g, " ")
99
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
100
+ .replace(/[*_~`>#]/g, "")
101
+ .replace(/\s+/g, " ")
102
+ .trim();
103
+ if (!raw)
104
+ summary = `review comment from ${input.author || "a reviewer"}`;
105
+ else {
106
+ const first = raw.split(/(?<=[.!?])\s+/)[0] ?? raw;
107
+ summary = first.length <= 72 ? first : `${first.slice(0, 71).replace(/\s+\S*$/, "")}…`;
108
+ }
109
+ const route = input.route.length > 32 ? `…${input.route.slice(-31)}` : input.route;
110
+ return `${route} — ${summary}`.slice(0, 256);
111
+ }
112
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/integrations/github.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
+ /**
3
+ * A tracker → us — D96 for GitHub, D97 for Linear: the status half of 07
4
+ * §3.5 and §5.2, for a self-host.
5
+ *
6
+ * NO ECHO. The change is written here, directly, and not through the status
7
+ * PATCH — so it queues no `issue.state` row and cannot bounce back. And when
8
+ * the change is ours to begin with (our worker moved the issue, the tracker
9
+ * tells us it moved), the thread already carries the status and the write
10
+ * is a no-op.
11
+ *
12
+ * REPLAY-PROOF by the sync ledger: `UNIQUE (provider, direction,
13
+ * delivery_id)` — a redelivered delivery id inserts nothing and changes
14
+ * nothing.
15
+ */
16
+ export function verifyGitHubSignature(raw, header, secret) {
17
+ if (!header || !header.startsWith("sha256="))
18
+ return false;
19
+ const given = Buffer.from(header.slice(7), "hex");
20
+ const want = createHmac("sha256", secret).update(raw, "utf8").digest();
21
+ return given.length === want.length && timingSafeEqual(given, want);
22
+ }
23
+ export async function applyIssueState(sql, ev) {
24
+ return sql.begin(async (tx) => {
25
+ await tx `SELECT set_config('app.org_id', ${ev.orgId}, true)`;
26
+ // The ledger row first: a replay stops here, before anything is touched.
27
+ const hash = createHash("sha256").update(`${ev.status}:${ev.externalId}`).digest();
28
+ const [ref] = await tx `
29
+ SELECT thread_id FROM external_refs
30
+ WHERE project_id = ${ev.projectId}::uuid AND provider = ${ev.provider}::snag_provider AND kind = 'issue'
31
+ AND external_id = ${ev.externalId} AND status <> 'detached'
32
+ `;
33
+ const [ledger] = await tx `
34
+ INSERT INTO sync_ledger (id, org_id, project_id, provider, direction, delivery_id, entity_kind, entity_id, external_id, content_hash, thread_id)
35
+ VALUES (${randomUUID()}, ${ev.orgId}::uuid, ${ev.projectId}::uuid, ${ev.provider}::snag_provider, 'inbound', ${ev.deliveryId}, 'status',
36
+ ${ref?.thread_id ?? null}, ${ev.externalId}, ${hash}, ${ref?.thread_id ?? null})
37
+ ON CONFLICT (provider, direction, delivery_id) DO NOTHING
38
+ RETURNING id
39
+ `;
40
+ if (!ledger)
41
+ return { kind: "ignored", why: "replay" };
42
+ if (!ref)
43
+ return { kind: "ignored", why: "no-thread" };
44
+ if (ev.status === "detach") {
45
+ await tx `UPDATE external_refs SET status = 'detached', synced_at = now()
46
+ WHERE thread_id = ${ref.thread_id}::uuid AND provider = ${ev.provider}::snag_provider AND kind = 'issue'`;
47
+ return { kind: "detached", threadId: ref.thread_id };
48
+ }
49
+ const [t] = await tx `
50
+ SELECT status, key, version, context FROM threads WHERE id = ${ref.thread_id}::uuid AND deleted_at IS NULL FOR UPDATE
51
+ `;
52
+ if (!t)
53
+ return { kind: "ignored", why: "no-thread" };
54
+ if (t.status === ev.status)
55
+ return { kind: "ignored", why: "unchanged" };
56
+ const [row] = await tx `
57
+ UPDATE threads
58
+ SET status = ${ev.status}::snag_thread_status,
59
+ resolved_at = ${ev.status === "resolved" ? tx `now()` : tx `NULL`},
60
+ version = version + 1, updated_at = now()
61
+ WHERE id = ${ref.thread_id}::uuid
62
+ RETURNING version
63
+ `;
64
+ return {
65
+ kind: ev.status === "resolved" ? "resolved" : "reopened",
66
+ status: ev.status,
67
+ threadId: ref.thread_id,
68
+ threadKey: t.key,
69
+ version: row?.version ?? t.version + 1,
70
+ pageUrl: String(t.context?.["url"] ?? ""),
71
+ };
72
+ });
73
+ }
74
+ /** GitHub's `issues` actions, mapped: closed resolves (any reason — no archived status since D9), reopened opens, deleted detaches. */
75
+ export async function applyGitHubIssueEvent(sql, ev) {
76
+ const status = ev.action === "closed" ? "resolved" : ev.action === "reopened" ? "open" : ev.action === "deleted" ? "detach" : null;
77
+ if (!status)
78
+ return { kind: "ignored", why: "action" };
79
+ return applyIssueState(sql, {
80
+ provider: "github",
81
+ orgId: ev.orgId,
82
+ projectId: ev.projectId,
83
+ deliveryId: ev.deliveryId,
84
+ externalId: String(ev.issueNumber),
85
+ status,
86
+ });
87
+ }
88
+ //# sourceMappingURL=inbound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound.js","sourceRoot":"","sources":["../../src/integrations/inbound.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export class JiraError extends Error {
3
+ status;
4
+ retryable;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.retryable = status >= 500 || status === 429;
9
+ }
10
+ }
11
+ export function jiraClient(opts) {
12
+ const base = opts.siteUrl.replace(/\/+$/, "");
13
+ const f = opts.fetch ?? globalThis.fetch;
14
+ const headers = {
15
+ authorization: "Basic " + Buffer.from(`${opts.email}:${opts.apiToken}`, "utf8").toString("base64"),
16
+ accept: "application/json",
17
+ "user-agent": "snagset",
18
+ };
19
+ async function call(method, path, body) {
20
+ let res;
21
+ try {
22
+ res = await f(`${base}${path}`, {
23
+ method,
24
+ headers: body === undefined ? headers : { ...headers, "content-type": "application/json" },
25
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
26
+ });
27
+ }
28
+ catch (e) {
29
+ throw new JiraError(0, `could not reach Jira: ${e instanceof Error ? e.message : String(e)}`);
30
+ }
31
+ if (res.status === 204)
32
+ return undefined;
33
+ let parsed = null;
34
+ try {
35
+ parsed = await res.json();
36
+ }
37
+ catch {
38
+ /* no body */
39
+ }
40
+ if (!res.ok) {
41
+ const detail = [
42
+ ...(parsed?.errorMessages ?? []),
43
+ ...Object.entries(parsed?.errors ?? {}).map(([k, v]) => `${k}: ${v}`),
44
+ ].join("; ");
45
+ const what = res.status === 401
46
+ ? "the email and API token were refused"
47
+ : res.status === 403
48
+ ? "the account is not allowed to do that in this project"
49
+ : res.status === 404
50
+ ? "the project was not found, or the account cannot see it"
51
+ : `Jira answered ${res.status}`;
52
+ throw new JiraError(res.status, detail ? `${what}: ${detail}` : what);
53
+ }
54
+ return parsed;
55
+ }
56
+ return {
57
+ /** The account, the project, and an issue type to create with — Task if the project has one. */
58
+ async probe(projectKey) {
59
+ try {
60
+ const me = await call("GET", "/rest/api/2/myself");
61
+ const project = await call("GET", `/rest/api/2/project/${encodeURIComponent(projectKey)}`);
62
+ const types = (project.issueTypes ?? []).filter((t) => !t.subtask);
63
+ const issueType = types.find((t) => /^task$/i.test(t.name)) ?? types.find((t) => /^bug$/i.test(t.name)) ?? types[0];
64
+ if (!issueType)
65
+ return { ok: false, detail: `project ${project.key} has no issue type this account can create` };
66
+ return {
67
+ ok: true,
68
+ me: me.displayName ?? me.emailAddress ?? opts.email,
69
+ project: { id: String(project.id), key: project.key, name: project.name },
70
+ issueType: { id: String(issueType.id), name: issueType.name },
71
+ };
72
+ }
73
+ catch (e) {
74
+ return { ok: false, detail: e instanceof Error ? e.message : String(e) };
75
+ }
76
+ },
77
+ async createIssue(input) {
78
+ const made = await call("POST", "/rest/api/2/issue", {
79
+ fields: {
80
+ project: { id: input.projectId },
81
+ issuetype: { id: input.issueTypeId },
82
+ summary: input.summary.slice(0, 255),
83
+ description: input.description,
84
+ },
85
+ });
86
+ return { id: String(made.id), key: made.key, url: `${base}/browse/${made.key}` };
87
+ },
88
+ /** Move the issue to a status in the category; refused with a sentence when the workflow offers none. */
89
+ async transitionTo(issueKey, category) {
90
+ const list = await call("GET", `/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`);
91
+ const t = list.transitions.find((x) => x.to?.statusCategory?.key === category);
92
+ if (!t) {
93
+ // Already there is not a failure — a workflow offers no transition to
94
+ // the status the issue holds.
95
+ const current = await call("GET", `/rest/api/2/issue/${encodeURIComponent(issueKey)}?fields=status`);
96
+ if (current.fields?.status?.statusCategory?.key === category)
97
+ return;
98
+ throw new JiraError(422, `the project's workflow offers no transition from the issue's current status to a ${category} status`);
99
+ }
100
+ await call("POST", `/rest/api/2/issue/${encodeURIComponent(issueKey)}/transitions`, { transition: { id: t.id } });
101
+ },
102
+ };
103
+ }
104
+ /** Jira Cloud signs a webhook that has a secret with `X-Hub-Signature: sha256=<hex HMAC-SHA256 of the body>`. */
105
+ export function verifyJiraSignature(raw, header, secret) {
106
+ if (!header)
107
+ return false;
108
+ const hex = header.startsWith("sha256=") ? header.slice(7) : header;
109
+ const given = Buffer.from(hex.trim(), "hex");
110
+ const want = createHmac("sha256", secret).update(raw, "utf8").digest();
111
+ return given.length === want.length && timingSafeEqual(given, want);
112
+ }
113
+ /** Our status → the status category to transition to; their category → our status. */
114
+ export function jiraCategoryFor(status) {
115
+ return status === "resolved" ? "done" : status === "progress" ? "indeterminate" : "new";
116
+ }
117
+ export function statusForJiraCategory(category) {
118
+ return category === "done" ? "resolved" : category === "indeterminate" ? "progress" : "open";
119
+ }
120
+ /**
121
+ * Jira's v2 description is wiki markup, which a reviewer's text could steer
122
+ * (`{code}`, `[link|url]`, `!image!`). The brief's own structure is plain
123
+ * Markdown and reads fine as text; the reviewer's words get the few wiki
124
+ * specials escaped after the outbound sanitiser has done its part.
125
+ */
126
+ export function escapeForJiraWiki(s) {
127
+ return s.replace(/([{}\[\]|!])/g, "\\$1");
128
+ }
129
+ //# sourceMappingURL=jira.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jira.js","sourceRoot":"","sources":["../../src/integrations/jira.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export class LinearError extends Error {
3
+ status;
4
+ retryable;
5
+ constructor(status, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.retryable = status >= 500 || status === 429;
9
+ }
10
+ }
11
+ export function linearClient(opts) {
12
+ const url = opts.apiBase ?? "https://api.linear.app/graphql";
13
+ const f = opts.fetch ?? globalThis.fetch;
14
+ async function gql(query, variables = {}) {
15
+ let res;
16
+ try {
17
+ res = await f(url, {
18
+ method: "POST",
19
+ headers: { authorization: opts.apiKey, "content-type": "application/json", "user-agent": "snagset" },
20
+ body: JSON.stringify({ query, variables }),
21
+ });
22
+ }
23
+ catch (e) {
24
+ throw new LinearError(0, `could not reach Linear: ${e instanceof Error ? e.message : String(e)}`);
25
+ }
26
+ let body = {};
27
+ try {
28
+ body = (await res.json());
29
+ }
30
+ catch {
31
+ /* no body */
32
+ }
33
+ if (res.status === 401 || res.status === 403) {
34
+ throw new LinearError(res.status, "the API key was refused (revoked, or not a Linear key)");
35
+ }
36
+ if (!res.ok)
37
+ throw new LinearError(res.status, `Linear answered ${res.status}${body.errors?.[0]?.message ? `: ${body.errors[0].message}` : ""}`);
38
+ if (body.errors?.length) {
39
+ const first = body.errors[0];
40
+ const code = first.extensions?.code ?? "";
41
+ const auth = /AUTHENTICATION|FORBIDDEN|UNAUTHENTICATED/i.test(code);
42
+ throw new LinearError(auth ? 401 : 422, first.message ?? "Linear refused the request");
43
+ }
44
+ if (!body.data)
45
+ throw new LinearError(502, "Linear answered without data");
46
+ return body.data;
47
+ }
48
+ return {
49
+ /** The team by key, and the first state of each type we need — the shape the worker moves issues between. */
50
+ async probe(teamKey) {
51
+ try {
52
+ const data = await gql(`query SnagsetProbe($key: String!) {
53
+ viewer { name }
54
+ teams(filter: { key: { eq: $key } }, first: 1) {
55
+ nodes { id key name states { nodes { id name type position } } }
56
+ }
57
+ }`, { key: teamKey });
58
+ const team = data.teams.nodes[0];
59
+ if (!team)
60
+ return { ok: false, detail: `no team with the key ${teamKey} is visible to this API key (private teams need a member's key)` };
61
+ const byType = (type) => team.states.nodes.filter((s) => s.type === type).sort((a, b) => a.position - b.position)[0];
62
+ const completed = byType("completed");
63
+ const started = byType("started");
64
+ const unstarted = byType("unstarted") ?? byType("backlog") ?? byType("triage");
65
+ if (!completed || !started || !unstarted) {
66
+ return { ok: false, detail: `team ${team.key} lacks a workflow state of type completed, started or unstarted` };
67
+ }
68
+ return {
69
+ ok: true,
70
+ team: { id: team.id, key: team.key, name: team.name },
71
+ states: { completed: completed.id, started: started.id, unstarted: unstarted.id },
72
+ viewer: data.viewer.name,
73
+ };
74
+ }
75
+ catch (e) {
76
+ return { ok: false, detail: e instanceof Error ? e.message : String(e) };
77
+ }
78
+ },
79
+ async createIssue(input) {
80
+ const data = await gql(`mutation SnagsetCreate($input: IssueCreateInput!) {
81
+ issueCreate(input: $input) { success issue { id identifier url } }
82
+ }`, { input: { teamId: input.teamId, title: input.title, description: input.description, ...(input.stateId ? { stateId: input.stateId } : {}) } });
83
+ if (!data.issueCreate.success || !data.issueCreate.issue)
84
+ throw new LinearError(502, "Linear did not create the issue");
85
+ return data.issueCreate.issue;
86
+ },
87
+ async setState(issueId, stateId) {
88
+ const data = await gql(`mutation SnagsetState($id: String!, $input: IssueUpdateInput!) {
89
+ issueUpdate(id: $id, input: $input) { success }
90
+ }`, { id: issueId, input: { stateId } });
91
+ if (!data.issueUpdate.success)
92
+ throw new LinearError(502, "Linear did not move the issue");
93
+ },
94
+ };
95
+ }
96
+ /** 07 §5.3: `Linear-Signature` is the hex HMAC-SHA256 of the raw body with the webhook's signing secret. */
97
+ export function verifyLinearSignature(raw, header, secret) {
98
+ if (!header)
99
+ return false;
100
+ const given = Buffer.from(header.trim(), "hex");
101
+ const want = createHmac("sha256", secret).update(raw, "utf8").digest();
102
+ return given.length === want.length && timingSafeEqual(given, want);
103
+ }
104
+ /** Our status → the team's state (by TYPE); their state type → our status. */
105
+ export function linearStateFor(status, states) {
106
+ return status === "resolved" ? states.completed : status === "progress" ? states.started : states.unstarted;
107
+ }
108
+ export function statusForLinearState(type) {
109
+ return type === "completed" || type === "canceled" ? "resolved" : type === "started" ? "progress" : "open";
110
+ }
111
+ //# sourceMappingURL=linear.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear.js","sourceRoot":"","sources":["../../src/integrations/linear.ts"],"names":[],"mappings":"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"}