@autono/pinbox-core 0.16.0 → 0.18.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.
@@ -1,6 +1,36 @@
1
1
  import { g as ThreadMessage } from "../schema-BlM4lTLW.js";
2
2
  import { a as RemoteStatus, i as RemoteComment, n as ConnectorEvents, r as ConnectorTransport, t as Connector } from "../types-BmfT_m1p.js";
3
3
  import { c as PinStore } from "../store-DAB5CEA1.js";
4
+ //#region src/connectors/github-app.d.ts
5
+ /** The slice of fetch this transport uses — injectable without dragging in Bun's extras. */
6
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
7
+ type GithubAppTransportOptions = {
8
+ /** The App's numeric id (the `iss` claim). */
9
+ appId: string;
10
+ /** PEM, PKCS#1 or PKCS#8 — GitHub downloads PKCS#1. */
11
+ privateKeyPem: string;
12
+ /** The installation on the target repo's owner. */
13
+ installationId: string;
14
+ /** "owner/name". */
15
+ repo: string;
16
+ /** Default https://api.github.com; GHES uses https://<host>/api/v3. */
17
+ apiBase?: string;
18
+ fetchImpl?: FetchLike;
19
+ /** Test seam: epoch ms. */
20
+ now?: () => number;
21
+ };
22
+ /** A transport failure carrying the hint the link route surfaces as E_CONNECTOR. */
23
+ declare class GithubAppError extends Error {
24
+ readonly hint: string | undefined;
25
+ readonly status: number;
26
+ constructor(message: string, status: number, hint?: string);
27
+ }
28
+ declare function createGithubAppTransport(opts: GithubAppTransportOptions): ConnectorTransport;
29
+ /** PKCS#8 PEM in, PKCS#8 PEM out; PKCS#1 PEM (GitHub's download) is wrapped into PKCS#8. */
30
+ declare function toPkcs8Pem(pem: string): string;
31
+ /** PrivateKeyInfo ::= SEQUENCE { version 0, algorithm rsaEncryption, privateKey OCTET STRING }. */
32
+ declare function pkcs1ToPkcs8(pkcs1: Uint8Array): Uint8Array;
33
+ //#endregion
4
34
  //#region src/connectors/mirror.d.ts
5
35
  declare function createConnectorEvents(store: PinStore, pinId: string): ConnectorEvents;
6
36
  /**
@@ -34,4 +64,4 @@ type SlackConnectorOptions = {
34
64
  /** Links are `ref: "<channel>/<thread_ts>"`; every thread op derives channel + ts from the ref. */
35
65
  declare function createSlackConnector(transport: ConnectorTransport, opts: SlackConnectorOptions): Connector;
36
66
  //#endregion
