harper-knowledge 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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +276 -0
  3. package/config.yaml +17 -0
  4. package/dist/core/embeddings.d.ts +29 -0
  5. package/dist/core/embeddings.js +199 -0
  6. package/dist/core/entries.d.ts +85 -0
  7. package/dist/core/entries.js +235 -0
  8. package/dist/core/history.d.ts +30 -0
  9. package/dist/core/history.js +119 -0
  10. package/dist/core/search.d.ts +23 -0
  11. package/dist/core/search.js +306 -0
  12. package/dist/core/tags.d.ts +32 -0
  13. package/dist/core/tags.js +76 -0
  14. package/dist/core/triage.d.ts +55 -0
  15. package/dist/core/triage.js +126 -0
  16. package/dist/http-utils.d.ts +37 -0
  17. package/dist/http-utils.js +132 -0
  18. package/dist/index.d.ts +21 -0
  19. package/dist/index.js +76 -0
  20. package/dist/mcp/server.d.ts +24 -0
  21. package/dist/mcp/server.js +124 -0
  22. package/dist/mcp/tools.d.ts +13 -0
  23. package/dist/mcp/tools.js +497 -0
  24. package/dist/oauth/authorize.d.ts +27 -0
  25. package/dist/oauth/authorize.js +438 -0
  26. package/dist/oauth/github.d.ts +28 -0
  27. package/dist/oauth/github.js +62 -0
  28. package/dist/oauth/keys.d.ts +33 -0
  29. package/dist/oauth/keys.js +100 -0
  30. package/dist/oauth/metadata.d.ts +21 -0
  31. package/dist/oauth/metadata.js +55 -0
  32. package/dist/oauth/middleware.d.ts +22 -0
  33. package/dist/oauth/middleware.js +64 -0
  34. package/dist/oauth/register.d.ts +14 -0
  35. package/dist/oauth/register.js +83 -0
  36. package/dist/oauth/token.d.ts +15 -0
  37. package/dist/oauth/token.js +178 -0
  38. package/dist/oauth/validate.d.ts +30 -0
  39. package/dist/oauth/validate.js +52 -0
  40. package/dist/resources/HistoryResource.d.ts +38 -0
  41. package/dist/resources/HistoryResource.js +38 -0
  42. package/dist/resources/KnowledgeEntryResource.d.ts +64 -0
  43. package/dist/resources/KnowledgeEntryResource.js +157 -0
  44. package/dist/resources/QueryLogResource.d.ts +20 -0
  45. package/dist/resources/QueryLogResource.js +57 -0
  46. package/dist/resources/ServiceKeyResource.d.ts +51 -0
  47. package/dist/resources/ServiceKeyResource.js +132 -0
  48. package/dist/resources/TagResource.d.ts +25 -0
  49. package/dist/resources/TagResource.js +32 -0
  50. package/dist/resources/TriageResource.d.ts +51 -0
  51. package/dist/resources/TriageResource.js +107 -0
  52. package/dist/types.d.ts +317 -0
  53. package/dist/types.js +7 -0
  54. package/dist/webhooks/datadog.d.ts +26 -0
  55. package/dist/webhooks/datadog.js +120 -0
  56. package/dist/webhooks/github.d.ts +24 -0
  57. package/dist/webhooks/github.js +167 -0
  58. package/dist/webhooks/middleware.d.ts +14 -0
  59. package/dist/webhooks/middleware.js +161 -0
  60. package/dist/webhooks/types.d.ts +17 -0
  61. package/dist/webhooks/types.js +4 -0
  62. package/package.json +72 -0
  63. package/schema/knowledge.graphql +134 -0
  64. package/web/index.html +735 -0
  65. package/web/js/app.js +461 -0
  66. package/web/js/detail.js +223 -0
  67. package/web/js/editor.js +303 -0
  68. package/web/js/search.js +238 -0
  69. package/web/js/triage.js +305 -0
