hippo-memory 1.2.0 → 1.3.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 (108) hide show
  1. package/README.md +15 -0
  2. package/dist/api.d.ts +1 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +30 -10
  5. package/dist/api.js.map +1 -1
  6. package/dist/cli.js +15 -3
  7. package/dist/cli.js.map +1 -1
  8. package/dist/connectors/github/backfill.d.ts +48 -0
  9. package/dist/connectors/github/backfill.d.ts.map +1 -0
  10. package/dist/connectors/github/backfill.js +257 -0
  11. package/dist/connectors/github/backfill.js.map +1 -0
  12. package/dist/connectors/github/cli-impl.d.ts +24 -0
  13. package/dist/connectors/github/cli-impl.d.ts.map +1 -0
  14. package/dist/connectors/github/cli-impl.js +152 -0
  15. package/dist/connectors/github/cli-impl.js.map +1 -0
  16. package/dist/connectors/github/deletion.d.ts +38 -0
  17. package/dist/connectors/github/deletion.d.ts.map +1 -0
  18. package/dist/connectors/github/deletion.js +78 -0
  19. package/dist/connectors/github/deletion.js.map +1 -0
  20. package/dist/connectors/github/dlq.d.ts +101 -0
  21. package/dist/connectors/github/dlq.d.ts.map +1 -0
  22. package/dist/connectors/github/dlq.js +181 -0
  23. package/dist/connectors/github/dlq.js.map +1 -0
  24. package/dist/connectors/github/idempotency.d.ts +19 -0
  25. package/dist/connectors/github/idempotency.d.ts.map +1 -0
  26. package/dist/connectors/github/idempotency.js +25 -0
  27. package/dist/connectors/github/idempotency.js.map +1 -0
  28. package/dist/connectors/github/ingest.d.ts +67 -0
  29. package/dist/connectors/github/ingest.d.ts.map +1 -0
  30. package/dist/connectors/github/ingest.js +107 -0
  31. package/dist/connectors/github/ingest.js.map +1 -0
  32. package/dist/connectors/github/octokit-client.d.ts +36 -0
  33. package/dist/connectors/github/octokit-client.d.ts.map +1 -0
  34. package/dist/connectors/github/octokit-client.js +65 -0
  35. package/dist/connectors/github/octokit-client.js.map +1 -0
  36. package/dist/connectors/github/ratelimit.d.ts +20 -0
  37. package/dist/connectors/github/ratelimit.d.ts.map +1 -0
  38. package/dist/connectors/github/ratelimit.js +31 -0
  39. package/dist/connectors/github/ratelimit.js.map +1 -0
  40. package/dist/connectors/github/scope.d.ts +8 -0
  41. package/dist/connectors/github/scope.d.ts.map +1 -0
  42. package/dist/connectors/github/scope.js +13 -0
  43. package/dist/connectors/github/scope.js.map +1 -0
  44. package/dist/connectors/github/signature.d.ts +24 -0
  45. package/dist/connectors/github/signature.d.ts.map +1 -0
  46. package/dist/connectors/github/signature.js +35 -0
  47. package/dist/connectors/github/signature.js.map +1 -0
  48. package/dist/connectors/github/tenant-routing.d.ts +33 -0
  49. package/dist/connectors/github/tenant-routing.d.ts.map +1 -0
  50. package/dist/connectors/github/tenant-routing.js +61 -0
  51. package/dist/connectors/github/tenant-routing.js.map +1 -0
  52. package/dist/connectors/github/transform.d.ts +7 -0
  53. package/dist/connectors/github/transform.d.ts.map +1 -0
  54. package/dist/connectors/github/transform.js +103 -0
  55. package/dist/connectors/github/transform.js.map +1 -0
  56. package/dist/connectors/github/types.d.ts +87 -0
  57. package/dist/connectors/github/types.d.ts.map +1 -0
  58. package/dist/connectors/github/types.js +94 -0
  59. package/dist/connectors/github/types.js.map +1 -0
  60. package/dist/db.d.ts.map +1 -1
  61. package/dist/db.js +72 -1
  62. package/dist/db.js.map +1 -1
  63. package/dist/mcp/server.d.ts.map +1 -1
  64. package/dist/mcp/server.js +6 -4
  65. package/dist/mcp/server.js.map +1 -1
  66. package/dist/server.d.ts.map +1 -1
  67. package/dist/server.js +310 -1
  68. package/dist/server.js.map +1 -1
  69. package/dist/src/api.js +30 -10
  70. package/dist/src/api.js.map +1 -1
  71. package/dist/src/cli.js +15 -3
  72. package/dist/src/cli.js.map +1 -1
  73. package/dist/src/connectors/github/backfill.js +257 -0
  74. package/dist/src/connectors/github/backfill.js.map +1 -0
  75. package/dist/src/connectors/github/cli-impl.js +152 -0
  76. package/dist/src/connectors/github/cli-impl.js.map +1 -0
  77. package/dist/src/connectors/github/deletion.js +78 -0
  78. package/dist/src/connectors/github/deletion.js.map +1 -0
  79. package/dist/src/connectors/github/dlq.js +181 -0
  80. package/dist/src/connectors/github/dlq.js.map +1 -0
  81. package/dist/src/connectors/github/idempotency.js +25 -0
  82. package/dist/src/connectors/github/idempotency.js.map +1 -0
  83. package/dist/src/connectors/github/ingest.js +107 -0
  84. package/dist/src/connectors/github/ingest.js.map +1 -0
  85. package/dist/src/connectors/github/octokit-client.js +65 -0
  86. package/dist/src/connectors/github/octokit-client.js.map +1 -0
  87. package/dist/src/connectors/github/ratelimit.js +31 -0
  88. package/dist/src/connectors/github/ratelimit.js.map +1 -0
  89. package/dist/src/connectors/github/scope.js +13 -0
  90. package/dist/src/connectors/github/scope.js.map +1 -0
  91. package/dist/src/connectors/github/signature.js +35 -0
  92. package/dist/src/connectors/github/signature.js.map +1 -0
  93. package/dist/src/connectors/github/tenant-routing.js +61 -0
  94. package/dist/src/connectors/github/tenant-routing.js.map +1 -0
  95. package/dist/src/connectors/github/transform.js +103 -0
  96. package/dist/src/connectors/github/transform.js.map +1 -0
  97. package/dist/src/connectors/github/types.js +94 -0
  98. package/dist/src/connectors/github/types.js.map +1 -0
  99. package/dist/src/db.js +72 -1
  100. package/dist/src/db.js.map +1 -1
  101. package/dist/src/mcp/server.js +6 -4
  102. package/dist/src/mcp/server.js.map +1 -1
  103. package/dist/src/server.js +310 -1
  104. package/dist/src/server.js.map +1 -1
  105. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  106. package/extensions/openclaw-plugin/package.json +1 -1
  107. package/openclaw.plugin.json +1 -1
  108. package/package.json +1 -1