37
- export { Connector, ConnectorEvents, ConnectorTransport, POLL_OPEN_MS, POLL_RESOLVED_MS, RemoteComment, RemoteStatus, SlackConnectorOptions, SlackTransportOptions, createConnectorEvents, createSlackConnector, createSlackTransport, drainConnectorPolls, outboundCandidates };
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 };
@@ -1,103 +1,3 @@
1
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 { pinsToMarkdown } from "../markdown.js";
3
- import { z } from "zod";
4
- //#region src/connectors/slack.ts
5
- /**
6
- * request(op, params) → POST https://slack.com/api/<op> (JSON, bearer botToken).
7
- * Slack's `{ok:false, error}` becomes a rejection carrying the Slack error string —
8
- * the route layer surfaces it as 502 E_CONNECTOR.
9
- */
10
- function createSlackTransport(opts) {
11
- const fetchImpl = opts.fetchImpl ?? fetch;
12
- return { async request(op, params) {
13
- const res = await fetchImpl(`https://slack.com/api/${op}`, {
14
- method: "POST",
15
- headers: {
16
- authorization: `Bearer ${opts.botToken}`,
17
- "content-type": "application/json; charset=utf-8"
18
- },
19
- body: JSON.stringify(params)
20
- });
21
- if (!res.ok) throw new Error(`slack ${op} failed: HTTP ${res.status}`);
22
- const data = await res.json();
23
- const envelope = SlackEnvelopeSchema.parse(data);
24
- if (!envelope.ok) throw new Error(`slack ${op} failed: ${envelope.error ?? "unknown_error"}`);
25
- return data;
26
- } };
27
- }
28
- /** Links are `ref: "<channel>/<thread_ts>"`; every thread op derives channel + ts from the ref. */
29
- function createSlackConnector(transport, opts) {
30
- return {
31
- name: "slack",
32
- async createItem(pin, thread) {
33
- const text = [pinsToMarkdown([pin], "standard"), ...thread.map((m) => `${m.role}: ${m.text}`)].join("\n").trimEnd();
34
- const posted = PostMessageSchema.parse(await transport.request("chat.postMessage", {
35
- channel: opts.channel,
36
- text
37
- }));
38
- const channel = posted.channel ?? opts.channel;
39
- const permalink = PermalinkSchema.parse(await transport.request("chat.getPermalink", {
40
- channel,
41
- message_ts: posted.ts
42
- }));
43
- return {
44
- connector: "slack",
45
- ref: `${channel}/${posted.ts}`,
46
- url: permalink.permalink
47
- };
48
- },
49
- async postComment(link, message) {
50
- const { channel, ts } = parseRef(link.ref);
51
- await transport.request("chat.postMessage", {
52
- channel,
53
- thread_ts: ts,
54
- text: message.text
55
- });
56
- },
57
- async sync(link, events) {
58
- const { channel, ts } = parseRef(link.ref);
59
- const replies = RepliesSchema.parse(await transport.request("conversations.replies", {
60
- channel,
61
- ts
62
- }));
63
- for (const reply of replies.messages) {
64
- if (reply.ts === ts) continue;
65
- const atMs = slackTsToMs(reply.ts);
66
- await events.onRemoteComment(link, {
67
- origin: `slack:${reply.user}`,
68
- text: reply.text ?? "",
69
- at: new Date(atMs).toISOString()
70
- });
71
- }
72
- },
73
- async setRemoteStatus() {}
74
- };
75
- }
76
- const SlackEnvelopeSchema = z.looseObject({
77
- ok: z.boolean(),
78
- error: z.string().optional()
79
- });
80
- const PostMessageSchema = z.looseObject({
81
- channel: z.string().optional(),
82
- ts: z.string()
83
- });
84
- const PermalinkSchema = z.looseObject({ permalink: z.string() });
85
- const RepliesSchema = z.looseObject({ messages: z.array(z.looseObject({
86
- ts: z.string(),
87
- user: z.string(),
88
- text: z.string().optional()
89
- })) });
90
- function parseRef(ref) {
91
- const slash = ref.indexOf("/");
92
- if (slash <= 0 || slash === ref.length - 1) throw new Error(`slack link ref must be "<channel>/<thread_ts>", got "${ref}"`);
93
- return {
94
- channel: ref.slice(0, slash),
95
- ts: ref.slice(slash + 1)
96
- };
97
- }
98
- /** Slack ts is "<epoch-seconds>.<suffix>" — epoch milliseconds for the reported `at`. */
99
- function slackTsToMs(ts) {
100
- return Number(ts) * 1e3;
101
- }
102
- //#endregion
103
- export { POLL_OPEN_MS, POLL_RESOLVED_MS, createConnectorEvents, createSlackConnector, createSlackTransport, drainConnectorPolls, outboundCandidates };
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 };
package/dist/do.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { a as Link, g as ThreadMessage, m as SessionRef, r as Attachment, s as Pin, t as AppliedEdit } from "./schema-BlM4lTLW.js";
2
+ import { t as Connector } from "./types-BmfT_m1p.js";
2
3
  import { r as SessionStore } from "./sessions-BOrd8Yvv.js";
3
4
  import { a as LinkStore, c as PinStore, i as DeliveryStore, l as StoredEvent, t as CursorStore } from "./store-DAB5CEA1.js";
4
5
  import { Broadcaster } from "./ws.js";