@@ -0,0 +1,317 @@
1
+ /**
2
+ * harper-knowledge — Type Definitions
3
+ *
4
+ * TypeScript interfaces for all table records, search parameters,
5
+ * and Harper global type declarations.
6
+ */
7
+ /**
8
+ * Describes what environment/configuration a knowledge entry applies to.
9
+ * Stored on KnowledgeEntry.appliesTo.
10
+ */
11
+ export interface ApplicabilityScope {
12
+ /** Harper version or semver range (e.g., ">=4.6.0") */
13
+ harper?: string;
14
+ /** Storage engine type (e.g., "lmdb", "rocksdb") */
15
+ storageEngine?: string;
16
+ /** Node.js version or semver range */
17
+ node?: string;
18
+ /** Platform identifier (e.g., "linux", "darwin", "win32") */
19
+ platform?: string;
20
+ }
21
+ /**
22
+ * Caller's context for search filtering — same shape as ApplicabilityScope.
23
+ * Used to boost or demote results based on the caller's environment.
24
+ */
25
+ export interface ApplicabilityContext {
26
+ /** Harper version the caller is running */
27
+ harper?: string;
28
+ /** Storage engine the caller is using */
29
+ storageEngine?: string;
30
+ /** Node.js version the caller is running */
31
+ node?: string;
32
+ /** Platform the caller is running on */
33
+ platform?: string;
34
+ }
35
+ /**
36
+ * Core knowledge base entry with vector embedding for semantic search.
37
+ * Stored in tables.KnowledgeEntry.
38
+ */
39
+ export interface KnowledgeEntry {
40
+ id: string;
41
+ title: string;
42
+ content: string;
43
+ tags: string[];
44
+ appliesTo?: ApplicabilityScope;
45
+ source?: string;
46
+ sourceUrl?: string;
47
+ /** "verified", "reviewed", or "ai-generated" */
48
+ confidence: string;
49
+ addedBy?: string;
50
+ reviewedBy?: string;
51
+ embedding?: number[];
52
+ supersedesId?: string;
53
+ supersededById?: string;
54
+ siblingIds?: string[];
55
+ relatedIds?: string[];
56
+ customerContext?: Record<string, unknown>;
57
+ deprecated?: boolean;
58
+ createdAt?: Date;
59
+ updatedAt?: Date;
60
+ }
61
+ /**
62
+ * Data for creating or updating a knowledge entry.
63
+ * All fields except id are optional for updates.
64
+ */
65
+ export interface KnowledgeEntryInput {
66
+ id?: string;
67
+ title: string;
68
+ content: string;
69
+ tags?: string[];
70
+ appliesTo?: ApplicabilityScope;
71
+ source?: string;
72
+ sourceUrl?: string;
73
+ confidence?: string;
74
+ addedBy?: string;
75
+ reviewedBy?: string;
76
+ customerContext?: Record<string, unknown>;
77
+ deprecated?: boolean;
78
+ }
79
+ /**
80
+ * Data for partial updates to an existing knowledge entry.
81
+ */
82
+ export interface KnowledgeEntryUpdate {
83
+ title?: string;
84
+ content?: string;
85
+ tags?: string[];
86
+ appliesTo?: ApplicabilityScope;
87
+ source?: string;
88
+ sourceUrl?: string;
89
+ confidence?: string;
90
+ addedBy?: string;
91
+ reviewedBy?: string;
92
+ customerContext?: Record<string, unknown>;
93
+ deprecated?: boolean;
94
+ }
95
+ /**
96
+ * Triage item for webhook intake queue.
97
+ * 7-day TTL. Stored in tables.TriageItem.
98
+ */
99
+ export interface TriageItem {
100
+ id: string;
101
+ source: string;
102
+ sourceId?: string;
103
+ rawPayload?: unknown;
104
+ summary?: string;
105
+ /** "pending", "processing", "accepted", or "dismissed" */
106
+ status: string;
107
+ matchedEntryId?: string;
108
+ draftEntryId?: string;
109
+ action?: string;
110
+ processedBy?: string;
111
+ createdAt?: Date;
112
+ processedAt?: Date;
113
+ }
114
+ /**
115
+ * Tag metadata with usage count.
116
+ * Tag name serves as the ID. Stored in tables.KnowledgeTag.
117
+ */
118
+ export interface KnowledgeTag {
119
+ /** Tag name (also serves as primary key) */
120
+ id: string;
121
+ description?: string;
122
+ entryCount: number;
123
+ }
124
+ /**
125
+ * Search query analytics log entry.
126
+ * 30-day TTL. Stored in tables.QueryLog.
127
+ */
128
+ export interface QueryLog {
129
+ id: string;
130
+ query: string;
131
+ context?: ApplicabilityContext;
132
+ source?: string;
133
+ resultCount: number;
134
+ topResultId?: string;
135
+ createdAt?: Date;
136
+ }
137
+ /**
138
+ * API key for webhooks and service accounts.
139
+ * Stored in tables.ServiceKey.
140
+ */
141
+ export interface ServiceKey {
142
+ id: string;
143
+ name: string;
144
+ keyHash: string;
145
+ /** "service_account" or "ai_agent" */
146
+ role: string;
147
+ permissions?: Record<string, unknown>;
148
+ createdBy?: string;
149
+ createdAt?: Date;
150
+ lastUsedAt?: Date;
151
+ }
152
+ /**
153
+ * Edit history record for a knowledge entry.
154
+ * Append-only audit log. Stored in tables.KnowledgeEntryEdit.
155
+ */
156
+ export interface KnowledgeEntryEdit {
157
+ id: string;
158
+ /** ID of the knowledge entry that was edited */
159
+ entryId: string;
160
+ /** Username of who made the edit */
161
+ editedBy: string;
162
+ /** Brief description of what changed and why */
163
+ editSummary?: string;
164
+ /** Snapshot of the entry before this edit */
165
+ previousSnapshot: Record<string, unknown>;
166
+ /** List of field names that were changed */
167
+ changedFields: string[];
168
+ createdAt?: Date;
169
+ }
170
+ /**
171
+ * Parameters for searching the knowledge base.
172
+ */
173
+ export interface SearchParams {
174
+ /** Search query string */
175
+ query: string;
176
+ /** Filter by tags */
177
+ tags?: string[];
178
+ /** Maximum number of results */
179
+ limit?: number;
180
+ /** Caller's environment context for applicability filtering */
181
+ context?: ApplicabilityContext;
182
+ /** Search mode: keyword, semantic, or hybrid (default) */
183
+ mode?: "keyword" | "semantic" | "hybrid";
184
+ }
185
+ /**
186
+ * A search result extends a knowledge entry with relevance scoring.
187
+ */
188
+ export interface SearchResult extends KnowledgeEntry {
189
+ /** Relevance score (higher is better) */
190
+ score: number;
191
+ /** How this result was matched: "keyword", "semantic", or "hybrid" */
192
+ matchType: string;
193
+ }
194
+ /** Action to take on a triage item */
195
+ export type TriageAction = "accepted" | "dismissed" | "linked";
196
+ /** Options for processing a triage item */
197
+ export interface TriageProcessOptions {
198
+ /** Entry data to create when accepting */
199
+ entryData?: KnowledgeEntryInput;
200
+ /** Existing entry ID to link to */
201
+ linkedEntryId?: string;
202
+ }
203
+ /**
204
+ * Webhook configuration for GitHub and Datadog integrations.
205
+ */
206
+ export interface WebhookConfig {
207
+ github?: {
208
+ /** HMAC-SHA256 secret for validating GitHub webhook signatures */
209
+ secret?: string;
210
+ /** Events to process (default: all supported events) */
211
+ enabledEvents?: string[];
212
+ };
213
+ datadog?: {
214
+ /** API key for validating Datadog webhook requests */
215
+ apiKey?: string;
216
+ };
217
+ }
218
+ /**
219
+ * Plugin configuration options (from parent's config.yaml).
220
+ */
221
+ export interface KnowledgePluginConfig {
222
+ /** Embedding model name (default: "nomic-embed-text") */
223
+ embeddingModel?: string;
224
+ /** Webhook intake configuration */
225
+ webhooks?: WebhookConfig;
226
+ }
227
+ /**
228
+ * Logger interface matching Harper's component logger.
229
+ */
230
+ export interface Logger {
231
+ info?: (message: string, ...args: unknown[]) => void;
232
+ error?: (message: string, ...args: unknown[]) => void;
233
+ warn?: (message: string, ...args: unknown[]) => void;
234
+ debug?: (message: string, ...args: unknown[]) => void;
235
+ }
236
+ /**
237
+ * Harper table search query.
238
+ */
239
+ export interface TableSearchQuery {
240
+ conditions?: TableCondition[];
241
+ sort?: TableSort;
242
+ limit?: number;
243
+ offset?: number;
244
+ }
245
+ export interface TableCondition {
246
+ attribute: string;
247
+ comparator: string;
248
+ value: unknown;
249
+ }
250
+ export interface TableSort {
251
+ attribute: string;
252
+ descending?: boolean;
253
+ /** Target vector for HNSW nearest-neighbor search */
254
+ target?: number[];
255
+ }
256
+ /**
257
+ * Harper table instance — methods available on each table global.
258
+ */
259
+ export interface Table {
260
+ get(id: string): Promise<Record<string, unknown> | null>;
261
+ put(record: Record<string, unknown>): Promise<void>;
262
+ delete(id: string): Promise<void>;
263
+ search(query: TableSearchQuery): AsyncIterable<Record<string, unknown>>;
264
+ }
265
+ /**
266
+ * Harper Scope passed to handleApplication for sub-component plugins.
267
+ */
268
+ export interface Scope {
269
+ logger: Logger;
270
+ resources: {
271
+ set(name: string, resource: unknown): void;
272
+ };
273
+ server: {
274
+ http?: (handler: (request: HarperRequest, next: (req: HarperRequest) => Promise<unknown>) => Promise<unknown>, options?: {
275
+ runFirst?: boolean;
276
+ }) => void;
277
+ };
278
+ options: {
279
+ get(keys: string[]): unknown;
280
+ getAll(): Record<string, unknown>;
281
+ on(event: string, handler: (...args: unknown[]) => void): void;
282
+ };
283
+ on(event: string, handler: (...args: unknown[]) => void): void;
284
+ }
285
+ /**
286
+ * Harper HTTP request object.
287
+ */
288
+ export interface HarperRequest {
289
+ method?: string;
290
+ pathname?: string;
291
+ url?: string;
292
+ headers?: Record<string, string | string[] | undefined>;
293
+ body?: unknown;
294
+ session?: Record<string, unknown>;
295
+ [key: string]: unknown;
296
+ }
297
+ /**
298
+ * Augment the global scope with Harper's runtime globals.
299
+ * These are available at runtime but not at compile time.
300
+ */
301
+ declare global {
302
+ const databases: {
303
+ kb: {
304
+ KnowledgeEntry: Table;
305
+ TriageItem: Table;
306
+ KnowledgeTag: Table;
307
+ QueryLog: Table;
308
+ ServiceKey: Table;
309
+ OAuthClient: Table;
310
+ OAuthCode: Table;
311
+ OAuthRefreshToken: Table;
312
+ OAuthSigningKey: Table;
313
+ KnowledgeEntryEdit: Table;
314
+ };
315
+ };
316
+ const logger: Logger;
317
+ }
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * harper-knowledge — Type Definitions
3
+ *
4
+ * TypeScript interfaces for all table records, search parameters,
5
+ * and Harper global type declarations.
6
+ */
7
+ export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Datadog Webhook Handler
3
+ *
4
+ * Validates Datadog webhook requests via API key and parses
5
+ * alert/monitor and incident payloads.
6
+ */
7
+ import type { WebhookResult } from "./types.ts";
8
+ /**
9
+ * Validate a Datadog webhook request by comparing the provided API key.
10
+ *
11
+ * Checks DD-API-KEY or X-API-Key headers against the configured key.
12
+ *
13
+ * @param headerValue - The API key from the request header
14
+ * @param configuredKey - The expected API key from config
15
+ * @returns true if the key is valid
16
+ */
17
+ export declare function validateApiKey(headerValue: string, configuredKey: string): boolean;
18
+ /**
19
+ * Parse a Datadog webhook payload and extract a triage-ready result.
20
+ *
21
+ * Detects payload type (alert/monitor vs incident) and formats accordingly.
22
+ *
23
+ * @param payload - The parsed JSON payload
24
+ * @returns A WebhookResult for triage
25
+ */
26
+ export declare function parsePayload(payload: Record<string, any>): WebhookResult;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Datadog Webhook Handler
3
+ *
4
+ * Validates Datadog webhook requests via API key and parses
5
+ * alert/monitor and incident payloads.
6
+ */
7
+ import crypto from "node:crypto";
8
+ /** Maximum body/text length included in the summary */
9
+ const MAX_BODY_LENGTH = 500;
10
+ /**
11
+ * Validate a Datadog webhook request by comparing the provided API key.
12
+ *
13
+ * Checks DD-API-KEY or X-API-Key headers against the configured key.
14
+ *
15
+ * @param headerValue - The API key from the request header
16
+ * @param configuredKey - The expected API key from config
17
+ * @returns true if the key is valid
18
+ */
19
+ export function validateApiKey(headerValue, configuredKey) {
20
+ if (!headerValue || !configuredKey)
21
+ return false;
22
+ // Constant-time comparison to prevent timing attacks
23
+ if (headerValue.length !== configuredKey.length)
24
+ return false;
25
+ return crypto.timingSafeEqual(Buffer.from(headerValue), Buffer.from(configuredKey));
26
+ }
27
+ /**
28
+ * Parse a Datadog webhook payload and extract a triage-ready result.
29
+ *
30
+ * Detects payload type (alert/monitor vs incident) and formats accordingly.
31
+ *
32
+ * @param payload - The parsed JSON payload
33
+ * @returns A WebhookResult for triage
34
+ */
35
+ export function parsePayload(payload) {
36
+ // Alert/monitor payload: has alert_type or event_type === 'alert'
37
+ if (payload.alert_type || payload.event_type === "alert") {
38
+ return parseAlertPayload(payload);
39
+ }
40
+ // Incident payload: has severity or status field with incident-like structure
41
+ if (payload.severity !== undefined ||
42
+ (payload.status && payload.incident_id)) {
43
+ return parseIncidentPayload(payload);
44
+ }
45
+ // Fallback: unknown format, still triage it
46
+ return parseFallbackPayload(payload);
47
+ }
48
+ function parseAlertPayload(payload) {
49
+ const id = payload.alert_id || payload.id || payload.event_id || crypto.randomUUID();
50
+ const title = payload.title ||
51
+ payload.alert_title ||
52
+ payload.msg_title ||
53
+ "Untitled Alert";
54
+ const priority = payload.priority ? `P${payload.priority}` : "";
55
+ const alertType = payload.alert_type || "alert";
56
+ const text = truncate(payload.body || payload.text || payload.msg_text || "");
57
+ const tags = formatTags(payload.tags);
58
+ const parts = [
59
+ `[Datadog Alert] ${title}${priority ? ` (${priority}, ${alertType})` : ` (${alertType})`}`,
60
+ ];
61
+ if (tags)
62
+ parts.push(`Tags: ${tags}`);
63
+ parts.push("---");
64
+ parts.push(text);
65
+ return {
66
+ source: "datadog-webhook",
67
+ sourceId: `datadog:alert:${id}`,
68
+ summary: parts.join("\n"),
69
+ rawPayload: payload,
70
+ };
71
+ }
72
+ function parseIncidentPayload(payload) {
73
+ const id = payload.incident_id || payload.id || crypto.randomUUID();
74
+ const title = payload.title || payload.incident_title || "Untitled Incident";
75
+ const severity = payload.severity || "unknown";
76
+ const status = payload.status || "unknown";
77
+ const text = truncate(payload.body || payload.text || payload.description || "");
78
+ const tags = formatTags(payload.tags);
79
+ const parts = [
80
+ `[Datadog Incident] ${title} (severity: ${severity}, status: ${status})`,
81
+ ];
82
+ if (tags)
83
+ parts.push(`Tags: ${tags}`);
84
+ parts.push("---");
85
+ parts.push(text);
86
+ return {
87
+ source: "datadog-webhook",
88
+ sourceId: `datadog:incident:${id}`,
89
+ summary: parts.join("\n"),
90
+ rawPayload: payload,
91
+ };
92
+ }
93
+ function parseFallbackPayload(payload) {
94
+ const id = payload.id || crypto.randomUUID();
95
+ const title = payload.title || payload.subject || "Datadog Event";
96
+ const text = truncate(payload.body ||
97
+ payload.text ||
98
+ payload.message ||
99
+ JSON.stringify(payload).slice(0, MAX_BODY_LENGTH));
100
+ return {
101
+ source: "datadog-webhook",
102
+ sourceId: `datadog:event:${id}`,
103
+ summary: `[Datadog Event] ${title}\n---\n${text}`,
104
+ rawPayload: payload,
105
+ };
106
+ }
107
+ function formatTags(tags) {
108
+ if (!tags)
109
+ return "";
110
+ if (Array.isArray(tags))
111
+ return tags.join(", ");
112
+ if (typeof tags === "string")
113
+ return tags;
114
+ return "";
115
+ }
116
+ function truncate(text) {
117
+ if (text.length <= MAX_BODY_LENGTH)
118
+ return text;
119
+ return text.slice(0, MAX_BODY_LENGTH) + "...";
120
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * GitHub Webhook Handler
3
+ *
4
+ * Validates GitHub webhook signatures (HMAC-SHA256) and parses payloads
5
+ * for issues, issue comments, discussions, and discussion comments.
6
+ */
7
+ import type { WebhookResult } from "./types.ts";
8
+ /**
9
+ * Validate a GitHub webhook signature using HMAC-SHA256.
10
+ *
11
+ * @param rawBody - The raw request body string
12
+ * @param signature - The X-Hub-Signature-256 header value
13
+ * @param secret - The configured webhook secret
14
+ * @returns true if the signature is valid
15
+ */
16
+ export declare function validateSignature(rawBody: string, signature: string, secret: string): boolean;
17
+ /**
18
+ * Parse a GitHub webhook payload and extract a triage-ready result.
19
+ *
20
+ * @param event - The X-GitHub-Event header value
21
+ * @param payload - The parsed JSON payload
22
+ * @returns A WebhookResult for triage, or null if the event should be ignored
23
+ */
24
+ export declare function parsePayload(event: string, payload: Record<string, any>): WebhookResult | null;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * GitHub Webhook Handler
3
+ *
4
+ * Validates GitHub webhook signatures (HMAC-SHA256) and parses payloads
5
+ * for issues, issue comments, discussions, and discussion comments.
6
+ */
7
+ import crypto from "node:crypto";
8
+ /** Maximum body length included in the summary */
9
+ const MAX_BODY_LENGTH = 500;
10
+ /**
11
+ * Validate a GitHub webhook signature using HMAC-SHA256.
12
+ *
13
+ * @param rawBody - The raw request body string
14
+ * @param signature - The X-Hub-Signature-256 header value
15
+ * @param secret - The configured webhook secret
16
+ * @returns true if the signature is valid
17
+ */
18
+ export function validateSignature(rawBody, signature, secret) {
19
+ if (!signature || !secret)
20
+ return false;
21
+ const expected = "sha256=" +
22
+ crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
23
+ // Constant-time comparison to prevent timing attacks
24
+ if (expected.length !== signature.length)
25
+ return false;
26
+ return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
27
+ }
28
+ /**
29
+ * Parse a GitHub webhook payload and extract a triage-ready result.
30
+ *
31
+ * @param event - The X-GitHub-Event header value
32
+ * @param payload - The parsed JSON payload
33
+ * @returns A WebhookResult for triage, or null if the event should be ignored
34
+ */
35
+ export function parsePayload(event, payload) {
36
+ switch (event) {
37
+ case "issues":
38
+ return parseIssueEvent(payload);
39
+ case "issue_comment":
40
+ return parseIssueCommentEvent(payload);
41
+ case "discussion":
42
+ return parseDiscussionEvent(payload);
43
+ case "discussion_comment":
44
+ return parseDiscussionCommentEvent(payload);
45
+ default:
46
+ return null;
47
+ }
48
+ }
49
+ function parseIssueEvent(payload) {
50
+ const action = payload.action;
51
+ const issue = payload.issue;
52
+ const repo = payload.repository?.full_name;
53
+ if (!issue || !repo)
54
+ return null;
55
+ // Only handle opened, closed, reopened
56
+ const directActions = ["opened", "closed", "reopened"];
57
+ if (directActions.includes(action)) {
58
+ return {
59
+ source: "github-webhook",
60
+ sourceId: `github:issues:${action}:${repo}#${issue.number}`,
61
+ summary: formatIssueSummary(repo, issue, action),
62
+ rawPayload: payload,
63
+ };
64
+ }
65
+ // Handle labeled — only if the label is "kb-candidate"
66
+ if (action === "labeled") {
67
+ const label = payload.label;
68
+ if (label?.name === "kb-candidate") {
69
+ return {
70
+ source: "github-webhook",
71
+ sourceId: `github:issues:labeled:${repo}#${issue.number}`,
72
+ summary: formatIssueSummary(repo, issue, "labeled"),
73
+ rawPayload: payload,
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+ return null;
79
+ }
80
+ function parseIssueCommentEvent(payload) {
81
+ const action = payload.action;
82
+ if (action !== "created")
83
+ return null;
84
+ const comment = payload.comment;
85
+ const issue = payload.issue;
86
+ const repo = payload.repository?.full_name;
87
+ if (!comment || !issue || !repo)
88
+ return null;
89
+ const body = truncate(comment.body || "");
90
+ const summary = [
91
+ `[GitHub Comment] ${repo}#${issue.number}: "${issue.title}" (comment by ${comment.user?.login || "unknown"})`,
92
+ "---",
93
+ body,
94
+ ].join("\n");
95
+ return {
96
+ source: "github-webhook",
97
+ sourceId: `github:issue_comment:${comment.id}`,
98
+ summary,
99
+ rawPayload: payload,
100
+ };
101
+ }
102
+ function parseDiscussionEvent(payload) {
103
+ const action = payload.action;
104
+ if (action !== "created" && action !== "answered")
105
+ return null;
106
+ const discussion = payload.discussion;
107
+ const repo = payload.repository?.full_name;
108
+ if (!discussion || !repo)
109
+ return null;
110
+ const body = truncate(discussion.body || "");
111
+ const category = discussion.category?.name;
112
+ const summary = [
113
+ `[GitHub Discussion] ${repo}#${discussion.number}: "${discussion.title}" (${action} by ${discussion.user?.login || "unknown"})`,
114
+ category ? `Category: ${category}` : "",
115
+ "---",
116
+ body,
117
+ ]
118
+ .filter(Boolean)
119
+ .join("\n");
120
+ return {
121
+ source: "github-webhook",
122
+ sourceId: `github:discussion:${action}:${repo}#${discussion.number}`,
123
+ summary,
124
+ rawPayload: payload,
125
+ };
126
+ }
127
+ function parseDiscussionCommentEvent(payload) {
128
+ const action = payload.action;
129
+ if (action !== "created")
130
+ return null;
131
+ const comment = payload.comment;
132
+ const discussion = payload.discussion;
133
+ const repo = payload.repository?.full_name;
134
+ if (!comment || !discussion || !repo)
135
+ return null;
136
+ const body = truncate(comment.body || "");
137
+ const summary = [
138
+ `[GitHub Discussion Comment] ${repo}#${discussion.number}: "${discussion.title}" (comment by ${comment.user?.login || "unknown"})`,
139
+ "---",
140
+ body,
141
+ ].join("\n");
142
+ return {
143
+ source: "github-webhook",
144
+ sourceId: `github:discussion_comment:${comment.id}`,
145
+ summary,
146
+ rawPayload: payload,
147
+ };
148
+ }
149
+ function formatIssueSummary(repo, issue, action) {
150
+ const labels = (issue.labels || [])
151
+ .map((l) => l.name)
152
+ .join(", ");
153
+ const body = truncate(issue.body || "");
154
+ const parts = [
155
+ `[GitHub Issue] ${repo}#${issue.number}: "${issue.title}" (${action} by ${issue.user?.login || "unknown"})`,
156
+ ];
157
+ if (labels)
158
+ parts.push(`Labels: ${labels}`);
159
+ parts.push("---");
160
+ parts.push(body);
161
+ return parts.join("\n");
162
+ }
163
+ function truncate(text) {
164
+ if (text.length <= MAX_BODY_LENGTH)
165
+ return text;
166
+ return text.slice(0, MAX_BODY_LENGTH) + "...";
167
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Webhook Middleware
3
+ *
4
+ * HTTP middleware for Harper's scope.server.http() that routes
5
+ * /webhooks/* requests to the appropriate webhook handler.
6
+ */
7
+ import type { Scope, HarperRequest } from "../types.ts";
8
+ /**
9
+ * Create a webhook middleware function for Harper's scope.server.http().
10
+ *
11
+ * Reads webhook config from scope.options and watches for changes
12
+ * to support secret rotation without restart.
13
+ */
14
+ export declare function createWebhookMiddleware(scope: Scope): (request: HarperRequest, next: (req: HarperRequest) => Promise<unknown>) => Promise<unknown>;