@@ -0,0 +1,181 @@
1
+ import { openHippoDb, closeHippoDb } from '../../db.js';
2
+ import { verifyGitHubSignature } from './signature.js';
3
+ import { isGitHubWebhookEnvelope } from './types.js';
4
+ export function writeToDlq(db, opts) {
5
+ const result = db
6
+ .prepare(`INSERT INTO github_dlq
7
+ (tenant_id, raw_payload, error, event_name, delivery_id, signature,
8
+ installation_id, repo_full_name, received_at, bucket)
9
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
10
+ .run(opts.tenantId ?? '__unroutable__', opts.rawPayload, opts.error, opts.eventName ?? null, opts.deliveryId ?? null, opts.signature ?? null, opts.installationId ?? null, opts.repoFullName ?? null, new Date().toISOString(), opts.bucket ?? 'parse_error');
11
+ return Number(result.lastInsertRowid);
12
+ }
13
+ const SELECT_COLUMNS = `id, tenant_id, raw_payload, error, event_name, delivery_id,
14
+ signature, installation_id, repo_full_name, retry_count,
15
+ received_at, retried_at, bucket`;
16
+ export function listDlq(db, opts) {
17
+ const rows = db
18
+ .prepare(`SELECT ${SELECT_COLUMNS}
19
+ FROM github_dlq
20
+ WHERE tenant_id = ?
21
+ ORDER BY received_at ASC
22
+ LIMIT ?`)
23
+ .all(opts.tenantId, opts.limit ?? 100);
24
+ return rows.map(rowToItem);
25
+ }
26
+ export function getDlqEntry(db, id) {
27
+ const row = db
28
+ .prepare(`SELECT ${SELECT_COLUMNS}
29
+ FROM github_dlq
30
+ WHERE id = ?`)
31
+ .get(id);
32
+ if (!row)
33
+ return null;
34
+ return rowToItem(row);
35
+ }
36
+ function rowToItem(r) {
37
+ return {
38
+ id: Number(r.id),
39
+ tenantId: String(r.tenant_id),
40
+ rawPayload: String(r.raw_payload),
41
+ error: String(r.error),
42
+ eventName: r.event_name == null ? null : String(r.event_name),
43
+ deliveryId: r.delivery_id == null ? null : String(r.delivery_id),
44
+ signature: r.signature == null ? null : String(r.signature),
45
+ installationId: r.installation_id == null ? null : String(r.installation_id),
46
+ repoFullName: r.repo_full_name == null ? null : String(r.repo_full_name),
47
+ retryCount: Number(r.retry_count ?? 0),
48
+ receivedAt: String(r.received_at),
49
+ retriedAt: r.retried_at == null ? null : String(r.retried_at),
50
+ bucket: r.bucket == null ? 'parse_error' : String(r.bucket),
51
+ };
52
+ }
53
+ function bumpRetryCount(hippoRoot, id) {
54
+ const db = openHippoDb(hippoRoot);
55
+ try {
56
+ db.prepare(`UPDATE github_dlq
57
+ SET retry_count = retry_count + 1,
58
+ retried_at = ?
59
+ WHERE id = ?`).run(new Date().toISOString(), id);
60
+ }
61
+ finally {
62
+ closeHippoDb(db);
63
+ }
64
+ }
65
+ /**
66
+ * Replay a DLQ row through the normal ingest path. Behavior:
67
+ * 1. Fetch row by id. Not found → `not_found`.
68
+ * 2. If !force and webhookSecret provided, verify signature with the
69
+ * current secret. Fail → bump retry_count, `sig_fail`.
70
+ * Missing signature on the row → `sig_missing` (no bump; --force required).
71
+ * 3. JSON.parse the raw payload. Fail → bump, `parse_error`.
72
+ * 4. Type-guard the envelope. Fail → bump, `unhandled`.
73
+ * 5. If an `ingestHook` is supplied, call it and return its memoryId.
74
+ * If not (dry-run path), bump retry_count and return status `replayed`
75
+ * with memoryId=null. The webhook route wires the real hook in Task 14.
76
+ *
77
+ * Mirrors Slack's "always use current routing" policy: replays use the
78
+ * deployment state NOW, not at the time of original DLQing.
79
+ */
80
+ export async function replayDlqEntry(ctx, id, opts = {}) {
81
+ const db = openHippoDb(ctx.hippoRoot);
82
+ let row;
83
+ try {
84
+ row = getDlqEntry(db, id);
85
+ }
86
+ finally {
87
+ closeHippoDb(db);
88
+ }
89
+ if (!row) {
90
+ return {
91
+ ok: false,
92
+ status: 'not_found',
93
+ memoryId: null,
94
+ retryCount: 0,
95
+ reason: `dlq id ${id} not found`,
96
+ };
97
+ }
98
+ // Signature verification (current secret, not the one in effect when DLQed).
99
+ if (!opts.force && opts.webhookSecret) {
100
+ if (!row.signature) {
101
+ return {
102
+ ok: false,
103
+ status: 'sig_missing',
104
+ memoryId: null,
105
+ retryCount: row.retryCount,
106
+ reason: 'legacy row without signature; pass --force to replay',
107
+ };
108
+ }
109
+ const sigOk = verifyGitHubSignature({
110
+ rawBody: row.rawPayload,
111
+ signature: row.signature,
112
+ webhookSecret: opts.webhookSecret,
113
+ });
114
+ if (!sigOk) {
115
+ bumpRetryCount(ctx.hippoRoot, id);
116
+ return {
117
+ ok: false,
118
+ status: 'sig_fail',
119
+ memoryId: null,
120
+ retryCount: row.retryCount + 1,
121
+ reason: 'signature did not verify against current GITHUB_WEBHOOK_SECRET; pass --force to replay anyway',
122
+ };
123
+ }
124
+ }
125
+ // Parse + envelope guard.
126
+ let parsed;
127
+ try {
128
+ parsed = JSON.parse(row.rawPayload);
129
+ }
130
+ catch (e) {
131
+ bumpRetryCount(ctx.hippoRoot, id);
132
+ return {
133
+ ok: false,
134
+ status: 'parse_error',
135
+ memoryId: null,
136
+ retryCount: row.retryCount + 1,
137
+ reason: `still unparseable: ${e.message}`,
138
+ };
139
+ }
140
+ if (!isGitHubWebhookEnvelope(parsed)) {
141
+ bumpRetryCount(ctx.hippoRoot, id);
142
+ return {
143
+ ok: false,
144
+ status: 'unhandled',
145
+ memoryId: null,
146
+ retryCount: row.retryCount + 1,
147
+ reason: 'not a GitHub webhook envelope',
148
+ };
149
+ }
150
+ // Without an ingest hook this is a dry-run validation. Bump and report.
151
+ if (!opts.ingestHook) {
152
+ bumpRetryCount(ctx.hippoRoot, id);
153
+ return {
154
+ ok: true,
155
+ status: 'replayed',
156
+ memoryId: null,
157
+ retryCount: row.retryCount + 1,
158
+ reason: 'dry-run: no ingest hook supplied',
159
+ };
160
+ }
161
+ // Real replay path. The route's IngestHook is responsible for routing,
162
+ // idempotency, and writing the memory. The DLQ module only validates the
163
+ // surface and bumps the retry counter.
164
+ const eventName = row.eventName ?? '';
165
+ const deliveryId = row.deliveryId ?? '';
166
+ const idempotencyKey = (await import('./signature.js')).computeIdempotencyKey(eventName, row.rawPayload);
167
+ const { memoryId } = await opts.ingestHook(ctx, {
168
+ rawPayload: row.rawPayload,
169
+ eventName,
170
+ idempotencyKey,
171
+ deliveryId,
172
+ });
173
+ bumpRetryCount(ctx.hippoRoot, id);
174
+ return {
175
+ ok: true,
176
+ status: 'replayed',
177
+ memoryId,
178
+ retryCount: row.retryCount + 1,
179
+ };
180
+ }
181
+ //# sourceMappingURL=dlq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dlq.js","sourceRoot":"","sources":["../../../../src/connectors/github/dlq.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Thrown by ingest's afterWrite hook when a concurrent worker has already
3
+ * inserted github_event_log for the same idempotency_key. Roll back the
4
+ * SAVEPOINT so exactly one memory row exists per key.
5
+ */
6
+ export class DuplicateIdempotencyError extends Error {
7
+ idempotencyKey;
8
+ constructor(idempotencyKey) {
9
+ super(`duplicate github idempotency_key: ${idempotencyKey}`);
10
+ this.name = 'DuplicateIdempotencyError';
11
+ this.idempotencyKey = idempotencyKey;
12
+ }
13
+ }
14
+ export function hasSeenKey(db, idempotencyKey) {
15
+ const row = db.prepare(`SELECT 1 FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
16
+ return !!row;
17
+ }
18
+ export function markKeySeen(db, args) {
19
+ db.prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`).run(args.idempotencyKey, args.deliveryId, args.eventName, new Date().toISOString(), args.memoryId);
20
+ }
21
+ export function lookupMemoryByKey(db, idempotencyKey) {
22
+ const row = db.prepare(`SELECT memory_id FROM github_event_log WHERE idempotency_key = ?`).get(idempotencyKey);
23
+ return row?.memory_id ?? null;
24
+ }
25
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../../../../src/connectors/github/idempotency.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IACzC,cAAc,CAAS;IAChC,YAAY,cAAsB;QAChC,KAAK,CAAC,qCAAqC,cAAc,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;QACxC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;IACvC,CAAC;CACF;AAED,MAAM,UAAU,UAAU,CAAC,EAAoB,EAAE,cAAsB;IACrE,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,0DAA0D,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACvG,OAAO,CAAC,CAAC,GAAG,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,EAAoB,EACpB,IAAgG;IAEhG,EAAE,CAAC,OAAO,CACR,kIAAkI,CACnI,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;AACvG,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,EAAoB,EAAE,cAAsB;IAC5E,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,kEAAkE,CAAC,CAAC,GAAG,CAAC,cAAc,CAEhG,CAAC;IACd,OAAO,GAAG,EAAE,SAAS,IAAI,IAAI,CAAC;AAChC,CAAC"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * GitHub event ingest with afterWrite race-safe idempotency.
3
+ *
4
+ * Mirrors src/connectors/slack/ingest.ts. The dedupe key is sha256(eventName +
5
+ * ':' + rawBody) (codex P0 #3) — derived from the signed body, not from the
6
+ * unsigned X-GitHub-Delivery header, so a replay attacker cannot bypass
7
+ * idempotency by rotating the delivery UUID.
8
+ *
9
+ * Race semantics (codex P1 #6):
10
+ * - Fast path: hasSeenKey pre-check returns 'duplicate' for the common case.
11
+ * - Slow path: hasSeenKey passes (no row yet). Two workers may race into
12
+ * remember() concurrently. Inside the writeEntry SAVEPOINT, INSERT OR
13
+ * IGNORE on github_event_log either inserts (changes=1, commit) or
14
+ * collides (changes=0, throw DuplicateIdempotencyError -> SAVEPOINT
15
+ * rolls back this worker's memory row). Exactly one memory exists per
16
+ * idempotency_key.
17
+ *
18
+ * The Slack precedent's race test was insufficient — it tested the fast path,
19
+ * not the SAVEPOINT collision. The `__testInjectBeforeLog` hook below lets
20
+ * tests pre-populate github_event_log inside the SAVEPOINT to actually
21
+ * exercise the changes=0 -> rollback path.
22
+ */
23
+ import { remember } from '../../api.js';
24
+ import { openHippoDb, closeHippoDb } from '../../db.js';
25
+ import { hasSeenKey, lookupMemoryByKey, DuplicateIdempotencyError } from './idempotency.js';
26
+ import { computeIdempotencyKey } from './signature.js';
27
+ import { issueEventToRememberOpts, issueCommentEventToRememberOpts, pullRequestEventToRememberOpts, prReviewCommentEventToRememberOpts, } from './transform.js';
28
+ function transformEvent(event) {
29
+ switch (event.eventName) {
30
+ case 'issues':
31
+ return issueEventToRememberOpts(event.payload);
32
+ case 'issue_comment':
33
+ return issueCommentEventToRememberOpts(event.payload);
34
+ case 'pull_request':
35
+ return pullRequestEventToRememberOpts(event.payload);
36
+ case 'pull_request_review_comment':
37
+ return prReviewCommentEventToRememberOpts(event.payload);
38
+ }
39
+ }
40
+ export function ingestEvent(ctx, input) {
41
+ const idempotencyKey = computeIdempotencyKey(input.event.eventName, input.rawBody);
42
+ // Fast path: pre-check. Avoids running the transform / opening a write tx
43
+ // for the common already-seen case (GitHub auto-retries with the same body).
44
+ const db = openHippoDb(ctx.hippoRoot);
45
+ try {
46
+ if (hasSeenKey(db, idempotencyKey)) {
47
+ return { status: 'duplicate', memoryId: lookupMemoryByKey(db, idempotencyKey) };
48
+ }
49
+ }
50
+ finally {
51
+ closeHippoDb(db);
52
+ }
53
+ const opts = transformEvent(input.event);
54
+ if (!opts) {
55
+ // Empty body: no memory to write, but mark seen so a retry of the same
56
+ // empty event returns 'duplicate' (not 'skipped' again — that would
57
+ // re-run the transform on every retry).
58
+ const db2 = openHippoDb(ctx.hippoRoot);
59
+ try {
60
+ db2
61
+ .prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`)
62
+ .run(idempotencyKey, input.deliveryId, input.event.eventName, new Date().toISOString(), null);
63
+ }
64
+ finally {
65
+ closeHippoDb(db2);
66
+ }
67
+ return { status: 'skipped', memoryId: null };
68
+ }
69
+ // Atomic write via afterWrite. Inside writeEntry's SAVEPOINT:
70
+ // 1. memories row INSERT lands.
71
+ // 2. (test-only) __testInjectBeforeLog can race-inject a colliding key.
72
+ // 3. INSERT OR IGNORE on github_event_log; if a concurrent worker (or
73
+ // the test injection) beat us, changes=0 -> throw -> SAVEPOINT rolls
74
+ // back our memory row. Other worker's commit stands.
75
+ try {
76
+ const result = remember({ ...ctx, actor: ctx.actor || 'connector:github' }, {
77
+ ...opts,
78
+ afterWrite: (innerDb, memoryId) => {
79
+ if (input.__testInjectBeforeLog) {
80
+ input.__testInjectBeforeLog(innerDb, idempotencyKey);
81
+ }
82
+ const inserted = innerDb
83
+ .prepare(`INSERT OR IGNORE INTO github_event_log (idempotency_key, delivery_id, event_name, ingested_at, memory_id) VALUES (?, ?, ?, ?, ?)`)
84
+ .run(idempotencyKey, input.deliveryId, input.event.eventName, new Date().toISOString(), memoryId);
85
+ if (Number(inserted.changes ?? 0) === 0) {
86
+ throw new DuplicateIdempotencyError(idempotencyKey);
87
+ }
88
+ },
89
+ });
90
+ return { status: 'ingested', memoryId: result.id };
91
+ }
92
+ catch (e) {
93
+ if (e instanceof DuplicateIdempotencyError) {
94
+ // Other worker's row is committed. Return its memory_id so callers
95
+ // behave identically to the fast-path 'duplicate' branch.
96
+ const db3 = openHippoDb(ctx.hippoRoot);
97
+ try {
98
+ return { status: 'skipped_duplicate', memoryId: lookupMemoryByKey(db3, idempotencyKey) };
99
+ }
100
+ finally {
101
+ closeHippoDb(db3);
102
+ }
103
+ }
104
+ throw e;
105
+ }
106
+ }
107
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../../../src/connectors/github/ingest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Octokit-shaped HTTP fetcher for the GitHub connector backfill.
3
+ *
4
+ * Production uses `realGitHubFetcher` against `https://api.github.com`.
5
+ * Tests inject a fake `GitHubFetcher` so they never hit the network.
6
+ *
7
+ * Codex P1 #4 mandate: any non-200 response that is NOT a recognized
8
+ * rate-limit pause MUST throw `GitHubFetchError`. Silently turning
9
+ * 401/403/404/500 into empty pages produced empty backfills with no
10
+ * operator signal, so this code path is now load-bearing.
11
+ */
12
+ import { parseRateLimit } from './ratelimit.js';
13
+ export class GitHubFetchError extends Error {
14
+ status;
15
+ bodyExcerpt;
16
+ url;
17
+ constructor(status, bodyExcerpt, url) {
18
+ super(`GitHub ${status} on ${url}: ${bodyExcerpt}`);
19
+ this.status = status;
20
+ this.bodyExcerpt = bodyExcerpt;
21
+ this.url = url;
22
+ this.name = 'GitHubFetchError';
23
+ }
24
+ }
25
+ /**
26
+ * Parse the rel="next" URL from an RFC 5988 `Link` header.
27
+ *
28
+ * Header format: `<url1>; rel="next", <url2>; rel="last"`.
29
+ * Returns the URL whose rel parameter is exactly `"next"`, or null.
30
+ */
31
+ export function parseNextLink(linkHeader) {
32
+ if (!linkHeader)
33
+ return null;
34
+ const re = /<([^>]+)>\s*;\s*rel="next"/;
35
+ const m = linkHeader.match(re);
36
+ return m ? m[1] : null;
37
+ }
38
+ function headersToRecord(h) {
39
+ const out = {};
40
+ h.forEach((v, k) => {
41
+ out[k.toLowerCase()] = v;
42
+ });
43
+ return out;
44
+ }
45
+ export const realGitHubFetcher = async ({ url, token }) => {
46
+ const res = await fetch(url, {
47
+ headers: {
48
+ Authorization: `Bearer ${token}`,
49
+ Accept: 'application/vnd.github+json',
50
+ 'X-GitHub-Api-Version': '2022-11-28',
51
+ },
52
+ });
53
+ const headers = headersToRecord(res.headers);
54
+ const rateLimit = parseRateLimit(headers, res.status);
55
+ // Codex P1 #4: don't silently turn 401/403/404/500 into empty pages.
56
+ if (res.status !== 200 && rateLimit.reason === 'none') {
57
+ const body = await res.text().catch(() => '');
58
+ throw new GitHubFetchError(res.status, body.slice(0, 256), url);
59
+ }
60
+ const items = res.status === 200 ? (await res.json()) : [];
61
+ const link = res.headers.get('link') ?? '';
62
+ const next = parseNextLink(link);
63
+ return { items, next, rateLimit };
64
+ };
65
+ //# sourceMappingURL=octokit-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"octokit-client.js","sourceRoot":"","sources":["../../../../src/connectors/github/octokit-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,cAAc,EAAsB,MAAM,gBAAgB,CAAC;AAEpE,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAE9B;IACA;IACA;IAHX,YACW,MAAc,EACd,WAAmB,EACnB,GAAW;QAEpB,KAAK,CAAC,UAAU,MAAM,OAAO,GAAG,KAAK,WAAW,EAAE,CAAC,CAAC;QAJ3C,WAAM,GAAN,MAAM,CAAQ;QACd,gBAAW,GAAX,WAAW,CAAQ;QACnB,QAAG,GAAH,GAAG,CAAQ;QAGpB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAaD;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC9C,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,EAAE,GAAG,4BAA4B,CAAC;IACxC,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC/B,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzB,CAAC;AAED,SAAS,eAAe,CAAC,CAAU;IACjC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACjB,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAkB,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE;IACvE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,MAAM,EAAE,6BAA6B;YACrC,sBAAsB,EAAE,YAAY;SACrC;KACF,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAEtD,qEAAqE;IACrE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9C,MAAM,IAAI,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,KAAK,GACT,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAE,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC3C,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AACpC,CAAC,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * GitHub rate-limit header parser.
3
+ *
4
+ * GitHub returns 403 for primary rate-limit (with `X-RateLimit-Remaining: 0`
5
+ * and `X-RateLimit-Reset: <epoch>`) and 429 + `Retry-After: <seconds>` for
6
+ * secondary rate-limit. Backfill must pause and resume rather than error.
7
+ */
8
+ /**
9
+ * Parse rate-limit signal from a GitHub HTTP response.
10
+ *
11
+ * @param headers Lower-cased HTTP response headers.
12
+ * @param status HTTP status code.
13
+ * @param now Optional current epoch seconds (for deterministic tests).
14
+ */
15
+ export function parseRateLimit(headers, status, now) {
16
+ const _now = now ?? Math.floor(Date.now() / 1000);
17
+ if (status === 429) {
18
+ const retry = Number(headers['retry-after'] ?? '60');
19
+ return {
20
+ sleepSeconds: Number.isFinite(retry) && retry >= 0 ? retry : 60,
21
+ reason: 'secondary',
22
+ };
23
+ }
24
+ if (status === 403 && Number(headers['x-ratelimit-remaining'] ?? '1') === 0) {
25
+ const reset = Number(headers['x-ratelimit-reset'] ?? '0');
26
+ const diff = reset - _now;
27
+ return { sleepSeconds: Math.max(diff, 1), reason: 'primary' };
28
+ }
29
+ return { sleepSeconds: 0, reason: 'none' };
30
+ }
31
+ //# sourceMappingURL=ratelimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ratelimit.js","sourceRoot":"","sources":["../../../../src/connectors/github/ratelimit.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,OAA2C,EAC3C,MAAc,EACd,GAAY;IAEZ,MAAM,IAAI,GAAG,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAElD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACnB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,IAAI,IAAI,CAAC,CAAC;QACrD,OAAO;YACL,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;YAC/D,MAAM,EAAE,WAAW;SACpB,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,uBAAuB,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5E,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,IAAI,GAAG,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC;QAC1B,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAChE,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Map a GitHub repository to a hippo scope string. Default-private when
3
+ * privacy is undetermined: cost of leaking public into private (recall returns
4
+ * nothing) << cost of leaking private into public (data exposure).
5
+ */
6
+ export function scopeFromRepository(repo) {
7
+ if (!repo)
8
+ return 'github:private:unknown';
9
+ if (repo.private === false)
10
+ return `github:public:${repo.full_name}`;
11
+ return `github:private:${repo.full_name}`;
12
+ }
13
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../../../src/connectors/github/scope.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAkC;IACpE,IAAI,CAAC,IAAI;QAAE,OAAO,wBAAwB,CAAC;IAC3C,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK;QAAE,OAAO,iBAAiB,IAAI,CAAC,SAAS,EAAE,CAAC;IACrE,OAAO,kBAAkB,IAAI,CAAC,SAAS,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { createHmac, createHash, timingSafeEqual } from 'crypto';
2
+ function verifyOne(rawBody, signature, secret) {
3
+ if (!signature.startsWith('sha256='))
4
+ return false;
5
+ const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
6
+ const a = Buffer.from(signature, 'utf8');
7
+ const b = Buffer.from(expected, 'utf8');
8
+ if (a.length !== b.length)
9
+ return false;
10
+ return timingSafeEqual(a, b);
11
+ }
12
+ export function verifyGitHubSignature(opts) {
13
+ if (verifyOne(opts.rawBody, opts.signature, opts.webhookSecret))
14
+ return true;
15
+ if (opts.previousSecret && verifyOne(opts.rawBody, opts.signature, opts.previousSecret))
16
+ return true;
17
+ return false;
18
+ }
19
+ /**
20
+ * Replay-safe idempotency key (codex P0 #3).
21
+ *
22
+ * GitHub does NOT sign X-GitHub-Delivery, so an attacker who captures one
23
+ * signed payload can replay the body with any new delivery UUID. Deriving
24
+ * idempotency from the delivery_id is unsafe.
25
+ *
26
+ * Key = sha256(eventName + ':' + rawBody). Both inputs are tamper-evident:
27
+ * - eventName comes from X-GitHub-Event, gated by upstream type guards.
28
+ * - rawBody is signed by the HMAC.
29
+ * A valid replay of (eventName, body) IS the same event. delivery_id is kept
30
+ * as audit metadata only, not as the dedupe seam.
31
+ */
32
+ export function computeIdempotencyKey(eventName, rawBody) {
33
+ return createHash('sha256').update(`${eventName}:${rawBody}`).digest('hex');
34
+ }
35
+ //# sourceMappingURL=signature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature.js","sourceRoot":"","sources":["../../../../src/connectors/github/signature.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAWjE,SAAS,SAAS,CAAC,OAAe,EAAE,SAAiB,EAAE,MAAc;IACnE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,QAAQ,GAAG,UAAU,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IACxF,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAgB;IACpD,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7E,IAAI,IAAI,CAAC,cAAc,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,cAAc,CAAC;QAAE,OAAO,IAAI,CAAC;IACrG,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,qBAAqB,CAAC,SAAiB,EAAE,OAAe;IACtE,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,IAAI,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Resolve the tenant_id for a GitHub webhook envelope.
3
+ *
4
+ * Returns:
5
+ * - mapped tenant_id when `github_installations` has a row for `installationId`
6
+ * (App-mode multi-tenant — primary path)
7
+ * - mapped tenant_id when `installation` is absent, `repository.full_name`
8
+ * matches a `github_repositories` row (PAT-mode multi-tenant)
9
+ * - the deployment's HIPPO_TENANT fallback (or 'default') when BOTH routing
10
+ * tables are empty (single-tenant deployment — env fallback is safe)
11
+ * - null when:
12
+ * - `installationId` is present but unknown AND `github_installations`
13
+ * is non-empty (multi-tenant install with foreign installation)
14
+ * - `installationId` is missing AND either routing table is non-empty
15
+ * AND no `repository.full_name` match (PAT-mode webhook from a foreign
16
+ * account — codex P0 #4 regression target)
17
+ *
18
+ * Escape hatch: `GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK=1` restores the
19
+ * env fallback for emergency rollback only. Mirrors the Slack equivalent
20
+ * (`SLACK_ALLOW_UNKNOWN_TEAM_FALLBACK`).
21
+ *
22
+ * The fail-closed contract lives here so every caller (route handler, CLI
23
+ * replay, future MCP) gets identical protection.
24
+ */
25
+ export function resolveTenantForGitHub(db, args) {
26
+ const envFallback = () => process.env.HIPPO_TENANT?.trim() || 'default';
27
+ const escapeHatch = process.env.GITHUB_ALLOW_UNKNOWN_INSTALLATION_FALLBACK === '1';
28
+ const instCount = db
29
+ .prepare(`SELECT COUNT(*) AS c FROM github_installations`)
30
+ .get().c;
31
+ const repoCount = db
32
+ .prepare(`SELECT COUNT(*) AS c FROM github_repositories`)
33
+ .get().c;
34
+ if (args.installationId) {
35
+ const row = db
36
+ .prepare(`SELECT tenant_id FROM github_installations WHERE installation_id = ?`)
37
+ .get(args.installationId);
38
+ if (row?.tenant_id)
39
+ return row.tenant_id;
40
+ if (Number(instCount) === 0) {
41
+ // Single-tenant install (table empty); env fallback is safe.
42
+ return envFallback();
43
+ }
44
+ // Multi-tenant install with unknown installation_id — fail closed.
45
+ return escapeHatch ? envFallback() : null;
46
+ }
47
+ // No installation.id (PAT-mode webhook).
48
+ if (Number(instCount) === 0 && Number(repoCount) === 0) {
49
+ // Single-tenant deployment with no routing tables populated.
50
+ return envFallback();
51
+ }
52
+ if (args.repoFullName) {
53
+ const row = db
54
+ .prepare(`SELECT tenant_id FROM github_repositories WHERE repo_full_name = ? ORDER BY added_at, tenant_id LIMIT 1`)
55
+ .get(args.repoFullName);
56
+ if (row?.tenant_id)
57
+ return row.tenant_id;
58
+ }
59
+ return escapeHatch ? envFallback() : null;
60
+ }
61
+ //# sourceMappingURL=tenant-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant-routing.js","sourceRoot":"","sources":["../../../../src/connectors/github/tenant-routing.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,sBAAsB,CACpC,EAAoB,EACpB,IAAiB;IAEjB,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;IAChF,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,0CAA0C,KAAK,GAAG,CAAC;IAEnF,MAAM,SAAS,GAAI,EAAE;SAClB,OAAO,CAAC,gDAAgD,CAAC;SACzD,GAAG,EAA6B,CAAC,CAAC,CAAC;IACtC,MAAM,SAAS,GAAI,EAAE;SAClB,OAAO,CAAC,+CAA+C,CAAC;SACxD,GAAG,EAA6B,CAAC,CAAC,CAAC;IAEtC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,EAAE;aACX,OAAO,CAAC,sEAAsE,CAAC;aAC/E,GAAG,CAAC,IAAI,CAAC,cAAc,CAAuC,CAAC;QAClE,IAAI,GAAG,EAAE,SAAS;YAAE,OAAO,GAAG,CAAC,SAAS,CAAC;QACzC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,6DAA6D;YAC7D,OAAO,WAAW,EAAE,CAAC;QACvB,CAAC;QACD,mEAAmE;QACnE,OAAO,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5C,CAAC;IAED,yCAAyC;IACzC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,6DAA6D;QAC7D,OAAO,WAAW,EAAE,CAAC;IACvB,CAAC;IACD,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,EAAE;aACX,OAAO,CACN,yGAAyG,CAC1G;aACA,GAAG,CAAC,IAAI,CAAC,YAAY,CAAuC,CAAC;QAChE,IAAI,GAAG,EAAE,SAAS;YAAE,OAAO,GAAG,CAAC,SAAS,CAAC;IAC3C,CAAC;IACD,OAAO,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5C,CAAC"}