@@ -34,6 +35,13 @@ type PinboxDoEnv = {
34
35
  JWT_ISSUER?: string;
35
36
  JWT_JWKS_URL?: string;
36
37
  JWT_AUDIENCE?: string;
38
+ GITHUB_APP_ID?: string;
39
+ GITHUB_APP_PRIVATE_KEY?: string;
40
+ GITHUB_INSTALLATION_ID?: string;
41
+ GITHUB_REPO?: string;
42
+ GITHUB_API_BASE?: string;
43
+ SLACK_BOT_TOKEN?: string;
44
+ SLACK_CHANNEL?: string;
37
45
  MEDIA?: R2Bucket;
38
46
  R2_ACCOUNT_ID?: string;
39
47
  R2_BUCKET?: string;
@@ -46,6 +54,12 @@ declare class DoBroadcaster implements Broadcaster {
46
54
  publish(topic: string, data: string): void;
47
55
  subscriberCount(topic: string): number;
48
56
  }
57
+ /**
58
+ * The cloud connector set, from the environment: github when the App quartet is present,
59
+ * slack when its pair is. A partial quartet is ignored rather than half-configured — the
60
+ * link route then answers 502 E_CONNECTOR with the `pinbox doctor` hint, as with no config.
61
+ */
62
+ declare function buildConnectors(env: PinboxDoEnv): Connector[];
49
63
  declare class PinboxHubDO {
50
64
  readonly store: DoPinStore;
51
65
  readonly broadcaster: DoBroadcaster;
@@ -56,6 +70,8 @@ declare class PinboxHubDO {
56
70
  private readonly handler;
57
71
  /** Undefined when no adapter is configured — the hub then stores pins and delivers nothing. */
58
72
  private readonly router;
73
+ /** Host-injected tracker connectors (github via App token, slack); empty ⇒ link routes 502. */
74
+ private readonly connectors;
59
75
  constructor(ctx: DurableObjectState, env: PinboxDoEnv);
60
76
  fetch(req: Request): Promise<Response>;
61
77
  private intercept;
@@ -69,6 +85,8 @@ declare class PinboxHubDO {
69
85
  alarm(): Promise<void>;
70
86
  /** Re-arm while work remains. Setting an alarm that already exists is a no-op. */
71
87
  private scheduleDrain;
88
+ /** Any delivery row pending, or any linked pin with a poll deadline set. */
89
+ private hasPendingWork;
72
90
  private upgrade;
73
91
  private protocolError;
74
92
  private unauthorized;
@@ -76,4 +94,4 @@ declare class PinboxHubDO {
76
94
  private serveMedia;
77
95
  }
78
96
  //#endregion
79
- export { DoBroadcaster, PinboxDoEnv, PinboxHubDO };
97
+ export { DoBroadcaster, PinboxDoEnv, PinboxHubDO, buildConnectors };
package/dist/do.js CHANGED
@@ -1,10 +1,13 @@
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";
4
5
  import { n as createHubHandler, o as ok, r as err } from "./hub-I67BuX9S.js";
5
6
  import { ClientHelloSchema, WS_CLOSE_PROTOCOL, WS_CLOSE_UNAUTHORIZED, WS_TOKEN_SUBPROTOCOL_PREFIX, encodeWsEvent } from "./ws-protocol.js";
6
7
  import { n as DeliveryRouter } from "./router-BH74psWn.js";
7
8
  import { createWebhookAdapter } from "./delivery/webhook.js";
9
+ import { i as createGithubAppTransport, n as createSlackTransport, t as createSlackConnector } from "./slack-Dnq9bnXd.js";
10
+ import { createGithubConnector } from "./connectors/github.js";
8
11
  import { i as verifyJwt, n as verifyNone, r as verifyToken } from "./verify-BDt9d9Np.js";
9
12
  //#region src/do-store-registries.ts
10
13
  function parseSession(json) {
@@ -583,6 +586,24 @@ function buildRouter(store, env) {
583
586
  })]
584
587
  });
585
588
  }
589
+ /**
590
+ * The cloud connector set, from the environment: github when the App quartet is present,
591
+ * slack when its pair is. A partial quartet is ignored rather than half-configured — the
592
+ * link route then answers 502 E_CONNECTOR with the `pinbox doctor` hint, as with no config.
593
+ */
594
+ function buildConnectors(env) {
595
+ const out = [];
596
+ const present = (v) => v !== void 0 && v !== "";
597
+ if (present(env.GITHUB_APP_ID) && present(env.GITHUB_APP_PRIVATE_KEY) && present(env.GITHUB_INSTALLATION_ID) && present(env.GITHUB_REPO)) out.push(createGithubConnector(createGithubAppTransport({
598
+ appId: env.GITHUB_APP_ID,
599
+ privateKeyPem: env.GITHUB_APP_PRIVATE_KEY,
600
+ installationId: env.GITHUB_INSTALLATION_ID,
601
+ repo: env.GITHUB_REPO,
602
+ ...present(env.GITHUB_API_BASE) ? { apiBase: env.GITHUB_API_BASE } : {}
603
+ })));
604
+ if (present(env.SLACK_BOT_TOKEN) && present(env.SLACK_CHANNEL)) out.push(createSlackConnector(createSlackTransport({ botToken: env.SLACK_BOT_TOKEN }), { channel: env.SLACK_CHANNEL }));
605
+ return out;
606
+ }
586
607
  var PinboxHubDO = class {
587
608
  store;
588
609
  broadcaster;
@@ -593,6 +614,8 @@ var PinboxHubDO = class {
593
614
  handler;
594
615
  /** Undefined when no adapter is configured — the hub then stores pins and delivers nothing. */
595
616
  router;
617
+ /** Host-injected tracker connectors (github via App token, slack); empty ⇒ link routes 502. */
618
+ connectors;
596
619
  constructor(ctx, env) {
597
620
  this.ctx = ctx;
598
621
  this.env = env;
@@ -600,10 +623,12 @@ var PinboxHubDO = class {
600
623
  this.topic = `project:${ctx.id.name ?? ctx.id.toString()}`;
601
624
  this.broadcaster = new DoBroadcaster(ctx);
602
625
  this.strategy = buildStrategy(env);
626
+ this.connectors = buildConnectors(env);
603
627
  this.handler = createHubHandler({
604
628
  store: this.store,
605
629
  token: env.PINBOX_TOKEN ?? "",
606
- ..."verify" in this.strategy ? { verify: this.strategy.verify } : {}
630
+ ..."verify" in this.strategy ? { verify: this.strategy.verify } : {},
631
+ ...this.connectors.length > 0 ? { connectors: this.connectors } : {}
607
632
  });
608
633
  this.store.subscribe((event) => this.broadcaster.publish(this.topic, encodeWsEvent(event)));
609
634
  this.router = buildRouter(this.store, env);
@@ -614,6 +639,10 @@ var PinboxHubDO = class {
614
639
  });
615
640
  this.scheduleDrain();
616
641
  }
642
+ if (this.connectors.length > 0) {
643
+ this.store.subscribe(() => void this.scheduleDrain());
644
+ this.scheduleDrain();
645
+ }
617
646
  ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong"));
618
647
  }
619
648
  async fetch(req) {
@@ -677,17 +706,21 @@ var PinboxHubDO = class {
677
706
  * DO has been unloaded and woken again.
678
707
  */
679
708
  async alarm() {
680
- if (this.router === void 0) return;
681
- await this.router.drainDue();
709
+ if (this.router !== void 0) await this.router.drainDue();
710
+ if (this.connectors.length > 0) await drainConnectorPolls(this.store, this.connectors);
682
711
  await this.scheduleDrain();
683
712
  }
684
713
  /** Re-arm while work remains. Setting an alarm that already exists is a no-op. */
685
714
  async scheduleDrain() {
686
- if (this.router === void 0) return;
687
- if (this.store.deliveries.due(FAR_FUTURE).length === 0) return;
715
+ if (!this.hasPendingWork()) return;
688
716
  if (await this.ctx.storage.getAlarm() !== null) return;
689
717
  await this.ctx.storage.setAlarm(Date.now() + DRAIN_INTERVAL_MS);
690
718
  }
719
+ /** Any delivery row pending, or any linked pin with a poll deadline set. */
720
+ hasPendingWork() {
721
+ if (this.router !== void 0 && this.store.deliveries.due(FAR_FUTURE).length > 0) return true;
722
+ return this.connectors.length > 0 && this.store.pinsDueBefore(FAR_FUTURE).length > 0;
723
+ }
691
724
  async upgrade(req, url, verify) {
692
725
  let token = url.searchParams.get("token");
693
726
  let acceptedProtocol;
@@ -763,4 +796,4 @@ var PinboxHubDO = class {
763
796
  }
764
797
  };
765
798
  //#endregion
766
- export { DoBroadcaster, PinboxHubDO };
799
+ export { DoBroadcaster, PinboxHubDO, buildConnectors };
@@ -0,0 +1,305 @@
1
+ import { pinsToMarkdown } from "./markdown.js";
2
+ import { z } from "zod";
3
+ import { SignJWT, importPKCS8 } from "jose";
4
+ //#region src/connectors/github-app.ts
5
+ const API_VERSION = "2022-11-28";
6
+ const USER_AGENT = "pinbox";
7
+ /** Mint a fresh installation token this close to expiry. */
8
+ const REFRESH_MARGIN_MS = 6e4;
9
+ /** App JWTs may live 10 minutes; nine leaves room for clock skew on GitHub's side. */
10
+ const APP_JWT_TTL_S = 540;
11
+ const COMMENTS_PER_PAGE = 100;
12
+ const MAX_COMMENT_PAGES = 10;
13
+ /** Ops that address an existing issue by number. */
14
+ const NUMBERED_OPS = /* @__PURE__ */ new Set([
15
+ "issue.comment",
16
+ "issue.view",
17
+ "issue.close",
18
+ "issue.reopen"
19
+ ]);
20
+ /** A transport failure carrying the hint the link route surfaces as E_CONNECTOR. */
21
+ var GithubAppError = class extends Error {
22
+ hint;
23
+ status;
24
+ constructor(message, status, hint) {
25
+ super(message);
26
+ this.name = "GithubAppError";
27
+ this.status = status;
28
+ this.hint = hint;
29
+ }
30
+ };
31
+ function createGithubAppTransport(opts) {
32
+ const fetchImpl = opts.fetchImpl ?? ((input, init) => fetch(input, init));
33
+ const now = opts.now ?? (() => Date.now());
34
+ const api = (opts.apiBase ?? "https://api.github.com").replace(/\/+$/, "");
35
+ const repo = opts.repo.replace(/^\/+|\/+$/g, "");
36
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) throw new GithubAppError(`GITHUB_REPO must be "owner/name", got "${opts.repo}"`, 0);
37
+ let keyPromise = null;
38
+ 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
+ async function installationToken() {
51
+ 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
+ };
63
+ return cached.token;
64
+ }
65
+ async function call(method, path, body) {
66
+ const res = await fetchImpl(`${api}${path}`, {
67
+ method,
68
+ headers: headers(await installationToken(), body !== void 0),
69
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
70
+ });
71
+ if (!res.ok) {
72
+ const detail = await res.text().catch(() => "");
73
+ if (res.status === 401) cached = null;
74
+ throw new GithubAppError(`GitHub ${method} ${path} failed: HTTP ${res.status}${detail ? ` — ${detail.slice(0, 200)}` : ""}`, res.status, res.status === 403 || res.status === 404 ? `check the App is installed on ${repo} with Issues: read & write` : void 0);
75
+ }
76
+ return await res.json();
77
+ }
78
+ async function allComments(number) {
79
+ const out = [];
80
+ for (let page = 1; page <= MAX_COMMENT_PAGES; page += 1) {
81
+ const batch = await call("GET", `/repos/${repo}/issues/${number}/comments?per_page=${COMMENTS_PER_PAGE}&page=${page}`);
82
+ out.push(...batch);
83
+ if (batch.length < COMMENTS_PER_PAGE) break;
84
+ }
85
+ return out;
86
+ }
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);
123
+ }
124
+ } };
125
+ }
126
+ function headers(bearer, json = false) {
127
+ return {
128
+ authorization: `Bearer ${bearer}`,
129
+ accept: "application/vnd.github+json",
130
+ "x-github-api-version": API_VERSION,
131
+ "user-agent": USER_AGENT,
132
+ ...json ? { "content-type": "application/json" } : {}
133
+ };
134
+ }
135
+ /** PKCS#8 PEM in, PKCS#8 PEM out; PKCS#1 PEM (GitHub's download) is wrapped into PKCS#8. */
136
+ function toPkcs8Pem(pem) {
137
+ const trimmed = pem.trim();
138
+ if (trimmed.includes("BEGIN PRIVATE KEY")) return trimmed;
139
+ if (!trimmed.includes("BEGIN RSA PRIVATE KEY")) throw new GithubAppError("GITHUB_APP_PRIVATE_KEY is not a PEM private key", 0, "paste the whole file GitHub downloaded, \"-----BEGIN RSA PRIVATE KEY-----\" through the END line");
140
+ return toPem("PRIVATE KEY", pkcs1ToPkcs8(pemBody(trimmed)));
141
+ }
142
+ function pemBody(pem) {
143
+ const b64 = pem.split("\n").filter((line) => !line.startsWith("-----")).join("").replace(/\s+/g, "");
144
+ const bin = atob(b64);
145
+ const out = new Uint8Array(bin.length);
146
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
147
+ return out;
148
+ }
149
+ function toPem(label, der) {
150
+ let bin = "";
151
+ for (const b of der) bin += String.fromCharCode(b);
152
+ return `-----BEGIN ${label}-----\n${btoa(bin).replace(/(.{64})/g, "$1\n").trimEnd()}\n-----END ${label}-----`;
153
+ }
154
+ /** rsaEncryption AlgorithmIdentifier: SEQUENCE { OID 1.2.840.113549.1.1.1, NULL }. */
155
+ const RSA_ALGORITHM = new Uint8Array([
156
+ 48,
157
+ 13,
158
+ 6,
159
+ 9,
160
+ 42,
161
+ 134,
162
+ 72,
163
+ 134,
164
+ 247,
165
+ 13,
166
+ 1,
167
+ 1,
168
+ 1,
169
+ 5,
170
+ 0
171
+ ]);
172
+ const VERSION_ZERO = new Uint8Array([
173
+ 2,
174
+ 1,
175
+ 0
176
+ ]);
177
+ /** PrivateKeyInfo ::= SEQUENCE { version 0, algorithm rsaEncryption, privateKey OCTET STRING }. */
178
+ function pkcs1ToPkcs8(pkcs1) {
179
+ return derTlv(48, concat(VERSION_ZERO, RSA_ALGORITHM, derTlv(4, pkcs1)));
180
+ }
181
+ function derTlv(tag, body) {
182
+ const len = body.length;
183
+ let header;
184
+ if (len < 128) header = [tag, len];
185
+ else {
186
+ const bytes = [];
187
+ for (let v = len; v > 0; v = Math.floor(v / 256)) bytes.unshift(v & 255);
188
+ header = [
189
+ tag,
190
+ 128 | bytes.length,
191
+ ...bytes
192
+ ];
193
+ }
194
+ return concat(new Uint8Array(header), body);
195
+ }
196
+ function concat(...parts) {
197
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
198
+ let at = 0;
199
+ for (const p of parts) {
200
+ out.set(p, at);
201
+ at += p.length;
202
+ }
203
+ return out;
204
+ }
205
+ //#endregion
206
+ //#region src/connectors/slack.ts
207
+ /**
208
+ * request(op, params) → POST https://slack.com/api/<op> (JSON, bearer botToken).
209
+ * Slack's `{ok:false, error}` becomes a rejection carrying the Slack error string —
210
+ * the route layer surfaces it as 502 E_CONNECTOR.
211
+ */
212
+ function createSlackTransport(opts) {
213
+ const fetchImpl = opts.fetchImpl ?? fetch;
214
+ return { async request(op, params) {
215
+ const res = await fetchImpl(`https://slack.com/api/${op}`, {
216
+ method: "POST",
217
+ headers: {
218
+ authorization: `Bearer ${opts.botToken}`,
219
+ "content-type": "application/json; charset=utf-8"
220
+ },
221
+ body: JSON.stringify(params)
222
+ });
223
+ if (!res.ok) throw new Error(`slack ${op} failed: HTTP ${res.status}`);
224
+ const data = await res.json();
225
+ const envelope = SlackEnvelopeSchema.parse(data);
226
+ if (!envelope.ok) throw new Error(`slack ${op} failed: ${envelope.error ?? "unknown_error"}`);
227
+ return data;
228
+ } };
229
+ }
230
+ /** Links are `ref: "<channel>/<thread_ts>"`; every thread op derives channel + ts from the ref. */
231
+ function createSlackConnector(transport, opts) {
232
+ return {
233
+ name: "slack",
234
+ async createItem(pin, thread) {
235
+ const text = [pinsToMarkdown([pin], "standard"), ...thread.map((m) => `${m.role}: ${m.text}`)].join("\n").trimEnd();
236
+ const posted = PostMessageSchema.parse(await transport.request("chat.postMessage", {
237
+ channel: opts.channel,
238
+ text
239
+ }));
240
+ const channel = posted.channel ?? opts.channel;
241
+ const permalink = PermalinkSchema.parse(await transport.request("chat.getPermalink", {
242
+ channel,
243
+ message_ts: posted.ts
244
+ }));
245
+ return {
246
+ connector: "slack",
247
+ ref: `${channel}/${posted.ts}`,
248
+ url: permalink.permalink
249
+ };
250
+ },
251
+ async postComment(link, message) {
252
+ const { channel, ts } = parseRef(link.ref);
253
+ await transport.request("chat.postMessage", {
254
+ channel,
255
+ thread_ts: ts,
256
+ text: message.text
257
+ });
258
+ },
259
+ async sync(link, events) {
260
+ const { channel, ts } = parseRef(link.ref);
261
+ const replies = RepliesSchema.parse(await transport.request("conversations.replies", {
262
+ channel,
263
+ ts
264
+ }));
265
+ for (const reply of replies.messages) {
266
+ if (reply.ts === ts) continue;
267
+ const atMs = slackTsToMs(reply.ts);
268
+ await events.onRemoteComment(link, {
269
+ origin: `slack:${reply.user}`,
270
+ text: reply.text ?? "",
271
+ at: new Date(atMs).toISOString()
272
+ });
273
+ }
274
+ },
275
+ async setRemoteStatus() {}
276
+ };
277
+ }
278
+ const SlackEnvelopeSchema = z.looseObject({
279
+ ok: z.boolean(),
280
+ error: z.string().optional()
281
+ });
282
+ const PostMessageSchema = z.looseObject({
283
+ channel: z.string().optional(),
284
+ ts: z.string()
285
+ });
286
+ const PermalinkSchema = z.looseObject({ permalink: z.string() });
287
+ const RepliesSchema = z.looseObject({ messages: z.array(z.looseObject({
288
+ ts: z.string(),
289
+ user: z.string(),
290
+ text: z.string().optional()
291
+ })) });
292
+ function parseRef(ref) {
293
+ const slash = ref.indexOf("/");
294
+ if (slash <= 0 || slash === ref.length - 1) throw new Error(`slack link ref must be "<channel>/<thread_ts>", got "${ref}"`);
295
+ return {
296
+ channel: ref.slice(0, slash),
297
+ ts: ref.slice(slash + 1)
298
+ };
299
+ }
300
+ /** Slack ts is "<epoch-seconds>.<suffix>" — epoch milliseconds for the reported `at`. */
301
+ function slackTsToMs(ts) {
302
+ return Number(ts) * 1e3;
303
+ }
304
+ //#endregion
305
+ export { pkcs1ToPkcs8 as a, createGithubAppTransport as i, createSlackTransport as n, toPkcs8Pem as o, GithubAppError as r, createSlackConnector as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autono/pinbox-core",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {