@voltro/plugin-atlassian 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/dist/index.js ADDED
@@ -0,0 +1,377 @@
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./types-CVyqPJzY.js";
2
+ import { Effect as o, Layer as s, Option as c } from "effect";
3
+ import { SubjectService as l, definePlugin as u } from "@voltro/protocol";
4
+ import { DEFAULT_POLICY as d, DEFAULT_POLICY as f, assertSameHost as p, request as m, requestJson as h } from "@voltro/integration-http";
5
+ //#region src/auth.ts
6
+ var g = "voltro", _ = (e, t = {}) => {
7
+ let n = {
8
+ authorization: `Bearer ${e.token}`,
9
+ "x-pat-application": e.patApplication,
10
+ "x-pat-environment": e.patEnvironment
11
+ };
12
+ return t.binary || (n.accept = "application/json"), t.json && (n["content-type"] = "application/json"), n;
13
+ }, v = "/_voltro/atlassian/avatar", y = (e, t) => ({
14
+ status: e,
15
+ body: t,
16
+ contentType: "text/plain"
17
+ }), b = { "cache-control": "public, max-age=31536000, immutable" }, x = (e) => e?.["48x48"] ?? e?.["32x32"] ?? e?.["24x24"] ?? e?.["16x16"] ?? (e ? Object.values(e)[0] : void 0), S = (e) => {
18
+ let t = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(e);
19
+ if (!t) return null;
20
+ let n = t[1] || "image/png";
21
+ return {
22
+ bytes: t[2] ? new Uint8Array(Buffer.from(t[3], "base64")) : new TextEncoder().encode(decodeURIComponent(t[3])),
23
+ contentType: n
24
+ };
25
+ }, C = (e) => {
26
+ let t = e.pathPrefix ?? "/_voltro/atlassian/avatar";
27
+ return {
28
+ method: "GET",
29
+ path: t,
30
+ handle: async (n) => {
31
+ let r = decodeURIComponent(n.path.slice(t.length).replace(/^\//, ""));
32
+ if (!r) return y(400, "missing avatar ref");
33
+ let i = await e.resolveCredentials(), a = i.baseUrl.replace(/\/$/, ""), o = encodeURIComponent, s = [
34
+ `${a}/rest/api/2/user?accountId=${o(r)}`,
35
+ `${a}/rest/api/2/user?username=${o(r)}`,
36
+ `${a}/rest/api/2/user?key=${o(r)}`
37
+ ], c;
38
+ try {
39
+ for (let t of s) {
40
+ if (!p(i.baseUrl, t)) continue;
41
+ let n = await e.fetchImpl(t, {
42
+ method: "GET",
43
+ headers: _(i)
44
+ });
45
+ if (!(n.status < 200 || n.status >= 300) && (c = x(JSON.parse(await n.text()).avatarUrls), c)) break;
46
+ }
47
+ } catch (e) {
48
+ return y(502, `avatar user lookup failed: ${String(e)}`);
49
+ }
50
+ if (!c) return y(404, "no avatar");
51
+ if (c.startsWith("data:")) {
52
+ let e = S(c);
53
+ return !e || !e.contentType.startsWith("image/") ? y(415, "not an image") : {
54
+ status: 200,
55
+ body: e.bytes,
56
+ contentType: e.contentType,
57
+ headers: b
58
+ };
59
+ }
60
+ let l = c.startsWith("http") ? c : `${a}${c.startsWith("/") ? "" : "/"}${c}`;
61
+ if (!p(i.baseUrl, l) && !l.startsWith("https://")) return y(400, "blocked off-host avatar request");
62
+ try {
63
+ let t = await e.fetchImpl(l, {
64
+ method: "GET",
65
+ headers: _(i, { binary: !0 })
66
+ });
67
+ if (t.status < 200 || t.status >= 300) return y(502, `upstream ${t.status}`);
68
+ let n = t.headers.get("content-type") ?? "";
69
+ return n.startsWith("image/") ? {
70
+ status: 200,
71
+ body: new Uint8Array(await t.arrayBuffer()),
72
+ contentType: n,
73
+ headers: b
74
+ } : y(415, "not an image");
75
+ } catch (e) {
76
+ return y(502, `avatar fetch failed: ${String(e)}`);
77
+ }
78
+ }
79
+ };
80
+ }, w = (e) => new i({
81
+ message: e.message,
82
+ transient: e.transient,
83
+ ...e.status === void 0 ? {} : { status: e.status },
84
+ ...e.code === void 0 ? {} : { code: e.code }
85
+ }), T = encodeURIComponent, E = (e) => {
86
+ let t = (t, n, r) => o.gen(function* () {
87
+ let a = yield* l, s = yield* e.credentialsResolver(a).pipe(o.mapError((e) => new i({
88
+ message: e.message,
89
+ transient: e.transient,
90
+ ...e.status === void 0 ? {} : { status: e.status },
91
+ ...e.code === void 0 ? {} : { code: e.code }
92
+ })));
93
+ return yield* h({
94
+ fetchImpl: e.fetchImpl,
95
+ baseUrl: s.baseUrl,
96
+ method: t,
97
+ path: n,
98
+ headers: _(s, { json: r !== void 0 }),
99
+ ...r === void 0 ? {} : { body: JSON.stringify(r) },
100
+ policy: e.policy,
101
+ makeError: w,
102
+ ...e.now ? { now: e.now } : {}
103
+ });
104
+ });
105
+ return {
106
+ getContent: (e, n) => {
107
+ let r = n?.expand?.length ? n.expand.join(",") : "body.view,version";
108
+ return t("GET", `/rest/api/content/${T(e)}?expand=${T(r)}`);
109
+ },
110
+ searchContent: (e, n) => {
111
+ let r = n?.expand?.length ? n.expand.join(",") : "version,space", i = n?.limit === void 0 ? "" : `&limit=${n.limit}`;
112
+ return t("GET", `/rest/api/content/search?cql=${T(e)}&expand=${T(r)}${i}`).pipe(o.map((e) => e.results ?? []));
113
+ },
114
+ getSpace: (e) => t("GET", `/rest/api/space/${T(e)}`),
115
+ createContent: (e) => t("POST", "/rest/api/content", e),
116
+ updateContent: (e, n, r) => t("PUT", `/rest/api/content/${T(e)}`, {
117
+ ...n,
118
+ version: { number: r.version }
119
+ }),
120
+ getAttachments: (e) => t("GET", `/rest/api/content/${T(e)}/child/attachment`).pipe(o.map((e) => e.results ?? [])),
121
+ addComment: (e, n) => t("POST", "/rest/api/content", {
122
+ type: "comment",
123
+ container: {
124
+ id: e,
125
+ type: "page"
126
+ },
127
+ body: { storage: {
128
+ value: n,
129
+ representation: "storage"
130
+ } }
131
+ }),
132
+ downloadImage: (t) => o.gen(function* () {
133
+ let n = yield* l, r = yield* e.credentialsResolver(n).pipe(o.mapError((e) => new i({
134
+ message: e.message,
135
+ transient: e.transient,
136
+ ...e.status === void 0 ? {} : { status: e.status },
137
+ ...e.code === void 0 ? {} : { code: e.code }
138
+ }))), a = t.startsWith("http") ? `${new URL(t).pathname}${new URL(t).search}` : t, s = yield* m({
139
+ fetchImpl: e.fetchImpl,
140
+ baseUrl: r.baseUrl,
141
+ method: "GET",
142
+ path: a,
143
+ headers: _(r, { json: !1 }),
144
+ policy: e.policy,
145
+ makeError: w,
146
+ ...e.now ? { now: e.now } : {}
147
+ });
148
+ return s.contentType !== null && !s.contentType.startsWith("image/") ? yield* o.fail(w({
149
+ message: `not an image: ${s.contentType}`,
150
+ transient: !1
151
+ })) : {
152
+ bytes: s.bytes,
153
+ contentType: s.contentType
154
+ };
155
+ })
156
+ };
157
+ }, D = (e, t, n, r, i) => {
158
+ if (!e) return i;
159
+ let a = `atlassian:${t}:${n ?? "_"}:${r}`, s = e.ttlMs?.[t];
160
+ return o.gen(function* () {
161
+ let t = yield* e.store.get(a).pipe(o.orElseSucceed(() => c.none()));
162
+ if (c.isSome(t)) return t.value;
163
+ let n = yield* i;
164
+ return yield* e.store.set(a, n, s === void 0 ? void 0 : { ttlMs: s }).pipe(o.ignore), n;
165
+ });
166
+ }, O = (t) => new e({
167
+ message: t.message,
168
+ transient: t.transient,
169
+ ...t.status === void 0 ? {} : { status: t.status },
170
+ ...t.code === void 0 ? {} : { code: t.code }
171
+ }), k = encodeURIComponent, A = (e) => {
172
+ let t = (t, n, r) => o.gen(function* () {
173
+ let i = yield* l, a = yield* e.credentialsResolver(i);
174
+ return yield* h({
175
+ fetchImpl: e.fetchImpl,
176
+ baseUrl: a.baseUrl,
177
+ method: t,
178
+ path: n,
179
+ headers: _(a, { json: r !== void 0 }),
180
+ ...r === void 0 ? {} : { body: JSON.stringify(r) },
181
+ policy: e.policy,
182
+ makeError: O,
183
+ ...e.now ? { now: e.now } : {}
184
+ });
185
+ }), n = o.gen(function* () {
186
+ return (yield* l).tenantId ?? null;
187
+ }), r = (r, i, a) => o.gen(function* () {
188
+ let o = yield* n;
189
+ return yield* D(e.cache, r, o, i, t("GET", a));
190
+ });
191
+ return {
192
+ searchIssues: (e, n) => o.gen(function* () {
193
+ let r = n?.maxResults ?? 100, i = n?.fields, a = [], o = n?.startAt ?? 0;
194
+ for (;;) {
195
+ let n = yield* t("POST", "/rest/api/2/search", {
196
+ jql: e,
197
+ startAt: o,
198
+ maxResults: r,
199
+ ...i ? { fields: i } : {}
200
+ }), s = n.issues ?? [];
201
+ a.push(...s);
202
+ let c = n.total ?? a.length;
203
+ if (o += s.length, s.length === 0 || a.length >= c) break;
204
+ }
205
+ return a;
206
+ }),
207
+ getGreenhopperBoard: (e, n) => o.gen(function* () {
208
+ let r = n?.quickFilterId === void 0 ? "" : `&activeQuickFilters=${k(String(n.quickFilterId))}`, i = yield* t("GET", `/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=${k(String(e))}${r}`);
209
+ return {
210
+ issues: i.issuesData?.issues ?? [],
211
+ columns: i.columnsData?.columns ?? [],
212
+ swimlanes: i.swimlanesData?.swimlanes ?? [],
213
+ raw: i
214
+ };
215
+ }),
216
+ getBoardConfig: (e) => r("boardConfig", String(e), `/rest/greenhopper/1.0/rapidviewconfig/editmodel?rapidViewId=${k(String(e))}`),
217
+ getActiveSprint: (e) => t("GET", `/rest/agile/1.0/board/${k(String(e))}/sprint?state=active`).pipe(o.map((e) => {
218
+ let t = (e.values ?? [])[0];
219
+ return t ? {
220
+ id: t.id,
221
+ name: t.name
222
+ } : null;
223
+ }), o.catchAll(() => o.succeed(null))),
224
+ getSprintIssues: (e) => t("GET", `/rest/agile/1.0/sprint/${k(String(e))}/issue`).pipe(o.map((e) => e.issues ?? [])),
225
+ getSprintInfo: (e) => t("GET", `/rest/agile/1.0/sprint/${k(String(e))}`),
226
+ getIssue: (e, n) => {
227
+ let r = [];
228
+ n?.expand?.length && r.push(`expand=${k(n.expand.join(","))}`), n?.fields?.length && r.push(`fields=${k(n.fields.join(","))}`);
229
+ let i = r.length ? `?${r.join("&")}` : "";
230
+ return t("GET", `/rest/api/2/issue/${k(e)}${i}`);
231
+ },
232
+ getIssueChangelog: (e) => t("GET", `/rest/api/2/issue/${k(e)}?expand=changelog`),
233
+ getComments: (e) => t("GET", `/rest/api/2/issue/${k(e)}/comment`).pipe(o.map((e) => e.comments ?? [])),
234
+ addComment: (e, n) => t("POST", `/rest/api/2/issue/${k(e)}/comment`, { body: n }),
235
+ getLinkedIssues: (e) => t("GET", `/rest/api/2/issue/${k(e)}?fields=issuelinks`).pipe(o.map((e) => (e.fields?.issuelinks ?? []).map((e) => e.inwardIssue ?? e.outwardIssue).filter((e) => e !== void 0))),
236
+ getUser: (e) => {
237
+ let n = e.key === void 0 ? `/rest/api/2/user/search?username=${k(e.username ?? "")}` : `/rest/api/2/user?key=${k(e.key)}`;
238
+ return t("GET", n);
239
+ },
240
+ getMyself: () => t("GET", "/rest/api/2/myself"),
241
+ getIssueTransitions: (e) => t("GET", `/rest/api/2/issue/${k(e)}/transitions`).pipe(o.map((e) => e.transitions ?? [])),
242
+ transitionIssue: (e, n, r) => t("POST", `/rest/api/2/issue/${k(e)}/transitions`, {
243
+ transition: { id: n },
244
+ ...r?.fields ? { fields: r.fields } : {}
245
+ }).pipe(o.asVoid),
246
+ createIssue: (e) => t("POST", "/rest/api/2/issue", e),
247
+ updateIssue: (e, n) => t("PUT", `/rest/api/2/issue/${k(e)}`, n).pipe(o.asVoid),
248
+ getProject: (e) => r("project", e, `/rest/api/2/project/${k(e)}`),
249
+ getFields: () => r("field", "all", "/rest/api/2/field").pipe(o.map((e) => Array.isArray(e) ? e : [])),
250
+ getStatuses: () => r("status", "all", "/rest/api/2/status").pipe(o.map((e) => Array.isArray(e) ? e : [])),
251
+ getFilters: () => t("GET", "/rest/api/2/filter").pipe(o.map((e) => Array.isArray(e) ? e : [])),
252
+ getAvatarUrl: (t) => o.gen(function* () {
253
+ let n = yield* l;
254
+ return `${(yield* e.credentialsResolver(n)).baseUrl.replace(/\/$/, "")}/secure/useravatar?ownerId=${k(t)}`;
255
+ }),
256
+ getBoard: (e) => t("GET", `/rest/agile/1.0/board/${k(String(e))}`),
257
+ getBoardQuickFilters: (e) => t("GET", `/rest/agile/1.0/board/${k(String(e))}/quickfilter`).pipe(o.map((e) => e.values ?? [])),
258
+ assignIssue: (e, n) => t("PUT", `/rest/api/2/issue/${k(e)}/assignee`, { name: n }).pipe(o.asVoid),
259
+ deleteIssue: (e, n) => t("DELETE", `/rest/api/2/issue/${k(e)}${n?.deleteSubtasks ? "?deleteSubtasks=true" : ""}`).pipe(o.asVoid),
260
+ getBaseUrl: () => o.gen(function* () {
261
+ let t = yield* l;
262
+ return (yield* e.credentialsResolver(t)).baseUrl.replace(/\/$/, "");
263
+ }),
264
+ getBoardFilterJql: (e) => o.gen(function* () {
265
+ let n = (yield* t("GET", `/rest/agile/1.0/board/${k(String(e))}/configuration`)).filter?.id;
266
+ if (n == null) return null;
267
+ let r = yield* t("GET", `/rest/api/2/filter/${k(String(n))}`);
268
+ return typeof r.jql == "string" && r.jql.length > 0 ? r.jql : null;
269
+ })
270
+ };
271
+ }, j = "https://auth.atlassian.com/authorize", M = "https://auth.atlassian.com/oauth/token", N = (e, t) => `${e.authorizeUrl ?? "https://auth.atlassian.com/authorize"}?${new URLSearchParams({
272
+ audience: t.audience ?? "api.atlassian.com",
273
+ client_id: e.clientId,
274
+ scope: t.scopes.join(" "),
275
+ redirect_uri: e.redirectUri,
276
+ state: t.state,
277
+ response_type: "code",
278
+ ...t.prompt ? { prompt: t.prompt } : {}
279
+ }).toString()}`, P = (e, t, n, r) => o.tryPromise({
280
+ try: async () => {
281
+ let i = await e(t, {
282
+ method: "POST",
283
+ headers: {
284
+ "content-type": "application/x-www-form-urlencoded",
285
+ accept: "application/json"
286
+ },
287
+ body: new URLSearchParams(n).toString()
288
+ }), o = new TextDecoder().decode(new Uint8Array(await i.arrayBuffer()));
289
+ if (i.status < 200 || i.status >= 300) throw new a({
290
+ message: `token endpoint ${i.status}: ${o.slice(0, 500)}`,
291
+ transient: i.status >= 500,
292
+ status: i.status
293
+ });
294
+ let s;
295
+ try {
296
+ s = JSON.parse(o);
297
+ } catch {
298
+ throw new a({
299
+ message: "invalid JSON in token response",
300
+ transient: !1,
301
+ status: i.status
302
+ });
303
+ }
304
+ if (typeof s.access_token != "string" || s.access_token.length === 0) throw new a({
305
+ message: "token response missing access_token",
306
+ transient: !1,
307
+ status: i.status
308
+ });
309
+ let c = (Number.isFinite(s.expires_in) ? s.expires_in : 3600) * 1e3;
310
+ return {
311
+ accessToken: s.access_token,
312
+ ...s.refresh_token ? { refreshToken: s.refresh_token } : {},
313
+ expiresAt: r() + c,
314
+ scopes: (s.scope ?? "").split(" ").filter(Boolean),
315
+ tokenType: s.token_type ?? "Bearer"
316
+ };
317
+ },
318
+ catch: (e) => e instanceof a ? e : new a({
319
+ message: `token endpoint network error: ${String(e)}`,
320
+ transient: !0
321
+ })
322
+ }), F = (e, t) => P(e.fetchImpl, e.tokenUrl ?? "https://auth.atlassian.com/oauth/token", {
323
+ grant_type: "authorization_code",
324
+ client_id: e.clientId,
325
+ client_secret: e.clientSecret,
326
+ code: t,
327
+ redirect_uri: e.redirectUri
328
+ }, e.now ?? Date.now), I = (e, t) => P(e.fetchImpl, e.tokenUrl ?? "https://auth.atlassian.com/oauth/token", {
329
+ grant_type: "refresh_token",
330
+ client_id: e.clientId,
331
+ client_secret: e.clientSecret,
332
+ refresh_token: t
333
+ }, e.now ?? Date.now), L = 6e4, R = (e, t, n) => o.gen(function* () {
334
+ let r = (e.now ?? Date.now)(), i = e.skewMs ?? 6e4;
335
+ if (t.expiresAt - r >= i) return t;
336
+ if (t.refreshToken === void 0) return yield* o.fail(new a({
337
+ message: "access token expired and no refresh token on file — re-consent required",
338
+ transient: !1
339
+ }));
340
+ let s = yield* I(e, t.refreshToken), c = n(s);
341
+ return c !== void 0 && (yield* c), s;
342
+ }), z = (e, t) => fetch(e, t), B = (e) => {
343
+ let n = e.fetchImpl ?? z, i = {
344
+ ...f,
345
+ ...e.policy
346
+ }, a = e.cache, c = A({
347
+ credentialsResolver: e.credentialsResolver,
348
+ fetchImpl: n,
349
+ policy: i,
350
+ ...a ? { cache: a } : {}
351
+ }), l = E({
352
+ credentialsResolver: e.credentialsResolver,
353
+ fetchImpl: n,
354
+ policy: i
355
+ }), d = s.mergeAll(s.succeed(r, c), s.succeed(t, l)), p = e.avatar ? [C({
356
+ resolveCredentials: e.avatar.resolveCredentials,
357
+ fetchImpl: e.avatar.fetchImpl ?? n,
358
+ ...e.avatar.pathPrefix ? { pathPrefix: e.avatar.pathPrefix } : {}
359
+ })] : [];
360
+ return u({
361
+ name: e.name ? `@voltro/plugin-atlassian#${e.name}` : "@voltro/plugin-atlassian",
362
+ description: "Jira + Confluence — JiraService/ConfluenceService over the Atlassian APIs with a pluggable per-subject PAT resolver, transient retry, timeouts, and a public avatar proxy.",
363
+ permissions: ["network:outbound:*"],
364
+ services: d,
365
+ httpRoutes: p,
366
+ onActivate: (e) => o.sync(() => {
367
+ e.logger.info("atlassian active", {
368
+ avatar: p.length > 0,
369
+ cache: a !== void 0,
370
+ maxAttempts: i.maxAttempts,
371
+ timeoutMs: i.timeoutMs
372
+ });
373
+ })
374
+ });
375
+ };
376
+ //#endregion
377
+ export { j as ATLASSIAN_AUTHORIZE_URL, M as ATLASSIAN_TOKEN_URL, a as AtlassianOAuthError, n as AtlassianWebhookError, i as ConfluenceError, t as ConfluenceService, v as DEFAULT_AVATAR_PREFIX, g as DEFAULT_PAT_APPLICATION, d as DEFAULT_POLICY, L as DEFAULT_REFRESH_SKEW_MS, e as JiraError, r as JiraService, B as atlassianPlugin, N as buildAuthorizeUrl, F as exchangeCode, I as refreshTokens, R as withFreshToken };
@@ -0,0 +1,22 @@
1
+ import { Context as e, Schema as t } from "effect";
2
+ //#region src/types.ts
3
+ var n = class extends t.TaggedError()("JiraError", {
4
+ message: t.String,
5
+ transient: t.Boolean,
6
+ status: t.optional(t.Number),
7
+ code: t.optional(t.Literal("session_expired"))
8
+ }) {}, r = class extends t.TaggedError()("ConfluenceError", {
9
+ message: t.String,
10
+ transient: t.Boolean,
11
+ status: t.optional(t.Number),
12
+ code: t.optional(t.Literal("session_expired"))
13
+ }) {}, i = class extends t.TaggedError()("AtlassianOAuthError", {
14
+ message: t.String,
15
+ transient: t.Boolean,
16
+ status: t.optional(t.Number)
17
+ }) {}, a = class extends t.TaggedError()("AtlassianWebhookError", {
18
+ message: t.String,
19
+ reason: t.Literal("signature", "payload")
20
+ }) {}, o = class extends e.Tag("@voltro/plugin-atlassian/JiraService")() {}, s = class extends e.Tag("@voltro/plugin-atlassian/ConfluenceService")() {};
21
+ //#endregion
22
+ export { n as a, s as i, a as n, o, r, i as t };
@@ -0,0 +1,86 @@
1
+ /** The header Atlassian writes the body HMAC to. */
2
+ export declare const ATLASSIAN_SIGNATURE_HEADER = "X-Hub-Signature";
3
+
4
+ /** HMAC-SHA-256 signature scheme over the raw body, `X-Hub-Signature: sha256=<hex>`.
5
+ * Structurally a `@voltro/plugin-webhooks` `HmacSignatureScheme`. */
6
+ export declare interface AtlassianSignatureScheme {
7
+ readonly _tag: 'hmac';
8
+ readonly algorithm: 'hmacSha256';
9
+ readonly header: string;
10
+ readonly includeTimestamp: false;
11
+ readonly encoding: 'hex';
12
+ readonly versionPrefix: 'sha256=';
13
+ }
14
+
15
+ export declare const atlassianSignatureScheme: (header?: string) => AtlassianSignatureScheme;
16
+
17
+ /** The minimal shape of an Atlassian webhook envelope the plugin surfaces.
18
+ * Everything beyond `webhookEvent` is kept loose — the app narrows per event
19
+ * (Atlassian's payloads are large and event-specific). */
20
+ export declare interface AtlassianWebhookEnvelope {
21
+ readonly webhookEvent: string;
22
+ readonly timestamp?: number;
23
+ readonly issue?: unknown;
24
+ readonly comment?: unknown;
25
+ readonly page?: unknown;
26
+ readonly user?: unknown;
27
+ /** The full decoded body, for fields not surfaced above. */
28
+ readonly raw: Record<string, unknown>;
29
+ }
30
+
31
+ /** A webhook-provider preset for inbound Jira/Confluence webhooks — a drop-in
32
+ * `@voltro/plugin-webhooks` `WebhookProviderDescriptor` (matched by shape).
33
+ *
34
+ * export default defineIncomingWebhook({
35
+ * id: 'jira-events',
36
+ * provider: atlassianWebhookProvider(),
37
+ * payload: Schema.Any,
38
+ * handler: async (ctx) => {
39
+ * // signature already verified by the framework; switch on ctx.body.webhookEvent
40
+ * },
41
+ * })
42
+ *
43
+ * The idempotency key defaults to the `X-Atlassian-Webhook-Identifier` header
44
+ * (present on Cloud) and falls back to a body-derived `<event>:<timestamp>`. */
45
+ export declare const atlassianWebhookProvider: (options?: {
46
+ readonly signatureHeader?: string;
47
+ }) => {
48
+ _tag: "webhookProvider";
49
+ id: string;
50
+ name: string;
51
+ signature: AtlassianSignatureScheme;
52
+ idempotency: {
53
+ from: (headers: Readonly<Record<string, string>>, rawBody: Uint8Array) => string | undefined;
54
+ ttl: "1d";
55
+ };
56
+ bodyType: "json";
57
+ eventTypeFrom: (body: unknown) => string | undefined;
58
+ };
59
+
60
+ /** Atlassian's event-envelope discriminator lives in the body's
61
+ * `webhookEvent` field (e.g. `jira:issue_updated`, `comment_created`,
62
+ * `page_created`). */
63
+ export declare const eventTypeFromBody: (body: unknown) => string | undefined;
64
+
65
+ /**
66
+ * Parse the raw JSON body into a typed envelope. Throws
67
+ * {@link AtlassianWebhookError} (`reason: 'payload'`) on invalid JSON or a
68
+ * missing `webhookEvent` discriminator — the caller maps that to a 400.
69
+ */
70
+ export declare const parseWebhookEnvelope: (rawBody: Uint8Array) => AtlassianWebhookEnvelope;
71
+
72
+ /**
73
+ * Verify an inbound Atlassian webhook's HMAC signature over the raw body.
74
+ * Succeeds (returns `void`) on a valid signature; throws
75
+ * {@link AtlassianWebhookError} (`reason: 'signature'`) on a missing/invalid
76
+ * one — the caller maps that to a 401. Use this only on the raw-`PluginHttpRoute`
77
+ * path; the `*.webhook.tsx` provider path verifies automatically.
78
+ */
79
+ export declare const verifyWebhookSignature: (args: {
80
+ readonly rawBody: Uint8Array;
81
+ readonly headers: Readonly<Record<string, string>>;
82
+ readonly secret: string;
83
+ readonly signatureHeader?: string;
84
+ }) => void;
85
+
86
+ export { }
@@ -0,0 +1,75 @@
1
+ import { n as e } from "./types-CVyqPJzY.js";
2
+ import { createHmac as t, timingSafeEqual as n } from "node:crypto";
3
+ //#region src/webhook.ts
4
+ var r = "X-Hub-Signature", i = (e = r) => ({
5
+ _tag: "hmac",
6
+ algorithm: "hmacSha256",
7
+ header: e,
8
+ includeTimestamp: !1,
9
+ encoding: "hex",
10
+ versionPrefix: "sha256="
11
+ }), a = (e) => {
12
+ if (typeof e != "object" || !e) return;
13
+ let t = e.webhookEvent;
14
+ return typeof t == "string" ? t : void 0;
15
+ }, o = (e, t) => {
16
+ let n = e["x-atlassian-webhook-identifier"];
17
+ if (n) return n;
18
+ try {
19
+ let e = JSON.parse(new TextDecoder().decode(t));
20
+ if (e.webhookEvent && e.timestamp !== void 0) return `${e.webhookEvent}:${e.timestamp}`;
21
+ } catch {
22
+ return;
23
+ }
24
+ }, s = (e) => ({
25
+ _tag: "webhookProvider",
26
+ id: "atlassian",
27
+ name: "Atlassian (Jira / Confluence)",
28
+ signature: i(e?.signatureHeader),
29
+ idempotency: {
30
+ from: o,
31
+ ttl: "1d"
32
+ },
33
+ bodyType: "json",
34
+ eventTypeFrom: a
35
+ }), c = (e, t) => {
36
+ let r = Buffer.from(e, "utf8"), i = Buffer.from(t, "utf8");
37
+ return r.length === i.length ? n(r, i) : (n(r, Buffer.alloc(r.length)), !1);
38
+ }, l = (n) => {
39
+ let r = (n.signatureHeader ?? "X-Hub-Signature").toLowerCase(), i = n.headers[r];
40
+ if (!i) throw new e({
41
+ message: "missing signature header",
42
+ reason: "signature"
43
+ });
44
+ let a = i.startsWith("sha256=") ? i.slice(7) : i;
45
+ if (!c(t("sha256", n.secret).update(Buffer.from(n.rawBody)).digest("hex"), a)) throw new e({
46
+ message: "signature mismatch",
47
+ reason: "signature"
48
+ });
49
+ }, u = (t) => {
50
+ let n;
51
+ try {
52
+ n = JSON.parse(new TextDecoder().decode(t));
53
+ } catch {
54
+ throw new e({
55
+ message: "invalid JSON in webhook body",
56
+ reason: "payload"
57
+ });
58
+ }
59
+ let r = n.webhookEvent;
60
+ if (typeof r != "string" || r.length === 0) throw new e({
61
+ message: "webhook body missing `webhookEvent`",
62
+ reason: "payload"
63
+ });
64
+ return {
65
+ webhookEvent: r,
66
+ ...typeof n.timestamp == "number" ? { timestamp: n.timestamp } : {},
67
+ ..."issue" in n ? { issue: n.issue } : {},
68
+ ..."comment" in n ? { comment: n.comment } : {},
69
+ ..."page" in n ? { page: n.page } : {},
70
+ ..."user" in n ? { user: n.user } : {},
71
+ raw: n
72
+ };
73
+ };
74
+ //#endregion
75
+ export { r as ATLASSIAN_SIGNATURE_HEADER, i as atlassianSignatureScheme, s as atlassianWebhookProvider, a as eventTypeFromBody, u as parseWebhookEnvelope, l as verifyWebhookSignature };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@voltro/plugin-atlassian",
3
+ "version": "0.1.0",
4
+ "description": "Jira + Confluence plugin — JiraService + ConfluenceService over the Atlassian REST/Greenhopper/Agile APIs, with a pluggable per-subject credentials resolver (PAT), transient retry + Retry-After, timeouts, an SSRF-guarded PAT-free avatar proxy, and optional response caching via @voltro/cache.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ },
26
+ "./webhook": {
27
+ "types": "./dist/webhook.d.ts",
28
+ "import": "./dist/webhook.js",
29
+ "default": "./dist/webhook.js"
30
+ }
31
+ },
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "sideEffects": false,
36
+ "engines": {
37
+ "node": ">=24.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@voltro/integration-http": "0.1.0",
41
+ "@voltro/protocol": "0.1.0"
42
+ },
43
+ "peerDependencies": {
44
+ "effect": "^3.21.4"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }