@saacms/github-bridge 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.
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Structural subset of the GitHub Issues API surface this bridge uses.
3
+ *
4
+ * Mirrors the `auth-better-auth` discipline: we never import `@octokit/*`
5
+ * here. `GitHubClientLike` is a structural interface covering exactly the
6
+ * four operations the escalation + scale-cost flows need. The host injects
7
+ * a real Octokit / GitHub-App-backed implementation that satisfies the
8
+ * shape; this package compiles and tests with a hand-rolled mock.
9
+ */
10
+ export type IssueState = "open" | "closed";
11
+ export interface CreateIssueArgs {
12
+ readonly owner: string;
13
+ readonly repo: string;
14
+ readonly title: string;
15
+ readonly body: string;
16
+ readonly labels: ReadonlyArray<string>;
17
+ }
18
+ export interface CreatedIssue {
19
+ readonly number: number;
20
+ readonly htmlUrl: string;
21
+ }
22
+ export interface UpdateIssueArgs {
23
+ readonly owner: string;
24
+ readonly repo: string;
25
+ readonly number: number;
26
+ readonly labels?: ReadonlyArray<string>;
27
+ readonly state?: IssueState;
28
+ }
29
+ export interface ListIssuesArgs {
30
+ readonly owner: string;
31
+ readonly repo: string;
32
+ readonly labels?: ReadonlyArray<string>;
33
+ readonly state?: IssueState;
34
+ }
35
+ export interface ListedIssue {
36
+ readonly number: number;
37
+ readonly title: string;
38
+ readonly labels: ReadonlyArray<string>;
39
+ readonly state: IssueState;
40
+ }
41
+ export interface AddCommentArgs {
42
+ readonly owner: string;
43
+ readonly repo: string;
44
+ readonly number: number;
45
+ readonly body: string;
46
+ }
47
+ /**
48
+ * The injected GitHub client. Any object with this shape works; a real
49
+ * Octokit wrapper supplied by the host satisfies it. The bridge never
50
+ * depends on the concrete SDK.
51
+ */
52
+ export interface GitHubClientLike {
53
+ createIssue(args: CreateIssueArgs): Promise<CreatedIssue>;
54
+ updateIssue(args: UpdateIssueArgs): Promise<void>;
55
+ listIssues(args: ListIssuesArgs): Promise<ReadonlyArray<ListedIssue>>;
56
+ addComment(args: AddCommentArgs): Promise<void>;
57
+ }
58
+ export interface GitHubBridgeOptions {
59
+ readonly client: GitHubClientLike;
60
+ readonly owner: string;
61
+ readonly repo: string;
62
+ }
63
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE1C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACvC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAA;CAC5B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACtC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACzD,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjD,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAA;IACrE,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Requested-block escalation → GitHub Issue, per ADR 0025 + CONTEXT.md.
3
+ *
4
+ * On Publish, a Requested block materializes as a GitHub Issue: the Owner's
5
+ * plain-language annotation + a deep-link back into the editor + a rendered
6
+ * snapshot + a machine-readable marker for round-trip. The Issue is the
7
+ * dev-side system of record; lifecycle is derived purely from its labels
8
+ * and state so a webhook can map it back to the editor badge.
9
+ */
10
+ import type { GitHubBridgeOptions } from "./client.ts";
11
+ export declare const ESCALATION_LABEL = "saacms:escalation";
12
+ /** Lifecycle stages, per the Team-Topologies Collaboration round-trip. */
13
+ export type Lifecycle = "open" | "triaged" | "approved" | "delivered";
14
+ export interface EscalationRequest {
15
+ readonly pageId: string;
16
+ readonly blockPath: string;
17
+ readonly annotation: string;
18
+ readonly editorDeepLink: string;
19
+ readonly snapshotMarkdown: string;
20
+ }
21
+ export interface EscalationFiled {
22
+ readonly issueNumber: number;
23
+ readonly htmlUrl: string;
24
+ }
25
+ /** The subset of the escalation request we round-trip through the marker. */
26
+ export interface EscalationMarker {
27
+ readonly pageId: string;
28
+ readonly blockPath: string;
29
+ readonly annotation: string;
30
+ }
31
+ /**
32
+ * File the escalation Issue. Deterministic title + body so the same request
33
+ * always produces the same artifact (and the marker round-trips cleanly).
34
+ */
35
+ export declare function fileEscalationIssue(opts: GitHubBridgeOptions, req: EscalationRequest): Promise<EscalationFiled>;
36
+ /**
37
+ * Pure lifecycle projection. closed → delivered (closed wins over all);
38
+ * open + `approved` → approved (approved implies triaged-or-beyond);
39
+ * open + `triaged` → triaged; else open.
40
+ */
41
+ export declare function mapIssueToLifecycle(issue: {
42
+ readonly labels: ReadonlyArray<string>;
43
+ readonly state: "open" | "closed";
44
+ }): Lifecycle;
45
+ /**
46
+ * Extract the machine-readable escalation marker from an Issue body.
47
+ * Untrusted-input boundary: returns null on absent/malformed marker,
48
+ * never throws.
49
+ */
50
+ export declare function parseEscalationMarker(issueBody: string): EscalationMarker | null;
51
+ //# sourceMappingURL=escalation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escalation.d.ts","sourceRoot":"","sources":["../src/escalation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEtD,eAAO,MAAM,gBAAgB,sBAAsB,CAAA;AAEnD,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,CAAA;AAQrE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AA2BD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE,iBAAiB,GACrB,OAAO,CAAC,eAAe,CAAC,CAS1B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAA;CAClC,GAAG,SAAS,CAKZ;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA4BhF"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @saacms/github-bridge — the ADR-0025 developer control plane.
3
+ *
4
+ * The Developer's working surface is GitHub Issues, never the admin. A
5
+ * Requested-block escalation and a Scale & Cost threshold crossing each
6
+ * materialize as a GitHub Issue; Issue activity is the dev-side system of
7
+ * record; a webhook maps it back to a thin Owner-facing editor badge.
8
+ *
9
+ * This package is pure and fully mock-testable: it never imports
10
+ * `@octokit/*` or touches the network. The host wires the real
11
+ * GitHub-App-backed client that satisfies `GitHubClientLike`.
12
+ */
13
+ export type { GitHubClientLike, GitHubBridgeOptions, CreateIssueArgs, CreatedIssue, UpdateIssueArgs, ListIssuesArgs, ListedIssue, AddCommentArgs, IssueState, } from "./client.ts";
14
+ export { fileEscalationIssue, mapIssueToLifecycle, parseEscalationMarker, ESCALATION_LABEL, } from "./escalation.ts";
15
+ export type { EscalationRequest, EscalationFiled, EscalationMarker, Lifecycle, } from "./escalation.ts";
16
+ export { fileScaleCostIssue, shouldFileNew, SCALE_COST_LABEL, } from "./scale-cost-issue.ts";
17
+ export type { ScaleCostSignal, ScaleCostFiled, ScaleAxis, } from "./scale-cost-issue.ts";
18
+ export { parseIssueWebhook, webhookToBadgeUpdate } from "./webhook.ts";
19
+ export type { ParsedIssueWebhook, BadgeUpdate } from "./webhook.ts";
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,YAAY,EACV,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,YAAY,EACZ,eAAe,EACf,cAAc,EACd,WAAW,EACX,cAAc,EACd,UAAU,GACX,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,SAAS,GACV,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,gBAAgB,GACjB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,cAAc,EACd,SAAS,GACV,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACtE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,195 @@
1
+ // src/escalation.ts
2
+ var ESCALATION_LABEL = "saacms:escalation";
3
+ var TRIAGED_LABEL = "triaged";
4
+ var APPROVED_LABEL = "approved";
5
+ var MARKER_OPEN = "<!-- saacms:escalation ";
6
+ var MARKER_CLOSE = " -->";
7
+ function escalationTitle(pageId, blockPath) {
8
+ return `[saacms] Requested block: ${pageId} ${blockPath}`;
9
+ }
10
+ function escalationBody(req) {
11
+ const marker = {
12
+ pageId: req.pageId,
13
+ blockPath: req.blockPath,
14
+ annotation: req.annotation
15
+ };
16
+ return [
17
+ "## Requested block",
18
+ "",
19
+ req.annotation,
20
+ "",
21
+ `**Editor:** ${req.editorDeepLink}`,
22
+ "",
23
+ "### Rendered snapshot",
24
+ "",
25
+ req.snapshotMarkdown,
26
+ "",
27
+ `${MARKER_OPEN}${JSON.stringify(marker)}${MARKER_CLOSE}`
28
+ ].join(`
29
+ `);
30
+ }
31
+ async function fileEscalationIssue(opts, req) {
32
+ const created = await opts.client.createIssue({
33
+ owner: opts.owner,
34
+ repo: opts.repo,
35
+ title: escalationTitle(req.pageId, req.blockPath),
36
+ body: escalationBody(req),
37
+ labels: [ESCALATION_LABEL]
38
+ });
39
+ return { issueNumber: created.number, htmlUrl: created.htmlUrl };
40
+ }
41
+ function mapIssueToLifecycle(issue) {
42
+ if (issue.state === "closed")
43
+ return "delivered";
44
+ if (issue.labels.includes(APPROVED_LABEL))
45
+ return "approved";
46
+ if (issue.labels.includes(TRIAGED_LABEL))
47
+ return "triaged";
48
+ return "open";
49
+ }
50
+ function parseEscalationMarker(issueBody) {
51
+ if (typeof issueBody !== "string")
52
+ return null;
53
+ const start = issueBody.indexOf(MARKER_OPEN);
54
+ if (start === -1)
55
+ return null;
56
+ const jsonStart = start + MARKER_OPEN.length;
57
+ const end = issueBody.indexOf(MARKER_CLOSE, jsonStart);
58
+ if (end === -1)
59
+ return null;
60
+ const raw = issueBody.slice(jsonStart, end).trim();
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ } catch {
65
+ return null;
66
+ }
67
+ if (parsed === null || typeof parsed !== "object")
68
+ return null;
69
+ const obj = parsed;
70
+ if (typeof obj.pageId !== "string" || typeof obj.blockPath !== "string" || typeof obj.annotation !== "string") {
71
+ return null;
72
+ }
73
+ return {
74
+ pageId: obj.pageId,
75
+ blockPath: obj.blockPath,
76
+ annotation: obj.annotation
77
+ };
78
+ }
79
+ // src/scale-cost-issue.ts
80
+ var SCALE_COST_LABEL = "saacms:scale-cost";
81
+ var MARKER_OPEN2 = "<!-- saacms:scale-cost ";
82
+ var MARKER_CLOSE2 = " -->";
83
+ function scaleCostTitle(axis, collection) {
84
+ const suffix = collection ? ` — ${collection}` : "";
85
+ return `[saacms] Scale & Cost: ${axis} threshold crossed${suffix}`;
86
+ }
87
+ function scaleCostBody(signal) {
88
+ const marker = {
89
+ axis: signal.axis,
90
+ collection: signal.collection ?? null
91
+ };
92
+ return [
93
+ "## Scale & Cost threshold crossed",
94
+ "",
95
+ ...signal.metricLines,
96
+ "",
97
+ `**Recommended action:** ${signal.recommendedAction}`,
98
+ "",
99
+ `${MARKER_OPEN2}${JSON.stringify(marker)}${MARKER_CLOSE2}`
100
+ ].join(`
101
+ `);
102
+ }
103
+ function shouldFileNew(existing, axis, collection) {
104
+ const wanted = scaleCostTitle(axis, collection);
105
+ for (const issue of existing) {
106
+ if (issue.state !== "open")
107
+ continue;
108
+ if (!issue.labels.includes(SCALE_COST_LABEL))
109
+ continue;
110
+ if (issue.title === wanted)
111
+ return false;
112
+ }
113
+ return true;
114
+ }
115
+ async function fileScaleCostIssue(opts, signal, existing = []) {
116
+ if (!shouldFileNew(existing, signal.axis, signal.collection)) {
117
+ const wanted = scaleCostTitle(signal.axis, signal.collection);
118
+ const match = existing.find((i) => i.state === "open" && i.labels.includes(SCALE_COST_LABEL) && i.title === wanted);
119
+ if (match) {
120
+ await opts.client.addComment({
121
+ owner: opts.owner,
122
+ repo: opts.repo,
123
+ number: match.number,
124
+ body: scaleCostBody(signal)
125
+ });
126
+ return { issueNumber: match.number };
127
+ }
128
+ }
129
+ const created = await opts.client.createIssue({
130
+ owner: opts.owner,
131
+ repo: opts.repo,
132
+ title: scaleCostTitle(signal.axis, signal.collection),
133
+ body: scaleCostBody(signal),
134
+ labels: [SCALE_COST_LABEL]
135
+ });
136
+ return { issueNumber: created.number };
137
+ }
138
+ // src/webhook.ts
139
+ function isObject(v) {
140
+ return typeof v === "object" && v !== null;
141
+ }
142
+ function parseIssueWebhook(payload) {
143
+ if (!isObject(payload))
144
+ return null;
145
+ const action = payload.action;
146
+ if (typeof action !== "string")
147
+ return null;
148
+ const issue = payload.issue;
149
+ if (!isObject(issue))
150
+ return null;
151
+ const number = issue.number;
152
+ if (typeof number !== "number" || !Number.isFinite(number))
153
+ return null;
154
+ const state = issue.state;
155
+ if (state !== "open" && state !== "closed")
156
+ return null;
157
+ const rawBody = issue.body;
158
+ const body = typeof rawBody === "string" ? rawBody : "";
159
+ const rawLabels = issue.labels;
160
+ const labels = [];
161
+ if (Array.isArray(rawLabels)) {
162
+ for (const l of rawLabels) {
163
+ if (isObject(l) && typeof l.name === "string") {
164
+ labels.push(l.name);
165
+ } else if (typeof l === "string") {
166
+ labels.push(l);
167
+ }
168
+ }
169
+ }
170
+ return { action, issue: { number, labels, state, body } };
171
+ }
172
+ function webhookToBadgeUpdate(payload) {
173
+ const parsed = parseIssueWebhook(payload);
174
+ if (parsed === null)
175
+ return null;
176
+ return {
177
+ issueNumber: parsed.issue.number,
178
+ lifecycle: mapIssueToLifecycle({
179
+ labels: parsed.issue.labels,
180
+ state: parsed.issue.state
181
+ }),
182
+ marker: parseEscalationMarker(parsed.issue.body)
183
+ };
184
+ }
185
+ export {
186
+ webhookToBadgeUpdate,
187
+ shouldFileNew,
188
+ parseIssueWebhook,
189
+ parseEscalationMarker,
190
+ mapIssueToLifecycle,
191
+ fileScaleCostIssue,
192
+ fileEscalationIssue,
193
+ SCALE_COST_LABEL,
194
+ ESCALATION_LABEL
195
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Scale & Cost threshold crossing → GitHub Issue, per ADR 0025 + CONTEXT.md.
3
+ *
4
+ * When the site crosses a growth point, the Developer gets a tracked Issue
5
+ * (labeled `saacms:scale-cost`) with the numbers + the exact recommended
6
+ * plugin/tier action — the Developer never opens the admin. Idempotent:
7
+ * one open Issue per axis(+collection); a re-crossing comments rather than
8
+ * duplicating, so it stays a single "failing test" the Developer acts on.
9
+ */
10
+ import type { GitHubBridgeOptions, ListedIssue } from "./client.ts";
11
+ export declare const SCALE_COST_LABEL = "saacms:scale-cost";
12
+ export type ScaleAxis = "serve" | "publish";
13
+ export interface ScaleCostSignal {
14
+ readonly axis: ScaleAxis;
15
+ readonly collection?: string;
16
+ readonly metricLines: ReadonlyArray<string>;
17
+ readonly recommendedAction: string;
18
+ }
19
+ export interface ScaleCostFiled {
20
+ readonly issueNumber: number;
21
+ }
22
+ /**
23
+ * Pure idempotency decision: file a fresh Issue only when no open
24
+ * scale-cost Issue already covers this exact axis(+collection).
25
+ */
26
+ export declare function shouldFileNew(existing: ReadonlyArray<ListedIssue>, axis: ScaleAxis, collection?: string): boolean;
27
+ /**
28
+ * File (or coalesce into) the Scale & Cost Issue. `existing` is the caller's
29
+ * `listIssues` result; when a matching open Issue exists we `addComment`
30
+ * instead of duplicating and return that Issue's number.
31
+ */
32
+ export declare function fileScaleCostIssue(opts: GitHubBridgeOptions, signal: ScaleCostSignal, existing?: ReadonlyArray<ListedIssue>): Promise<ScaleCostFiled>;
33
+ //# sourceMappingURL=scale-cost-issue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scale-cost-issue.d.ts","sourceRoot":"","sources":["../src/scale-cost-issue.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEnE,eAAO,MAAM,gBAAgB,sBAAsB,CAAA;AAKnD,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,SAAS,CAAA;AAE3C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;IACxB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AA4BD;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,EACpC,IAAI,EAAE,SAAS,EACf,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAQT;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,eAAe,EACvB,QAAQ,GAAE,aAAa,CAAC,WAAW,CAAM,GACxC,OAAO,CAAC,cAAc,CAAC,CA6BzB"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * GitHub `issues` webhook → editor badge update, per ADR 0025.
3
+ *
4
+ * Webhooks are untrusted input: every parser here is defensive and returns
5
+ * null on any shape mismatch — never throws. `webhookToBadgeUpdate`
6
+ * composes the parse + the pure lifecycle projection + the marker
7
+ * round-trip so the saacms webhook handler can flip the Owner-side badge.
8
+ */
9
+ import { type EscalationMarker, type Lifecycle } from "./escalation.ts";
10
+ export interface ParsedIssueWebhook {
11
+ readonly action: string;
12
+ readonly issue: {
13
+ readonly number: number;
14
+ readonly labels: ReadonlyArray<string>;
15
+ readonly state: "open" | "closed";
16
+ readonly body: string;
17
+ };
18
+ }
19
+ /**
20
+ * Defensive parse of the GitHub `issues` event payload. Pulls action +
21
+ * issue.number + issue.labels[].name + issue.state + issue.body. Returns
22
+ * null on any missing/wrong-typed field.
23
+ */
24
+ export declare function parseIssueWebhook(payload: unknown): ParsedIssueWebhook | null;
25
+ export interface BadgeUpdate {
26
+ readonly issueNumber: number;
27
+ readonly lifecycle: Lifecycle;
28
+ readonly marker: EscalationMarker | null;
29
+ }
30
+ /**
31
+ * Compose parse → lifecycle → marker. Returns null when the payload is not
32
+ * a parseable `issues` event.
33
+ */
34
+ export declare function webhookToBadgeUpdate(payload: unknown): BadgeUpdate | null;
35
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAA;AAExB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;QACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAA;QACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KACtB,CAAA;CACF;AAMD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CA+B7E;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAA;CACzC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAWzE"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@saacms/github-bridge",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "import": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc --build",
18
+ "typecheck": "tsc --build --noEmit",
19
+ "prepack": "cp package.json package.json.pack-bak && bun run ../../scripts/prepack-pkg.ts",
20
+ "postpack": "mv package.json.pack-bak package.json"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "@saacms/core": "workspace:*"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "latest",
30
+ "typescript": "^5.7.0"
31
+ },
32
+ "main": "./dist/index.js",
33
+ "types": "./dist/index.d.ts"
34
+ }