@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.
- package/README.md +4 -3
- package/dist/app.js +167 -30
- package/dist/app.js.map +1 -1
- package/dist/bin.js +46 -9
- package/dist/bin.js.map +1 -1
- package/dist/boot.js +105 -54
- package/dist/boot.js.map +1 -1
- package/dist/config.generated.js +5 -2
- package/dist/config.generated.js.map +1 -1
- package/dist/db/connect.js +7 -0
- package/dist/db/connect.js.map +1 -1
- package/dist/db/migrations/0005_studio_reviewers.sql +45 -0
- package/dist/db/migrations/0006_instance_settings.sql +69 -0
- package/dist/db/migrations/0007_jira_provider.no-txn.sql +13 -0
- package/dist/domain.js +129 -0
- package/dist/domain.js.map +1 -0
- package/dist/instance/settings.js +54 -0
- package/dist/instance/settings.js.map +1 -0
- package/dist/integrations/github.js +112 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/inbound.js +88 -0
- package/dist/integrations/inbound.js.map +1 -0
- package/dist/integrations/jira.js +129 -0
- package/dist/integrations/jira.js.map +1 -0
- package/dist/integrations/linear.js +111 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/outbox.js +305 -0
- package/dist/integrations/outbox.js.map +1 -0
- package/dist/integrations/slack.js +135 -0
- package/dist/integrations/slack.js.map +1 -0
- package/dist/integrations/store.js +101 -0
- package/dist/integrations/store.js.map +1 -0
- package/dist/mail/index.js +21 -11
- package/dist/mail/index.js.map +1 -1
- package/dist/mail/resolve.js +80 -0
- package/dist/mail/resolve.js.map +1 -0
- package/dist/projectCache.js +3 -2
- package/dist/projectCache.js.map +1 -1
- package/dist/projects.js +8 -2
- package/dist/projects.js.map +1 -1
- package/dist/routes/account.js +59 -0
- package/dist/routes/account.js.map +1 -0
- package/dist/routes/activity.js +111 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/anchorReports.js +196 -0
- package/dist/routes/anchorReports.js.map +1 -0
- package/dist/routes/assignees.js +139 -0
- package/dist/routes/assignees.js.map +1 -0
- package/dist/routes/attachments.js +2 -2
- package/dist/routes/attachments.js.map +1 -1
- package/dist/routes/bodyLimit.js +60 -0
- package/dist/routes/bodyLimit.js.map +1 -0
- package/dist/routes/comments.js +27 -17
- package/dist/routes/comments.js.map +1 -1
- package/dist/routes/cors.js +10 -1
- package/dist/routes/cors.js.map +1 -1
- package/dist/routes/instance.js +164 -0
- package/dist/routes/instance.js.map +1 -0
- package/dist/routes/integrations.js +610 -0
- package/dist/routes/integrations.js.map +1 -0
- package/dist/routes/jobs.js +15 -3
- package/dist/routes/jobs.js.map +1 -1
- package/dist/routes/mcpTokens.js +119 -0
- package/dist/routes/mcpTokens.js.map +1 -0
- package/dist/routes/members.js +177 -0
- package/dist/routes/members.js.map +1 -0
- package/dist/routes/owner.js +64 -7
- package/dist/routes/owner.js.map +1 -1
- package/dist/routes/sites.js +181 -8
- package/dist/routes/sites.js.map +1 -1
- package/dist/routes/threads.js +276 -9
- package/dist/routes/threads.js.map +1 -1
- package/dist/serverless.js +39 -6
- package/dist/serverless.js.map +1 -1
- package/dist/studioReviewers.js +67 -0
- package/dist/studioReviewers.js.map +1 -0
- package/dist/vendor/adapters/outbound/sanitise.js +18 -3
- package/dist/vendor/types/brief.js +121 -0
- package/package.json +1 -1
- package/static/dashboard/assets/CompositeItem-CoPuxIvr.js +1 -0
- package/static/dashboard/assets/ConfirmDialog-DkCcJwll.js +1 -0
- package/static/dashboard/assets/CopyButton-BNa2pLLv.js +1 -0
- package/static/dashboard/assets/FieldItemContext-DQXjpFtW.js +1 -0
- package/static/dashboard/assets/InstallTag-SOlWl7Hx.js +1 -0
- package/static/dashboard/assets/SaveState-CmxB4XyX.js +6 -0
- package/static/dashboard/assets/_app-DNkv3HBi.js +1 -0
- package/static/dashboard/assets/_app-xVQe811c.js +56 -0
- package/static/dashboard/assets/activity-DsMa5Ja6.js +1 -0
- package/static/dashboard/assets/arrow-left-vQV4xhAD.js +6 -0
- package/static/dashboard/assets/avatar-DuPL6QD_.js +1 -0
- package/static/dashboard/assets/badge-BWjiIc8T.js +1 -0
- package/static/dashboard/assets/card-BIBfCno8.js +1 -0
- package/static/dashboard/assets/check-Dpx6GTdT.js +6 -0
- package/static/dashboard/assets/chevron-right-DKIlBUGN.js +6 -0
- package/static/dashboard/assets/copy-DCbhEe2L.js +6 -0
- package/static/dashboard/assets/createBaseUIEventDetails-C_ZVkrgp.js +1 -0
- package/static/dashboard/assets/createLucideIcon-C_IGE4WD.js +36 -0
- package/static/dashboard/assets/dialog-BT2uWf4e.js +6 -0
- package/static/dashboard/assets/dropdown-menu-DSXzPAwB.js +11 -0
- package/static/dashboard/assets/image-off-DGWejLDG.js +6 -0
- package/static/dashboard/assets/index-BRTBtV91.css +1 -0
- package/static/dashboard/assets/index-D6FWZoJT.js +129 -0
- package/static/dashboard/assets/label-9QsSpLMA.js +1 -0
- package/static/dashboard/assets/labels--UUyt9uB.js +1 -0
- package/static/dashboard/assets/layout-grid-UPGEmEw-.js +6 -0
- package/static/dashboard/assets/me-0HQsBXLU.js +1 -0
- package/static/dashboard/assets/parts-C29uZ5zo.js +1 -0
- package/static/dashboard/assets/plus-BFNS5LcY.js +6 -0
- package/static/dashboard/assets/s._site-D6Ep2tdc.js +1 -0
- package/static/dashboard/assets/s._site.comments-BRVI5gpJ.js +39 -0
- package/static/dashboard/assets/s._site.reviewers-CiEkZnnp.js +1 -0
- package/static/dashboard/assets/s._site.settings-BqLGyfEd.js +1 -0
- package/static/dashboard/assets/setup-dUMCPWdf.js +1 -0
- package/static/dashboard/assets/sign-in-CdRa-bio.js +1 -0
- package/static/dashboard/assets/sites-CmbtFKoo.js +1 -0
- package/static/dashboard/assets/studio-BHuVXWZh.js +1 -0
- package/static/dashboard/assets/studio-BjeTOKda.js +1 -0
- package/static/dashboard/assets/studio.index--kuzOs7w.js +1 -0
- package/static/dashboard/assets/studio.integrations-Bq6HP80a.js +1 -0
- package/static/dashboard/assets/studio.members-8AB1KYsw.js +1 -0
- package/static/dashboard/assets/sun-BCiTcgnh.js +16 -0
- package/static/dashboard/assets/table-BXmeI_yA.js +1 -0
- package/static/dashboard/assets/update-duXzIzFS.js +1 -0
- package/static/dashboard/assets/useBaseQuery-Cp9wivVR.js +1 -0
- package/static/dashboard/assets/useQuery-BZXZlOqO.js +1 -0
- package/static/dashboard/assets/useRender-CO8aPW06.js +1 -0
- package/static/dashboard/assets/users-Dvw2FCKM.js +6 -0
- package/static/dashboard/assets/welcome-BHKjw20b.js +11 -0
- package/static/dashboard/assets/you-RBpAfUMU.js +1 -0
- package/static/dashboard/assets/zod-BpyjQIvb.js +1 -0
- package/static/dashboard/index.html +17 -2
- package/static/widget/snag.capture.js +1 -1
- package/static/widget/snag.dock.js +1 -1
- package/static/widget/snag.panels.js +1 -1
- package/static/widget/snag.runtime.js +2 -2
- package/static/widget/snag.text.js +1 -1
- package/vercel.json +4 -0
- package/static/dashboard/assets/index-DsUJ96yc.css +0 -1
- package/static/dashboard/assets/index-DultqpS7.js +0 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
-- ───────────────────────────────────────
|
|
2
|
+
-- Instance settings — D89, plan/08 §5b: what the dashboard connected.
|
|
3
|
+
--
|
|
4
|
+
-- "If added via the environment variables it takes those and doesn't allow
|
|
5
|
+
-- to change in the dashboard, but if no env variables then we can add and
|
|
6
|
+
-- connect via the general settings." The environment wins and locks; this
|
|
7
|
+
-- table is where the dashboard's half lives. One row per key per org, the
|
|
8
|
+
-- non-secret half in `value` and the secret sealed exactly as
|
|
9
|
+
-- `orgs.storage_credentials` and `integrations.credentials` are — AES-256-GCM
|
|
10
|
+
-- under HKDF(SNAGSET_SECRET_KEY, 'credential-enc') — so there is one way to
|
|
11
|
+
-- hold a secret at rest rather than three.
|
|
12
|
+
--
|
|
13
|
+
-- Mail is the first key (`mail`: the transport URL sealed, the From address
|
|
14
|
+
-- and the transport's shape in `value`); the trackers' credentials follow
|
|
15
|
+
-- with phase 7. Storage stays on the `orgs.storage_*` columns it had before
|
|
16
|
+
-- this table existed: moving it would be a migration of secrets for no gain.
|
|
17
|
+
--
|
|
18
|
+
-- Under D71, §3 describes the CURRENT schema, so the table is added there in
|
|
19
|
+
-- the same commit and `pnpm schema:diff:check` asserts the two agree.
|
|
20
|
+
--
|
|
21
|
+
-- THE POLICY, TWICE. The generator renders every policy in 0001's shape, and
|
|
22
|
+
-- 0002's rewrite (NULLIF and the maintenance claim) runs only over the
|
|
23
|
+
-- `public` schema — in the diff tool's scratch schema it touches nothing, so
|
|
24
|
+
-- both sides compare policies in 0001's shape. This file does the same as
|
|
25
|
+
-- 0002 did for every earlier table: create the policy in 0001's shape, then
|
|
26
|
+
-- rewrite it in place, scoped to this table and to `public`. On a real
|
|
27
|
+
-- database the second step lands; in the diff's scratch schema it is inert,
|
|
28
|
+
-- and the two agree. Guarded against running twice, unlike 0002, because a
|
|
29
|
+
-- double NULLIF is what a re-run produced while this was being written.
|
|
30
|
+
CREATE TABLE instance_settings (
|
|
31
|
+
org_id uuid NOT NULL REFERENCES orgs(id) ON DELETE CASCADE,
|
|
32
|
+
key text NOT NULL CHECK (key ~ '^[a-z][a-z0-9_.-]{0,63}$'),
|
|
33
|
+
value jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
34
|
+
secret bytea,
|
|
35
|
+
secret_iv bytea,
|
|
36
|
+
updated_by uuid REFERENCES users(id) ON DELETE SET NULL,
|
|
37
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
38
|
+
PRIMARY KEY (org_id, key),
|
|
39
|
+
CHECK ((secret IS NULL) = (secret_iv IS NULL))
|
|
40
|
+
);
|
|
41
|
+
ALTER TABLE instance_settings ENABLE ROW LEVEL SECURITY;
|
|
42
|
+
ALTER TABLE instance_settings FORCE ROW LEVEL SECURITY;
|
|
43
|
+
CREATE POLICY instance_settings_org ON instance_settings
|
|
44
|
+
USING (org_id = current_setting('app.org_id', true)::uuid);
|
|
45
|
+
|
|
46
|
+
DO $$
|
|
47
|
+
DECLARE
|
|
48
|
+
existing text;
|
|
49
|
+
BEGIN
|
|
50
|
+
SELECT pg_get_expr(pol.polqual, pol.polrelid) INTO existing
|
|
51
|
+
FROM pg_policy pol
|
|
52
|
+
JOIN pg_class c ON c.oid = pol.polrelid
|
|
53
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
54
|
+
WHERE n.nspname = 'public' AND c.relname = 'instance_settings' AND pol.polname = 'instance_settings_org';
|
|
55
|
+
IF existing IS NULL OR existing LIKE '%NULLIF%' THEN
|
|
56
|
+
RETURN;
|
|
57
|
+
END IF;
|
|
58
|
+
existing := replace(
|
|
59
|
+
existing,
|
|
60
|
+
'current_setting(''app.org_id''::text, true)',
|
|
61
|
+
'NULLIF(current_setting(''app.org_id''::text, true), ''''::text)'
|
|
62
|
+
);
|
|
63
|
+
IF existing NOT LIKE '%app.maintenance%' THEN
|
|
64
|
+
existing := existing || ' OR current_setting(''app.maintenance'', true) = ''on''';
|
|
65
|
+
END IF;
|
|
66
|
+
EXECUTE 'DROP POLICY instance_settings_org ON public.instance_settings';
|
|
67
|
+
EXECUTE format('CREATE POLICY instance_settings_org ON public.instance_settings USING (%s)', existing);
|
|
68
|
+
END
|
|
69
|
+
$$;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- ───────────────────────────────────────
|
|
2
|
+
-- Jira joins the providers — D88 named it, D98 built it (2026-09-03).
|
|
3
|
+
--
|
|
4
|
+
-- `snag_provider` was closed at github, linear, slack and webhook in 0001;
|
|
5
|
+
-- Jira Cloud was not in the original set and Faster Fixes made it a parity
|
|
6
|
+
-- item. ADD VALUE cannot run inside a transaction block on every Postgres
|
|
7
|
+
-- this ships on, and the new value cannot be used in the transaction that
|
|
8
|
+
-- added it either — hence `.no-txn`, which migrate.ts runs on the bare
|
|
9
|
+
-- connection, and hence a file of its own before anything writes 'jira'.
|
|
10
|
+
--
|
|
11
|
+
-- Under D71, §3.1's enum is amended in the same commit and
|
|
12
|
+
-- `pnpm schema:diff:check` asserts the two agree.
|
|
13
|
+
ALTER TYPE snag_provider ADD VALUE IF NOT EXISTS 'jira';
|
package/dist/domain.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A site's DOMAIN — plan/08 §3, D92, borrowed from Faster Fixes's one-field
|
|
3
|
+
* setup: the studio types `acme.com` once and the widget works on
|
|
4
|
+
* `https://acme.com`, `https://staging.acme.com`, `http://preview.acme.com:3000`
|
|
5
|
+
* and on localhost, with no list of origins to maintain. Exact origins stay
|
|
6
|
+
* as "also allowed" for the cases a domain cannot say (a client's separate
|
|
7
|
+
* marketing host, a numbered preview on somebody else's suffix).
|
|
8
|
+
*
|
|
9
|
+
* The rule, and the two boundaries that make it safe:
|
|
10
|
+
* · the origin's HOSTNAME equals the domain, or ends with "." + domain — a
|
|
11
|
+
* dot boundary, so `notacme.com` never matches `acme.com`;
|
|
12
|
+
* · any scheme and any port, because a staging server on :3000 is the same
|
|
13
|
+
* site as the one on :443, and the client's own site is the trust anchor
|
|
14
|
+
* (05 §4.3's "scheme and port must match" was written for exact origins,
|
|
15
|
+
* where a mismatch means a different origin; a domain is a claim about a
|
|
16
|
+
* host, and the host is what the client owns);
|
|
17
|
+
* · never a public suffix: `vercel.app` would allow every site on Vercel,
|
|
18
|
+
* which is the subdomain-takeover class §4.3 refuses for wildcards.
|
|
19
|
+
*/
|
|
20
|
+
/** Suffixes shared by strangers — the ones §4.3 names, plus the usual PaaS hosts. */
|
|
21
|
+
export const SHARED_SUFFIXES = [
|
|
22
|
+
"vercel.app",
|
|
23
|
+
"netlify.app",
|
|
24
|
+
"pages.dev",
|
|
25
|
+
"github.io",
|
|
26
|
+
"gitlab.io",
|
|
27
|
+
"workers.dev",
|
|
28
|
+
"ngrok-free.app",
|
|
29
|
+
"ngrok.io",
|
|
30
|
+
"ngrok.app",
|
|
31
|
+
"herokuapp.com",
|
|
32
|
+
"fly.dev",
|
|
33
|
+
"onrender.com",
|
|
34
|
+
"render.com",
|
|
35
|
+
"web.app",
|
|
36
|
+
"firebaseapp.com",
|
|
37
|
+
"azurewebsites.net",
|
|
38
|
+
"azurestaticapps.net",
|
|
39
|
+
"amazonaws.com",
|
|
40
|
+
"cloudfront.net",
|
|
41
|
+
"surge.sh",
|
|
42
|
+
"glitch.me",
|
|
43
|
+
"repl.co",
|
|
44
|
+
"replit.app",
|
|
45
|
+
"webflow.io",
|
|
46
|
+
"framer.app",
|
|
47
|
+
"framer.website",
|
|
48
|
+
"wixsite.com",
|
|
49
|
+
"squarespace.com",
|
|
50
|
+
"myshopify.com",
|
|
51
|
+
"wordpress.com",
|
|
52
|
+
"blogspot.com",
|
|
53
|
+
"github.dev",
|
|
54
|
+
"codesandbox.io",
|
|
55
|
+
"stackblitz.io",
|
|
56
|
+
"localhost",
|
|
57
|
+
];
|
|
58
|
+
const HOST = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
|
|
59
|
+
/**
|
|
60
|
+
* What the studio typed → the domain to store, or a sentence.
|
|
61
|
+
* Accepts a bare host or a full address (the hostname is taken), lower-cases,
|
|
62
|
+
* strips a trailing dot and a leading `www.` — `www.acme.com` and `acme.com`
|
|
63
|
+
* are one site, and the shorter one covers both.
|
|
64
|
+
*/
|
|
65
|
+
export function normaliseDomain(input) {
|
|
66
|
+
let raw = input.trim().toLowerCase();
|
|
67
|
+
if (!raw)
|
|
68
|
+
return { error: "Type the site's domain, like acme.com." };
|
|
69
|
+
if (raw.includes("://")) {
|
|
70
|
+
try {
|
|
71
|
+
raw = new URL(raw).hostname;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { error: `${JSON.stringify(input.trim())} is not an address. A domain looks like acme.com.` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
raw = raw.replace(/^\/+/, "").split(/[/?#:]/)[0] ?? "";
|
|
79
|
+
}
|
|
80
|
+
raw = raw.replace(/\.$/, "").replace(/^www\./, "");
|
|
81
|
+
if (raw === "localhost" || /^\d{1,3}(\.\d{1,3}){3}$/.test(raw) || raw.startsWith("[")) {
|
|
82
|
+
return { error: "localhost and IP addresses are always allowed while developing — the domain is the site's real host, like acme.com." };
|
|
83
|
+
}
|
|
84
|
+
if (!HOST.test(raw))
|
|
85
|
+
return { error: `${JSON.stringify(input.trim())} is not a domain. It looks like acme.com or staging.acme.com.` };
|
|
86
|
+
if (isSharedSuffix(raw)) {
|
|
87
|
+
return {
|
|
88
|
+
error: `${raw} is a host shared by everyone who deploys there. Name your own subdomain (like kilnwork.${raw}) or add the exact address under "Also allowed".`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return { domain: raw };
|
|
92
|
+
}
|
|
93
|
+
/** A bare shared suffix, or a bare top-level label — never a site of its own. */
|
|
94
|
+
export function isSharedSuffix(host) {
|
|
95
|
+
if (!host.includes("."))
|
|
96
|
+
return true;
|
|
97
|
+
return SHARED_SUFFIXES.includes(host);
|
|
98
|
+
}
|
|
99
|
+
/** Does this browser origin belong to the domain? Any scheme, any port, dot boundary on the host. */
|
|
100
|
+
export function originMatchesDomain(domain, origin) {
|
|
101
|
+
if (!domain)
|
|
102
|
+
return false;
|
|
103
|
+
let host;
|
|
104
|
+
try {
|
|
105
|
+
host = new URL(origin.toLowerCase()).hostname;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return host === domain || host.endsWith("." + domain);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 05 §4.3 rule 4 — the developer's own machine, allowed by default:
|
|
114
|
+
* http(s)://localhost[:port], 127.0.0.1, [::1], and *.localhost.
|
|
115
|
+
*/
|
|
116
|
+
export function isLocalOrigin(origin) {
|
|
117
|
+
let u;
|
|
118
|
+
try {
|
|
119
|
+
u = new URL(origin.toLowerCase());
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
125
|
+
return false;
|
|
126
|
+
const h = u.hostname;
|
|
127
|
+
return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || h === "[::1]";
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"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"}
|
|
@@ -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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"}
|