@autono/pinbox-core 0.18.0 → 0.19.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.
@@ -25,12 +25,38 @@ declare class GithubAppError extends Error {
25
25
  readonly status: number;
26
26
  constructor(message: string, status: number, hint?: string);
27
27
  }
28
+ /** The App's own credential: a short-lived RS256 JWT with `iss` = app id. */
29
+ declare function signAppJwt(appId: string, privateKeyPem: string, now?: () => number): Promise<string>;
30
+ /** GitHub's standard request headers for the REST API. */
31
+ declare function githubHeaders(bearer: string, json?: boolean): Record<string, string>;
32
+ type InstallationToken = {
33
+ token: string;
34
+ expiresAt: number;
35
+ };
36
+ /** Exchange an App JWT for a one-hour installation token. */
37
+ declare function mintInstallationToken(api: string, installationId: string, appJwt: string, fetchImpl: FetchLike): Promise<InstallationToken>;
28
38
  declare function createGithubAppTransport(opts: GithubAppTransportOptions): ConnectorTransport;
29
39
  /** PKCS#8 PEM in, PKCS#8 PEM out; PKCS#1 PEM (GitHub's download) is wrapped into PKCS#8. */
30
40
  declare function toPkcs8Pem(pem: string): string;
31
41
  /** PrivateKeyInfo ::= SEQUENCE { version 0, algorithm rsaEncryption, privateKey OCTET STRING }. */
32
42
  declare function pkcs1ToPkcs8(pkcs1: Uint8Array): Uint8Array;
33
43
  //#endregion
44
+ //#region src/connectors/github-webhook.d.ts
45
+ type GithubWebhookOptions = {
46
+ /** The webhook secret configured on the App — HMAC-SHA256 key for the signature. */
47
+ secret: string;
48
+ /** "owner/name": events for any other repository are acknowledged and ignored. */
49
+ repo: string;
50
+ };
51
+ /**
52
+ * Handle one webhook delivery. Returns the hub's machine envelope: 200 with
53
+ * `{ applied, ignored }`, 401 `E_AUTH` on a signature failure, 400 `E_INVALID_INPUT` on a
54
+ * body GitHub would never send.
55
+ */
56
+ declare function handleGithubWebhook(req: Request, store: PinStore, opts: GithubWebhookOptions): Promise<Response>;
57
+ /** `sha256=<hex hmac>` over the raw body, compared in constant time. */
58
+ declare function signatureValid(secret: string, rawBody: string, header: string | null): Promise<boolean>;
59
+ //#endregion
34
60
  //#region src/connectors/mirror.d.ts
35
61
  declare function createConnectorEvents(store: PinStore, pinId: string): ConnectorEvents;
36
62
  /**
@@ -64,4 +90,4 @@ type SlackConnectorOptions = {
64
90
  /** Links are `ref: "<channel>/<thread_ts>"`; every thread op derives channel + ts from the ref. */
65
91
  declare function createSlackConnector(transport: ConnectorTransport, opts: SlackConnectorOptions): Connector;
66
92
  //#endregion
67
- export { Connector, ConnectorEvents, ConnectorTransport, FetchLike, GithubAppError, GithubAppTransportOptions, POLL_OPEN_MS, POLL_RESOLVED_MS, RemoteComment, RemoteStatus, SlackConnectorOptions, SlackTransportOptions, createConnectorEvents, createGithubAppTransport, createSlackConnector, createSlackTransport, drainConnectorPolls, outboundCandidates, pkcs1ToPkcs8, toPkcs8Pem };
93
+ export { Connector, ConnectorEvents, ConnectorTransport, FetchLike, GithubAppError, GithubAppTransportOptions, GithubWebhookOptions, InstallationToken, POLL_OPEN_MS, POLL_RESOLVED_MS, RemoteComment, RemoteStatus, SlackConnectorOptions, SlackTransportOptions, createConnectorEvents, createGithubAppTransport, createSlackConnector, createSlackTransport, drainConnectorPolls, githubHeaders, handleGithubWebhook, mintInstallationToken, outboundCandidates, pkcs1ToPkcs8, signAppJwt, signatureValid, toPkcs8Pem };
@@ -1,3 +1,3 @@
1
- import { a as outboundCandidates, i as createConnectorEvents, n as POLL_RESOLVED_MS, r as drainConnectorPolls, t as POLL_OPEN_MS } from "../poll-BrcAuaAz.js";
2
- import { a as pkcs1ToPkcs8, i as createGithubAppTransport, n as createSlackTransport, o as toPkcs8Pem, r as GithubAppError, t as createSlackConnector } from "../slack-Dnq9bnXd.js";
3
- export { GithubAppError, POLL_OPEN_MS, POLL_RESOLVED_MS, createConnectorEvents, createGithubAppTransport, createSlackConnector, createSlackTransport, drainConnectorPolls, outboundCandidates, pkcs1ToPkcs8, toPkcs8Pem };
1
+ import { a as createConnectorEvents, n as POLL_RESOLVED_MS, o as outboundCandidates, r as drainConnectorPolls, t as POLL_OPEN_MS } from "../poll-ZSv3wN9C.js";
2
+ import { a as GithubAppError, c as mintInstallationToken, d as toPkcs8Pem, i as signatureValid, l as pkcs1ToPkcs8, n as createSlackTransport, o as createGithubAppTransport, r as handleGithubWebhook, s as githubHeaders, t as createSlackConnector, u as signAppJwt } from "../slack-C4P0AbuZ.js";
3
+ export { GithubAppError, POLL_OPEN_MS, POLL_RESOLVED_MS, createConnectorEvents, createGithubAppTransport, createSlackConnector, createSlackTransport, drainConnectorPolls, githubHeaders, handleGithubWebhook, mintInstallationToken, outboundCandidates, pkcs1ToPkcs8, signAppJwt, signatureValid, toPkcs8Pem };
package/dist/do.d.ts CHANGED
@@ -40,6 +40,7 @@ type PinboxDoEnv = {
40
40
  GITHUB_INSTALLATION_ID?: string;
41
41
  GITHUB_REPO?: string;
42
42
  GITHUB_API_BASE?: string;
43
+ GITHUB_WEBHOOK_SECRET?: string;
43
44
  SLACK_BOT_TOKEN?: string;
44
45
  SLACK_CHANNEL?: string;
45
46
  MEDIA?: R2Bucket;
@@ -75,6 +76,11 @@ declare class PinboxHubDO {
75
76
  constructor(ctx: DurableObjectState, env: PinboxDoEnv);
76
77
  fetch(req: Request): Promise<Response>;
77
78
  private intercept;
79
+ /**
80
+ * GitHub cannot present our credential: the HMAC signature is this route's whole auth
81
+ * (github-webhook.ts), so it sits outside the bearer/JWT gate. Unconfigured ⇒ falls to 404.
82
+ */
83
+ private githubWebhook;
78
84
  webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): void;
79
85
  webSocketClose(_ws: WebSocket, _code: number, _reason: string, _wasClean: boolean): void;
80
86
  /**
package/dist/do.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { AttachmentSchema, PinInputSchema, PinSchema, SessionRefSchema, ThreadMessageSchema } from "./schema.js";
2
2
  import { a as NotFoundError, i as ConflictError, o as newId, t as SessionSchema } from "./sessions-DrCVTMfI.js";
3
3
  import { t as MIGRATIONS } from "./store-IR3o5YwY.js";
4
- import { r as drainConnectorPolls } from "./poll-BrcAuaAz.js";
5
- import { n as createHubHandler, o as ok, r as err } from "./hub-I67BuX9S.js";
4
+ import { r as drainConnectorPolls } from "./poll-ZSv3wN9C.js";
5
+ import { n as createHubHandler, o as ok, r as err } from "./hub-D-cHpKmA.js";
6
6
  import { ClientHelloSchema, WS_CLOSE_PROTOCOL, WS_CLOSE_UNAUTHORIZED, WS_TOKEN_SUBPROTOCOL_PREFIX, encodeWsEvent } from "./ws-protocol.js";
7
7
  import { n as DeliveryRouter } from "./router-BH74psWn.js";
8
8
  import { createWebhookAdapter } from "./delivery/webhook.js";
9
- import { i as createGithubAppTransport, n as createSlackTransport, t as createSlackConnector } from "./slack-Dnq9bnXd.js";
9
+ import { n as createSlackTransport, o as createGithubAppTransport, r as handleGithubWebhook, t as createSlackConnector } from "./slack-C4P0AbuZ.js";
10
10
  import { createGithubConnector } from "./connectors/github.js";
11
11
  import { i as verifyJwt, n as verifyNone, r as verifyToken } from "./verify-BDt9d9Np.js";
12
12
  //#region src/do-store-registries.ts
@@ -564,6 +564,8 @@ var DoBroadcaster = class {
564
564
  return this.state.getWebSockets(topic).length;
565
565
  }
566
566
  };
567
+ /** Hub-root path of the GitHub webhook receiver (`/_pinbox/webhooks/github` on the Worker). */
568
+ const WEBHOOK_PATH = "/webhooks/github";
567
569
  /** Retry cadence for the delivery queue. Coarse: the receiver being down is not urgent. */
568
570
  const DRAIN_INTERVAL_MS = 3e4;
569
571
  /** Later than any due_at this hub can write, so `due()` answers "anything pending?". */
@@ -653,11 +655,25 @@ var PinboxHubDO = class {
653
655
  return await this.intercept(req, url, verify) ?? this.handler(req);
654
656
  }
655
657
  async intercept(req, url, verify) {
658
+ if (req.method === "POST" && url.pathname === WEBHOOK_PATH) return this.githubWebhook(req);
656
659
  if (req.method === "POST" && url.pathname === "/attachments") return await this.unauthorized(verify, req) ?? this.createAttachment(req, url);
657
660
  const mediaKey = /^\/media\/([^/]+)$/.exec(url.pathname)?.[1];
658
661
  if (req.method === "GET" && mediaKey !== void 0) return await this.unauthorized(verify, req) ?? this.serveMedia(mediaKey);
659
662
  return null;
660
663
  }
664
+ /**
665
+ * GitHub cannot present our credential: the HMAC signature is this route's whole auth
666
+ * (github-webhook.ts), so it sits outside the bearer/JWT gate. Unconfigured ⇒ falls to 404.
667
+ */
668
+ githubWebhook(req) {
669
+ const secret = this.env.GITHUB_WEBHOOK_SECRET;
670
+ const repo = this.env.GITHUB_REPO;
671
+ if (secret === void 0 || secret === "" || repo === void 0 || repo === "") return null;
672
+ return handleGithubWebhook(req, this.store, {
673
+ secret,
674
+ repo
675
+ });
676
+ }
661
677
  webSocketMessage(ws, message) {
662
678
  if (typeof message !== "string") {
663
679
  this.protocolError(ws, "binary frames are not part of protocol 1");
@@ -1,6 +1,6 @@
1
1
  import { AppliedEditSchema, AttachmentSchema, PinInputSchema, SessionRefSchema } from "./schema.js";
2
2
  import { a as NotFoundError, i as ConflictError, o as newId } from "./sessions-DrCVTMfI.js";
3
- import { t as POLL_OPEN_MS } from "./poll-BrcAuaAz.js";
3
+ import { t as POLL_OPEN_MS } from "./poll-ZSv3wN9C.js";
4
4
  import { t as buildInjectionContext } from "./context-BHcEpzVb.js";
5
5
  import { z } from "zod";
6
6
  //#region src/routes-links.ts
@@ -1,4 +1,4 @@
1
- import { c as registerAttachmentSink, n as createHubHandler } from "./hub-I67BuX9S.js";
1
+ import { c as registerAttachmentSink, n as createHubHandler } from "./hub-D-cHpKmA.js";
2
2
  import { ClientHelloSchema, WS_CLOSE_PROTOCOL, WS_CLOSE_UNAUTHORIZED, WS_TOKEN_SUBPROTOCOL_PREFIX, encodeWsEvent } from "./ws-protocol.js";
3
3
  //#region src/attachments-local.ts
4
4
  const EXTENSIONS = {
package/dist/hub.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as mustGetPin, i as match, n as createHubHandler, o as ok, r as err, s as readJson, t as BodyNotJsonError } from "./hub-I67BuX9S.js";
1
+ import { a as mustGetPin, i as match, n as createHubHandler, o as ok, r as err, s as readJson, t as BodyNotJsonError } from "./hub-D-cHpKmA.js";
2
2
  export { BodyNotJsonError, createHubHandler, err, match, mustGetPin, ok, readJson };
@@ -249,4 +249,4 @@ function advances(next, current) {
249
249
  return current === null || next > current;
250
250
  }
251
251
  //#endregion
252
- export { outboundCandidates as a, createConnectorEvents as i, POLL_RESOLVED_MS as n, drainConnectorPolls as r, POLL_OPEN_MS as t };
252
+ export { createConnectorEvents as a, inboundEvents as i, POLL_RESOLVED_MS as n, outboundCandidates as o, drainConnectorPolls as r, POLL_OPEN_MS as t };
@@ -1,3 +1,4 @@
1
+ import { i as inboundEvents } from "./poll-ZSv3wN9C.js";
1
2
  import { pinsToMarkdown } from "./markdown.js";
2
3
  import { z } from "zod";
3
4
  import { SignJWT, importPKCS8 } from "jose";
@@ -28,38 +29,44 @@ var GithubAppError = class extends Error {
28
29
  this.hint = hint;
29
30
  }
30
31
  };
32
+ /** The App's own credential: a short-lived RS256 JWT with `iss` = app id. */
33
+ async function signAppJwt(appId, privateKeyPem, now = () => Date.now()) {
34
+ const key = await importPKCS8(toPkcs8Pem(privateKeyPem), "RS256");
35
+ const iat = Math.floor(now() / 1e3) - 60;
36
+ return new SignJWT({}).setProtectedHeader({
37
+ alg: "RS256",
38
+ typ: "JWT"
39
+ }).setIssuer(appId).setIssuedAt(iat).setExpirationTime(iat + 60 + APP_JWT_TTL_S).sign(key);
40
+ }
41
+ /** GitHub's standard request headers for the REST API. */
42
+ function githubHeaders(bearer, json = false) {
43
+ return headers(bearer, json);
44
+ }
45
+ /** Exchange an App JWT for a one-hour installation token. */
46
+ async function mintInstallationToken(api, installationId, appJwt, fetchImpl) {
47
+ const res = await fetchImpl(`${api}/app/installations/${installationId}/access_tokens`, {
48
+ method: "POST",
49
+ headers: headers(appJwt)
50
+ });
51
+ if (!res.ok) throw new GithubAppError(`GitHub App token request failed: HTTP ${res.status}`, res.status, res.status === 401 ? "check GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY belong to the same App" : res.status === 404 ? "check GITHUB_INSTALLATION_ID — the App may not be installed on this repo's owner" : void 0);
52
+ const body = await res.json();
53
+ if (typeof body.token !== "string" || typeof body.expires_at !== "string") throw new GithubAppError("GitHub App token response was not { token, expires_at }", 502);
54
+ return {
55
+ token: body.token,
56
+ expiresAt: Date.parse(body.expires_at)
57
+ };
58
+ }
31
59
  function createGithubAppTransport(opts) {
32
60
  const fetchImpl = opts.fetchImpl ?? ((input, init) => fetch(input, init));
33
61
  const now = opts.now ?? (() => Date.now());
34
62
  const api = (opts.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
35
63
  const repo = opts.repo.replace(/^\/+|\/+$/g, "");
36
64
  if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) throw new GithubAppError(`GITHUB_REPO must be "owner/name", got "${opts.repo}"`, 0);
37
- let keyPromise = null;
38
65
  let cached = null;
39
- function key() {
40
- keyPromise ??= importPKCS8(toPkcs8Pem(opts.privateKeyPem), "RS256");
41
- return keyPromise;
42
- }
43
- async function appJwt() {
44
- const iat = Math.floor(now() / 1e3) - 60;
45
- return new SignJWT({}).setProtectedHeader({
46
- alg: "RS256",
47
- typ: "JWT"
48
- }).setIssuer(opts.appId).setIssuedAt(iat).setExpirationTime(iat + 60 + APP_JWT_TTL_S).sign(await key());
49
- }
50
66
  async function installationToken() {
51
67
  if (cached !== null && cached.expiresAt - now() > REFRESH_MARGIN_MS) return cached.token;
52
- const res = await fetchImpl(`${api}/app/installations/${opts.installationId}/access_tokens`, {
53
- method: "POST",
54
- headers: headers(await appJwt())
55
- });
56
- if (!res.ok) throw new GithubAppError(`GitHub App token request failed: HTTP ${res.status}`, res.status, res.status === 401 ? "check GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY belong to the same App" : res.status === 404 ? "check GITHUB_INSTALLATION_ID — the App may not be installed on this repo's owner" : void 0);
57
- const body = await res.json();
58
- if (typeof body.token !== "string" || typeof body.expires_at !== "string") throw new GithubAppError("GitHub App token response was not { token, expires_at }", 502);
59
- cached = {
60
- token: body.token,
61
- expiresAt: Date.parse(body.expires_at)
62
- };
68
+ const jwt = await signAppJwt(opts.appId, opts.privateKeyPem, now);
69
+ cached = await mintInstallationToken(api, opts.installationId, jwt, fetchImpl);
63
70
  return cached.token;
64
71
  }
65
72
  async function call(method, path, body) {
@@ -84,44 +91,51 @@ function createGithubAppTransport(opts) {
84
91
  }
85
92
  return out;
86
93
  }
87
- return { async request(op, params) {
88
- const number = Number(params["number"]);
89
- if (NUMBERED_OPS.has(op) && (!Number.isInteger(number) || number <= 0)) throw new GithubAppError(`github ${op} needs a positive issue number`, 0);
90
- switch (op) {
91
- case "issue.create": {
92
- const issue = await call("POST", `/repos/${repo}/issues`, {
93
- title: params["title"],
94
- body: params["body"]
95
- });
96
- return {
97
- number: issue.number,
98
- url: issue.html_url
99
- };
100
- }
101
- case "issue.comment":
102
- await call("POST", `/repos/${repo}/issues/${number}/comments`, { body: params["body"] });
103
- return;
104
- case "issue.view": {
105
- const issue = await call("GET", `/repos/${repo}/issues/${number}`);
106
- const comments = await allComments(number);
107
- return {
108
- state: issue.state === "closed" ? "closed" : "open",
109
- comments: comments.map((c) => ({
110
- author: c.user?.login ?? "ghost",
111
- body: c.body ?? "",
112
- createdAt: c.created_at
113
- }))
114
- };
115
- }
116
- case "issue.close":
117
- await call("PATCH", `/repos/${repo}/issues/${number}`, { state: "closed" });
118
- return;
119
- case "issue.reopen":
120
- await call("PATCH", `/repos/${repo}/issues/${number}`, { state: "open" });
121
- return;
122
- default: throw new GithubAppError(`unknown github op: ${op}`, 0);
94
+ return { request: (op, params) => dispatch(op, params, {
95
+ repo,
96
+ call,
97
+ allComments
98
+ }) };
99
+ }
100
+ /** The pinned op vocabulary (github.ts) over GitHub's REST paths. */
101
+ async function dispatch(op, params, api) {
102
+ const { repo, call } = api;
103
+ const number = Number(params["number"]);
104
+ if (NUMBERED_OPS.has(op) && (!Number.isInteger(number) || number <= 0)) throw new GithubAppError(`github ${op} needs a positive issue number`, 0);
105
+ switch (op) {
106
+ case "issue.create": {
107
+ const issue = await call("POST", `/repos/${repo}/issues`, {
108
+ title: params["title"],
109
+ body: params["body"]
110
+ });
111
+ return {
112
+ number: issue.number,
113
+ url: issue.html_url
114
+ };
123
115
  }
124
- } };
116
+ case "issue.comment":
117
+ await call("POST", `/repos/${repo}/issues/${number}/comments`, { body: params["body"] });
118
+ return;
119
+ case "issue.view": {
120
+ const issue = await call("GET", `/repos/${repo}/issues/${number}`);
121
+ const comments = await api.allComments(number);
122
+ return {
123
+ state: issue.state === "closed" ? "closed" : "open",
124
+ comments: comments.map((c) => ({
125
+ author: c.user?.login ?? "ghost",
126
+ body: c.body ?? "",
127
+ createdAt: c.created_at
128
+ }))
129
+ };
130
+ }
131
+ case "issue.close":
132
+ await call("PATCH", `/repos/${repo}/issues/${number}`, { state: "closed" });
133
+ return;
134
+ case "issue.reopen":
135
+ await call("PATCH", `/repos/${repo}/issues/${number}`, { state: "open" });
136
+ return;
137
+ default: throw new GithubAppError(`unknown github op: ${op}`, 0);
138
+ }
125
139
  }
126
140
  function headers(bearer, json = false) {
127
141
  return {
@@ -203,6 +217,142 @@ function concat(...parts) {
203
217
  return out;
204
218
  }
205
219
  //#endregion
220
+ //#region src/connectors/github-webhook.ts
221
+ /** Trailer marking bodies pinbox itself wrote (github.ts); never mirror those back in. */
222
+ const PINBOX_TRAILER = "— pinbox";
223
+ /**
224
+ * Handle one webhook delivery. Returns the hub's machine envelope: 200 with
225
+ * `{ applied, ignored }`, 401 `E_AUTH` on a signature failure, 400 `E_INVALID_INPUT` on a
226
+ * body GitHub would never send.
227
+ */
228
+ async function handleGithubWebhook(req, store, opts) {
229
+ const raw = await req.text();
230
+ const signature = req.headers.get("x-hub-signature-256");
231
+ if (!await signatureValid(opts.secret, raw, signature)) return envelope(401, {
232
+ code: "E_AUTH",
233
+ message: "webhook signature missing or invalid",
234
+ hint: "the App's webhook secret must equal GITHUB_WEBHOOK_SECRET on the hub"
235
+ });
236
+ let payload;
237
+ try {
238
+ payload = JSON.parse(raw);
239
+ } catch {
240
+ return envelope(400, {
241
+ code: "E_INVALID_INPUT",
242
+ message: "webhook body is not JSON"
243
+ });
244
+ }
245
+ const event = req.headers.get("x-github-event") ?? "";
246
+ return envelope(200, void 0, await apply(store, opts.repo, event, payload));
247
+ }
248
+ async function apply(store, repo, event, payload) {
249
+ if (event === "ping") return {
250
+ applied: 0,
251
+ ignored: "ping"
252
+ };
253
+ if (event !== "issues" && event !== "issue_comment") return {
254
+ applied: 0,
255
+ ignored: `event ${event}`
256
+ };
257
+ const body = payload;
258
+ const fullName = body.repository?.full_name;
259
+ if (typeof fullName !== "string" || fullName.toLowerCase() !== repo.toLowerCase()) return {
260
+ applied: 0,
261
+ ignored: `repository ${fullName ?? "?"}`
262
+ };
263
+ const number = body.issue?.number;
264
+ if (typeof number !== "number") return {
265
+ applied: 0,
266
+ ignored: "no issue"
267
+ };
268
+ const row = store.links.all().find((r) => r.link.connector === "github" && r.link.ref === String(number));
269
+ if (row === void 0) return {
270
+ applied: 0,
271
+ ignored: `issue #${number} is not linked`
272
+ };
273
+ const { events } = inboundEvents(store, row.pinId, "github", false);
274
+ return event === "issue_comment" ? applyComment(body, row.link, events) : applyStatus(body, row.link, events);
275
+ }
276
+ async function applyComment(body, link, events) {
277
+ if (body.action !== "created") return {
278
+ applied: 0,
279
+ ignored: `issue_comment ${body.action}`
280
+ };
281
+ const comment = body.comment;
282
+ const text = comment?.body ?? "";
283
+ if (isOwnMirror(text) || comment?.user?.type === "Bot") return {
284
+ applied: 0,
285
+ ignored: "own mirror"
286
+ };
287
+ await events.onRemoteComment(link, {
288
+ origin: `github:${comment?.user?.login ?? "ghost"}`,
289
+ text,
290
+ at: comment?.created_at ?? (/* @__PURE__ */ new Date()).toISOString()
291
+ });
292
+ return {
293
+ applied: 1,
294
+ ignored: null
295
+ };
296
+ }
297
+ async function applyStatus(body, link, events) {
298
+ if (body.action === "closed") {
299
+ await events.onRemoteStatus(link, "closed");
300
+ return {
301
+ applied: 1,
302
+ ignored: null
303
+ };
304
+ }
305
+ if (body.action === "reopened") {
306
+ await events.onRemoteStatus(link, "open");
307
+ return {
308
+ applied: 1,
309
+ ignored: null
310
+ };
311
+ }
312
+ return {
313
+ applied: 0,
314
+ ignored: `issues ${body.action}`
315
+ };
316
+ }
317
+ function isOwnMirror(text) {
318
+ return (text.trimEnd().split("\n").at(-1) ?? "").startsWith(PINBOX_TRAILER);
319
+ }
320
+ /** `sha256=<hex hmac>` over the raw body, compared in constant time. */
321
+ async function signatureValid(secret, rawBody, header) {
322
+ if (header === null || !header.startsWith("sha256=")) return false;
323
+ const expected = await hmacHex(secret, rawBody);
324
+ return timingSafeEqual(header.slice(7), expected);
325
+ }
326
+ async function hmacHex(secret, body) {
327
+ const enc = new TextEncoder();
328
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), {
329
+ name: "HMAC",
330
+ hash: "SHA-256"
331
+ }, false, ["sign"]);
332
+ const sig = new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(body)));
333
+ let hex = "";
334
+ for (const b of sig) hex += b.toString(16).padStart(2, "0");
335
+ return hex;
336
+ }
337
+ function timingSafeEqual(a, b) {
338
+ if (a.length !== b.length) return false;
339
+ let diff = 0;
340
+ for (let i = 0; i < a.length; i += 1) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
341
+ return diff === 0;
342
+ }
343
+ function envelope(status, error, data) {
344
+ return new Response(JSON.stringify(error === void 0 ? {
345
+ ok: true,
346
+ data
347
+ } : {
348
+ ok: false,
349
+ error
350
+ }), {
351
+ status,
352
+ headers: { "content-type": "application/json; charset=utf-8" }
353
+ });
354
+ }
355
+ //#endregion
206
356
  //#region src/connectors/slack.ts
207
357
  /**
208
358
  * request(op, params) → POST https://slack.com/api/<op> (JSON, bearer botToken).
@@ -302,4 +452,4 @@ function slackTsToMs(ts) {
302
452
  return Number(ts) * 1e3;
303
453
  }
304
454
  //#endregion
305
- export { pkcs1ToPkcs8 as a, createGithubAppTransport as i, createSlackTransport as n, toPkcs8Pem as o, GithubAppError as r, createSlackConnector as t };
455
+ export { GithubAppError as a, mintInstallationToken as c, toPkcs8Pem as d, signatureValid as i, pkcs1ToPkcs8 as l, createSlackTransport as n, createGithubAppTransport as o, handleGithubWebhook as r, githubHeaders as s, createSlackConnector as t, signAppJwt as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autono/pinbox-core",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {