@obsunified/collector 2.2.0 → 2.2.3
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.
- package/dist/auth.js +1 -1
- package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
- package/dist/chunk-2FALTDCE.js.map +1 -0
- package/dist/index.d.ts +61 -3
- package/dist/index.js +10933 -9769
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/__tests__/autosre-closed-loop.test.ts +1554 -17
- package/src/auth/dashboard-auth.test.ts +61 -0
- package/src/auth/dashboard-auth.ts +17 -2
- package/src/lib/ai-store/calls.ts +43 -23
- package/src/lib/ai-store.ts +12 -1
- package/src/lib/analyses-runner.ts +44 -7
- package/src/lib/autosre-change-correlation.ts +39 -12
- package/src/lib/autosre-findings.ts +237 -44
- package/src/lib/autosre-ontology-cache.test.ts +78 -0
- package/src/lib/autosre-ontology.ts +52 -28
- package/src/lib/autosre-remediation-gate.ts +942 -348
- package/src/lib/housekeeping.test.ts +70 -0
- package/src/lib/housekeeping.ts +91 -83
- package/src/lib/projects-store.test.ts +38 -1
- package/src/lib/projects-store.ts +23 -4
- package/src/lib/sql-db-postgres.integration.test.ts +75 -2
- package/src/lib/store/ingest.ts +8 -56
- package/src/lib/usage-store.ts +9 -3
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations/052_remediation_callback.sql +10 -0
- package/src/migrations/053_remediation_callback_delivery.sql +7 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations-postgres/052_remediation_callback.sql +10 -0
- package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
- package/src/plugins/ai-call-by-id.test.ts +73 -0
- package/src/plugins/ai-receiver.ts +10 -0
- package/src/plugins/alerts-evaluator.ts +15 -3
- package/src/plugins/autosre-routes.ts +374 -108
- package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
- package/src/plugins/connected-routes/manifest.test.ts +142 -0
- package/src/plugins/connected-routes/manifest.ts +123 -6
- package/src/plugins/connected-routes-links.test.ts +96 -0
- package/src/plugins/connected-routes.test.ts +3 -3
- package/src/plugins/connected-routes.ts +56 -83
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +397 -25
- package/dist/chunk-KMEZKGSO.js.map +0 -1
package/dist/auth.js
CHANGED
|
@@ -34,6 +34,9 @@ var rowToKey = (row) => ({
|
|
|
34
34
|
revokedAt: row.revoked_at
|
|
35
35
|
});
|
|
36
36
|
var SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]{0,48}[a-z0-9])?$/;
|
|
37
|
+
function displayPrefix(plaintext) {
|
|
38
|
+
return plaintext.slice(0, Math.min(16, Math.floor(plaintext.length / 3)));
|
|
39
|
+
}
|
|
37
40
|
var ProjectsStore = class {
|
|
38
41
|
constructor(db) {
|
|
39
42
|
this.db = db;
|
|
@@ -135,10 +138,15 @@ var ProjectsStore = class {
|
|
|
135
138
|
if (!plaintext) return;
|
|
136
139
|
await this.ensureDefaultProject();
|
|
137
140
|
const keyHash = await sha256Hex(plaintext);
|
|
138
|
-
const
|
|
139
|
-
|
|
141
|
+
const keyPrefix = displayPrefix(plaintext);
|
|
142
|
+
const existing = await this.db.prepare(`SELECT id, key_prefix FROM ingest_keys WHERE key_hash = ?`).bind(keyHash).first();
|
|
143
|
+
if (existing) {
|
|
144
|
+
if (existing.key_prefix.length > keyPrefix.length) {
|
|
145
|
+
await this.db.prepare(`UPDATE ingest_keys SET key_prefix = ? WHERE id = ?`).bind(keyPrefix, existing.id).run();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
140
149
|
const id = randomHex(16);
|
|
141
|
-
const keyPrefix = plaintext.slice(0, Math.min(16, plaintext.length));
|
|
142
150
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
143
151
|
await this.db.prepare(
|
|
144
152
|
`INSERT INTO ingest_keys (id, project_id, key_hash, key_prefix, name, created_at) VALUES (?, 'default', ?, ?, 'bootstrap', ?)`
|
|
@@ -328,4 +336,4 @@ export {
|
|
|
328
336
|
resetIngestAuthCache,
|
|
329
337
|
createIngestAuth
|
|
330
338
|
};
|
|
331
|
-
//# sourceMappingURL=chunk-
|
|
339
|
+
//# sourceMappingURL=chunk-2FALTDCE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/hash.ts","../src/lib/projects-store.ts","../src/lib/sql-db.ts","../src/auth/ingest-auth.ts"],"sourcesContent":["/**\n * Cross-runtime crypto helpers using Web Crypto API.\n * Works in Cloudflare Workers, Node 20+, Deno, Bun.\n */\n\n/** SHA-256 hash of a string, returned as lowercase hex. */\nexport async function sha256Hex(input: string): Promise<string> {\n\tconst encoder = new TextEncoder();\n\tconst digest = await crypto.subtle.digest(\"SHA-256\", encoder.encode(input));\n\treturn bytesToHex(new Uint8Array(digest));\n}\n\n/** Generate `bytes` cryptographically random bytes and return as lowercase hex. */\nexport function randomHex(bytes: number): string {\n\tconst buf = new Uint8Array(bytes);\n\tcrypto.getRandomValues(buf);\n\treturn bytesToHex(buf);\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n\tlet out = \"\";\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tout += bytes[i].toString(16).padStart(2, \"0\");\n\t}\n\treturn out;\n}\n","import type {\n\tIngestKey,\n\tIngestKeyRow,\n\tIngestKeyWithPlaintext,\n\tProject,\n\tProjectRow,\n} from \"@obsunified/types\";\nimport { randomHex, sha256Hex } from \"./hash\";\nimport type { SqlDb } from \"./sql-db\";\n\nconst rowToProject = (row: ProjectRow): Project => ({\n\tid: row.id,\n\tname: row.name,\n\tslug: row.slug,\n\tcreatedAt: row.created_at,\n\tpayloadCaptureEnabled:\n\t\trow.payload_capture_enabled === true ||\n\t\trow.payload_capture_enabled === 1 ||\n\t\trow.payload_capture_enabled === \"1\" ||\n\t\trow.payload_capture_enabled === \"true\",\n});\n\nconst rowToKey = (row: IngestKeyRow): IngestKey => ({\n\tid: row.id,\n\tprojectId: row.project_id,\n\tname: row.name,\n\tkeyPrefix: row.key_prefix,\n\tcreatedAt: row.created_at,\n\trevokedAt: row.revoked_at,\n});\n\nconst SLUG_PATTERN = /^[a-z0-9]([a-z0-9-]{0,48}[a-z0-9])?$/;\n\n/**\n * Display identifier for a secret: at most 16 chars (what minted keys show),\n * but never more than a third of the secret, so a short operator-chosen\n * INGEST_KEY can never be returned in full by `keys list`.\n */\nexport function displayPrefix(plaintext: string): string {\n\treturn plaintext.slice(0, Math.min(16, Math.floor(plaintext.length / 3)));\n}\n\nexport class ProjectsStore {\n\tconstructor(private readonly db: SqlDb) {}\n\n\tasync listProjects(): Promise<Project[]> {\n\t\tconst rs = await this.db\n\t\t\t.prepare(`SELECT * FROM projects ORDER BY created_at ASC`)\n\t\t\t.all<ProjectRow>();\n\t\treturn (rs.results ?? []).map(rowToProject);\n\t}\n\n\tasync getProject(id: string): Promise<Project | null> {\n\t\tconst row = await this.db\n\t\t\t.prepare(`SELECT * FROM projects WHERE id = ?`)\n\t\t\t.bind(id)\n\t\t\t.first<ProjectRow>();\n\t\treturn row ? rowToProject(row) : null;\n\t}\n\n\tasync createProject(input: { name: string; slug: string }): Promise<Project> {\n\t\tconst name = input.name.trim();\n\t\tconst slug = input.slug.trim().toLowerCase();\n\t\tif (!name) throw new Error(\"Project name is required\");\n\t\tif (!SLUG_PATTERN.test(slug))\n\t\t\tthrow new Error(\n\t\t\t\t\"Slug must be lowercase letters, digits, and hyphens (max 50 chars)\",\n\t\t\t);\n\n\t\tconst id = randomHex(16);\n\t\tconst createdAt = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT INTO projects (id, name, slug, created_at) VALUES (?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.bind(id, name, slug, createdAt)\n\t\t\t.run();\n\n\t\treturn { id, name, slug, createdAt, payloadCaptureEnabled: false };\n\t}\n\n\tasync updateProject(\n\t\tid: string,\n\t\tpatch: { name?: string; payloadCaptureEnabled?: boolean },\n\t): Promise<Project | null> {\n\t\tconst existing = await this.getProject(id);\n\t\tif (!existing) return null;\n\n\t\tconst name = patch.name?.trim();\n\t\tlet next = existing;\n\t\tif (name !== undefined && name !== existing.name) {\n\t\t\tawait this.db\n\t\t\t\t.prepare(`UPDATE projects SET name = ? WHERE id = ?`)\n\t\t\t\t.bind(name, id)\n\t\t\t\t.run();\n\t\t\tnext = { ...next, name };\n\t\t}\n\t\tif (\n\t\t\tpatch.payloadCaptureEnabled !== undefined &&\n\t\t\tpatch.payloadCaptureEnabled !== existing.payloadCaptureEnabled\n\t\t) {\n\t\t\tawait this.db\n\t\t\t\t.prepare(`UPDATE projects SET payload_capture_enabled = ? WHERE id = ?`)\n\t\t\t\t.bind(patch.payloadCaptureEnabled ? 1 : 0, id)\n\t\t\t\t.run();\n\t\t\tnext = {\n\t\t\t\t...next,\n\t\t\t\tpayloadCaptureEnabled: patch.payloadCaptureEnabled,\n\t\t\t};\n\t\t}\n\t\treturn next;\n\t}\n\n\tasync ensureDefaultProject(): Promise<void> {\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT OR IGNORE INTO projects (id, name, slug, created_at) VALUES ('default', 'Default', 'default', ?)`,\n\t\t\t)\n\t\t\t.bind(new Date().toISOString())\n\t\t\t.run();\n\t}\n\n\t// ── Ingest keys ──\n\n\tasync listKeys(projectId: string): Promise<IngestKey[]> {\n\t\tconst rs = await this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT * FROM ingest_keys WHERE project_id = ? ORDER BY created_at DESC`,\n\t\t\t)\n\t\t\t.bind(projectId)\n\t\t\t.all<IngestKeyRow>();\n\t\treturn (rs.results ?? []).map(rowToKey);\n\t}\n\n\tasync createKey(\n\t\tprojectId: string,\n\t\tname: string,\n\t): Promise<IngestKeyWithPlaintext> {\n\t\tconst project = await this.getProject(projectId);\n\t\tif (!project) throw new Error(\"Project not found\");\n\n\t\tconst trimmedName = name.trim() || \"unnamed\";\n\t\tconst id = randomHex(16);\n\t\tconst plaintext = `obs_${project.slug}_${randomHex(24)}`;\n\t\tconst keyHash = await sha256Hex(plaintext);\n\t\t// Prefix: show enough to identify but not recover (first 16 chars).\n\t\tconst keyPrefix = plaintext.slice(0, 16);\n\t\tconst createdAt = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT INTO ingest_keys (id, project_id, key_hash, key_prefix, name, created_at) VALUES (?, ?, ?, ?, ?, ?)`,\n\t\t\t)\n\t\t\t.bind(id, projectId, keyHash, keyPrefix, trimmedName, createdAt)\n\t\t\t.run();\n\n\t\treturn {\n\t\t\tid,\n\t\t\tprojectId,\n\t\t\tname: trimmedName,\n\t\t\tkeyPrefix,\n\t\t\tcreatedAt,\n\t\t\trevokedAt: null,\n\t\t\tkey: plaintext,\n\t\t\twarning: \"This key will not be shown again. Store it somewhere safe.\",\n\t\t};\n\t}\n\n\tasync revokeKey(keyId: string, projectId: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.prepare(\n\t\t\t\t`UPDATE ingest_keys SET revoked_at = ?\n\t\t\t\t WHERE id = ? AND project_id = ? AND revoked_at IS NULL`,\n\t\t\t)\n\t\t\t.bind(new Date().toISOString(), keyId, projectId)\n\t\t\t.run();\n\t\treturn (result.meta?.changes ?? 0) > 0;\n\t}\n\n\tasync findByKeyHash(\n\t\tkeyHash: string,\n\t): Promise<{ projectId: string; keyId: string } | null> {\n\t\tconst row = await this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT id, project_id FROM ingest_keys WHERE key_hash = ? AND revoked_at IS NULL`,\n\t\t\t)\n\t\t\t.bind(keyHash)\n\t\t\t.first<{ id: string; project_id: string }>();\n\t\treturn row ? { projectId: row.project_id, keyId: row.id } : null;\n\t}\n\n\t/**\n\t * Idempotently register the legacy env INGEST_KEY as a bootstrap key on the\n\t * default project, so existing deployments keep working without manual migration.\n\t */\n\tasync bootstrapEnvKey(plaintext: string): Promise<void> {\n\t\tif (!plaintext) return;\n\t\tawait this.ensureDefaultProject();\n\t\tconst keyHash = await sha256Hex(plaintext);\n\t\tconst keyPrefix = displayPrefix(plaintext);\n\t\tconst existing = await this.db\n\t\t\t.prepare(`SELECT id, key_prefix FROM ingest_keys WHERE key_hash = ?`)\n\t\t\t.bind(keyHash)\n\t\t\t.first<{ id: string; key_prefix: string }>();\n\t\tif (existing) {\n\t\t\t// Rows written before the prefix was length-capped may hold a short\n\t\t\t// bootstrap key in full; shrink them to the safe display prefix.\n\t\t\tif (existing.key_prefix.length > keyPrefix.length) {\n\t\t\t\tawait this.db\n\t\t\t\t\t.prepare(`UPDATE ingest_keys SET key_prefix = ? WHERE id = ?`)\n\t\t\t\t\t.bind(keyPrefix, existing.id)\n\t\t\t\t\t.run();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst id = randomHex(16);\n\t\tconst createdAt = new Date().toISOString();\n\t\tawait this.db\n\t\t\t.prepare(\n\t\t\t\t`INSERT INTO ingest_keys (id, project_id, key_hash, key_prefix, name, created_at) VALUES (?, 'default', ?, ?, 'bootstrap', ?)`,\n\t\t\t)\n\t\t\t.bind(id, keyHash, keyPrefix, createdAt)\n\t\t\t.run();\n\t}\n}\n","/**\n * Storage seam for the collector — RFC 0008.\n *\n * Stores and plugins target this interface instead of `D1Database` directly,\n * so a future Node / better-sqlite3 / ClickHouse runtime is one new adapter\n * away rather than a fork-and-rewrite. The shape is deliberately D1's\n * existing `prepare → bind → first|all|run` chain so the migration of\n * existing stores is mechanical (search-and-replace the constructor type).\n *\n * Today there is one implementation, `D1Adapter`. A `MemSqlDb` test double\n * lives next door under `test-utils/`; `BetterSqliteAdapter` and\n * `ClickHouseAdapter` are deferred until they have a real runtime to serve.\n */\n\nexport interface SqlDb {\n\treadonly dialect?: SqlDialect;\n\tprepare(sql: string): SqlStatement;\n\t/**\n\t * Run multiple prepared statements as a batch. On D1 this maps to a\n\t * single network round-trip; on a future better-sqlite3 adapter it\n\t * wraps the calls in a transaction. Returns one result per statement\n\t * in input order, mirroring D1's `batch`.\n\t *\n\t * Statements MUST have been produced by this same `SqlDb` instance —\n\t * mixing adapters is an error and runtime behavior is undefined.\n\t */\n\tbatch(\n\t\tstatements: SqlStatement[],\n\t): Promise<Array<{ meta: { changes: number } }>>;\n}\n\nexport type SqlDialectName = \"sqlite\" | \"postgres\";\n\nexport interface SqlDialect {\n\treadonly name: SqlDialectName;\n\tnow(): string;\n\tsinceHours(param: string): string;\n\tsinceMinutes(param: string): string;\n\tjsonText(expr: string, path: string): string;\n}\n\nexport interface SqlStatement {\n\tbind(...args: unknown[]): SqlStatement;\n\tfirst<T = Record<string, unknown>>(): Promise<T | null>;\n\tall<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n\trun(): Promise<{ meta: { changes: number } }>;\n}\n\n/**\n * D1 already conforms to this shape almost exactly; the adapter normalizes\n * two small differences:\n * - `all()` on D1 returns `results?: T[]` (optional). We coerce to `[]`.\n * - `run()`'s `meta.changes` is optional on D1. We coerce to `0`.\n * Both differences are runtime-undetectable for the queries this codebase\n * issues (every successful all() returns results; every run() against an\n * INSERT/UPDATE/DELETE reports changes), but coercing keeps the typed\n * interface honest.\n */\nexport class D1Adapter implements SqlDb {\n\treadonly dialect = sqliteDialect;\n\n\tconstructor(private readonly d1: D1Database) {}\n\n\tprepare(sql: string): SqlStatement {\n\t\treturn new D1StatementAdapter(this.d1.prepare(sql));\n\t}\n\n\tasync batch(\n\t\tstatements: SqlStatement[],\n\t): Promise<Array<{ meta: { changes: number } }>> {\n\t\t// Unwrap each statement back to its D1 form — D1's batch takes\n\t\t// D1PreparedStatement[], not our typed wrapper.\n\t\tconst unwrapped = statements.map((s) => {\n\t\t\tif (!(s instanceof D1StatementAdapter)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"D1Adapter.batch: statement was not produced by D1Adapter\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn s.unwrap();\n\t\t});\n\t\tconst results = await this.d1.batch(unwrapped);\n\t\treturn results.map((r) => ({\n\t\t\tmeta: { changes: r.meta?.changes ?? 0 },\n\t\t}));\n\t}\n}\n\n/**\n * Convenience wrapper for the common case: take a `CollectorEnv`-shaped\n * object and return an `SqlDb`. Most plugins don't have access to the\n * `CollectorRuntime` (they get it at register time, not handler time)\n * so they call this directly. Plugins that *do* hold the runtime should\n * prefer `runtime.getSqlDb(env)` so a host-supplied `sqlDb` factory in\n * `CollectorConfig` takes effect.\n */\nexport const sqlDbFor = (env: { DB: D1Database | SqlDb }): SqlDb =>\n\tisSqlDb(env.DB) ? env.DB : new D1Adapter(env.DB);\n\n/**\n * SQLite `strftime` format that reproduces `Date#toISOString()` output\n * (`YYYY-MM-DDTHH:MM:SS.mmmZ`). Every stored timestamp column holds that\n * shape, so time windows must be rendered the same way: `datetime('now')`\n * yields `YYYY-MM-DD HH:MM:SS` and in a text comparison the space sorts\n * before the `T`, which made \"last 5 minutes\" match the whole current UTC\n * day on D1 and made `expires_at < now` purges run a day late.\n */\nexport const SQLITE_ISO_FORMAT = \"%Y-%m-%dT%H:%M:%fZ\";\n\n/** Render SQLite `now` (optionally shifted by a modifier expression) as ISO text. */\nexport const sqliteIsoNow = (modifier?: string): string =>\n\tmodifier\n\t\t? `strftime('${SQLITE_ISO_FORMAT}', 'now', ${modifier})`\n\t\t: `strftime('${SQLITE_ISO_FORMAT}', 'now')`;\n\nexport const sqliteDialect: SqlDialect = {\n\tname: \"sqlite\",\n\tnow: () => sqliteIsoNow(),\n\tsinceHours: (param) => sqliteIsoNow(`'-' || ${param} || ' hours'`),\n\tsinceMinutes: (param) => sqliteIsoNow(`'-' || ${param} || ' minutes'`),\n\tjsonText: (expr, path) => `json_extract(${expr}, '${path}')`,\n};\n\n/**\n * The collector stores every timestamp column (`received_at`, `expires_at`,\n * `occurred_at`, …) as TEXT holding `Date#toISOString()` output on both\n * D1 and Postgres. Postgres will not compare TEXT against a\n * `timestamptz`, so time-window expressions have to be rendered as ISO\n * text in the same `YYYY-MM-DDTHH:MM:SS.mmmZ` shape the collector writes.\n */\nconst POSTGRES_ISO_TEXT_FORMAT = `'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'`;\nconst POSTGRES_UTC_NOW = \"(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')\";\n\n/** Render a Postgres `timestamp` expression as collector-style ISO text. */\nexport const postgresIsoText = (expr: string): string =>\n\t`to_char(${expr}, ${POSTGRES_ISO_TEXT_FORMAT})`;\n\nexport const postgresDialect: SqlDialect = {\n\tname: \"postgres\",\n\tnow: () => postgresIsoText(POSTGRES_UTC_NOW),\n\tsinceHours: (param) =>\n\t\tpostgresIsoText(\n\t\t\t`${POSTGRES_UTC_NOW} - (${param}::text || ' hours')::interval`,\n\t\t),\n\tsinceMinutes: (param) =>\n\t\tpostgresIsoText(\n\t\t\t`${POSTGRES_UTC_NOW} - (${param}::text || ' minutes')::interval`,\n\t\t),\n\tjsonText: (expr, path) => {\n\t\tconst key = path\n\t\t\t.replace(/^\\$\\./, \"\")\n\t\t\t.replace(/^\"|\"$/g, \"\")\n\t\t\t.replace(/\\\\u002E/g, \".\");\n\t\treturn `(${expr}::jsonb ->> '${key.replace(/'/g, \"''\")}')`;\n\t},\n};\n\nexport const dialectFor = (db: SqlDb): SqlDialect =>\n\tdb.dialect ?? sqliteDialect;\n\nconst isSqlDb = (db: unknown): db is SqlDb => {\n\tif (!db || typeof db !== \"object\") return false;\n\tconst candidate = db as {\n\t\tprepare?: unknown;\n\t\tbatch?: unknown;\n\t\texec?: unknown;\n\t};\n\treturn (\n\t\ttypeof candidate.prepare === \"function\" &&\n\t\ttypeof candidate.batch === \"function\" &&\n\t\ttypeof candidate.exec !== \"function\"\n\t);\n};\n\nclass D1StatementAdapter implements SqlStatement {\n\tconstructor(private readonly stmt: D1PreparedStatement) {}\n\n\t/** Internal escape hatch for `D1Adapter.batch` to recover the wrapped\n\t * D1 statement. Not part of the public `SqlStatement` shape. */\n\tunwrap(): D1PreparedStatement {\n\t\treturn this.stmt;\n\t}\n\n\tbind(...args: unknown[]): SqlStatement {\n\t\treturn new D1StatementAdapter(this.stmt.bind(...args));\n\t}\n\n\tfirst<T = Record<string, unknown>>(): Promise<T | null> {\n\t\treturn this.stmt.first<T>();\n\t}\n\n\tasync all<T = Record<string, unknown>>(): Promise<{ results: T[] }> {\n\t\tconst r = await this.stmt.all<T>();\n\t\treturn { results: r.results ?? [] };\n\t}\n\n\tasync run(): Promise<{ meta: { changes: number } }> {\n\t\tconst r = await this.stmt.run();\n\t\treturn { meta: { changes: r.meta?.changes ?? 0 } };\n\t}\n}\n","import type { MiddlewareHandler } from \"hono\";\nimport type { CollectorEnv } from \"../framework/env\";\nimport { sha256Hex } from \"../lib/hash\";\nimport { ProjectsStore } from \"../lib/projects-store\";\nimport { sqlDbFor } from \"../lib/sql-db\";\n\nconst CACHE_TTL_MS = 60_000; // 60 seconds\nconst CACHE_MAX_ENTRIES = 500;\n\ninterface CacheEntry {\n\tprojectId: string;\n\texpiresAt: number;\n}\n\nlet keyCaches = new WeakMap<object, Map<string, CacheEntry>>();\nlet bootstrapDone = false;\n\nfunction cacheFor(dbKey: object): Map<string, CacheEntry> {\n\tlet cache = keyCaches.get(dbKey);\n\tif (!cache) {\n\t\tcache = new Map();\n\t\tkeyCaches.set(dbKey, cache);\n\t}\n\treturn cache;\n}\n\nfunction getCached(\n\tcache: Map<string, CacheEntry>,\n\thash: string,\n\tnow: number,\n): string | null {\n\tpruneKeyCache(cache, now);\n\tconst entry = cache.get(hash);\n\tif (!entry) return null;\n\tif (entry.expiresAt <= now) {\n\t\tcache.delete(hash);\n\t\treturn null;\n\t}\n\treturn entry.projectId;\n}\n\nfunction setCached(\n\tcache: Map<string, CacheEntry>,\n\thash: string,\n\tprojectId: string,\n\tnow: number,\n): void {\n\tpruneKeyCache(cache, now);\n\twhile (cache.size >= CACHE_MAX_ENTRIES) {\n\t\tconst oldestKey = cache.keys().next().value;\n\t\tif (!oldestKey) break;\n\t\tcache.delete(oldestKey);\n\t}\n\tcache.set(hash, { projectId, expiresAt: now + CACHE_TTL_MS });\n}\n\nfunction pruneKeyCache(cache: Map<string, CacheEntry>, now: number): void {\n\tfor (const [hash, entry] of cache) {\n\t\tif (entry.expiresAt <= now) cache.delete(hash);\n\t}\n}\n\n/** Test helper — clears the in-memory cache and bootstrap flag. */\nexport function resetIngestAuthCache(): void {\n\tkeyCaches = new WeakMap();\n\tbootstrapDone = false;\n}\n\n/**\n * Creates middleware that validates ingest API keys against the ingest_keys table.\n *\n * Accepts the key from either:\n * - Authorization: Bearer <key>\n * - X-API-Key: <key>\n *\n * On success, sets `projectId` on the Hono context via `c.set('projectId', ...)`.\n *\n * On the first request per isolate, if `env.INGEST_KEY` is set, the middleware\n * registers it as a `bootstrap` key on the default project — so existing\n * single-tenant deployments continue to work without manual migration.\n */\nexport function createIngestAuth(config?: {\n\tallowUnauthenticated?: boolean;\n}): MiddlewareHandler<{\n\tBindings: CollectorEnv;\n\tVariables: { projectId: string };\n}> {\n\treturn async (c, next) => {\n\t\t// Lazy bootstrap of legacy env key (idempotent).\n\t\tif (!bootstrapDone) {\n\t\t\tconst legacy = c.env.INGEST_KEY || c.env.TELEMETRY_INGEST_TOKEN;\n\t\t\tif (legacy) {\n\t\t\t\tconst store = new ProjectsStore(sqlDbFor(c.env));\n\t\t\t\ttry {\n\t\t\t\t\tawait store.bootstrapEnvKey(legacy);\n\t\t\t\t\t// Latch only after success: a transient DB error on the first\n\t\t\t\t\t// request must not permanently disable legacy-key bootstrap for\n\t\t\t\t\t// the isolate. bootstrapEnvKey is idempotent, so concurrent\n\t\t\t\t\t// first-requests racing here are harmless.\n\t\t\t\t\tbootstrapDone = true;\n\t\t\t\t} catch (err) {\n\t\t\t\t\tconsole.error(\"[ingest-auth] bootstrap failed:\", err);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Nothing to bootstrap — don't keep re-checking on every request.\n\t\t\t\tbootstrapDone = true;\n\t\t\t}\n\t\t}\n\n\t\tconst bearer = c.req.header(\"Authorization\")?.replace(/^Bearer\\s+/i, \"\");\n\t\tconst apiKey = c.req.header(\"X-API-Key\");\n\t\tconst token = bearer || apiKey;\n\n\t\tif (!token) {\n\t\t\tif (\n\t\t\t\tconfig?.allowUnauthenticated ||\n\t\t\t\tc.env.ALLOW_UNAUTHENTICATED === \"true\"\n\t\t\t) {\n\t\t\t\tc.set(\"projectId\", \"default\");\n\t\t\t\treturn next();\n\t\t\t}\n\t\t\treturn c.json({ error: \"Unauthorized\" }, 401);\n\t\t}\n\n\t\tconst now = Date.now();\n\t\tconst hash = await sha256Hex(token);\n\t\tconst rawDb =\n\t\t\t(c.env.DB as unknown as Record<string, unknown>)?.__unwrapped_db ||\n\t\t\tc.env.DB;\n\t\tconst cache = cacheFor(rawDb);\n\t\tconst cached = getCached(cache, hash, now);\n\t\tif (cached) {\n\t\t\tc.set(\"projectId\", cached);\n\t\t\treturn next();\n\t\t}\n\n\t\tconst store = new ProjectsStore(sqlDbFor(c.env));\n\t\tconst match = await store.findByKeyHash(hash);\n\t\tif (!match) {\n\t\t\treturn c.json({ error: \"Unauthorized\" }, 401);\n\t\t}\n\n\t\tsetCached(cache, hash, match.projectId, now);\n\t\tc.set(\"projectId\", match.projectId);\n\t\treturn next();\n\t};\n}\n"],"mappings":"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","names":["store"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -154,6 +154,8 @@ interface HousekeepingSummary {
|
|
|
154
154
|
ontology_nodes?: number;
|
|
155
155
|
change_events?: number;
|
|
156
156
|
incident_investigations?: number;
|
|
157
|
+
/** `leased` incidents whose lease expired and were returned to `open` (RFC 0013 § 11). */
|
|
158
|
+
incident_leases_reaped?: number;
|
|
157
159
|
investigation_findings?: number;
|
|
158
160
|
remediation_proposals?: number;
|
|
159
161
|
}
|
|
@@ -1116,7 +1118,9 @@ declare function computeChangeBlastRadius(db: SqlDb, projectId: string, changeId
|
|
|
1116
1118
|
minComparableSample?: number;
|
|
1117
1119
|
}): Promise<ChangeBlastRadiusResult | null>;
|
|
1118
1120
|
|
|
1119
|
-
declare function recordInvestigationFinding(db: SqlDb, projectId: string, input: InvestigationFindingInput
|
|
1121
|
+
declare function recordInvestigationFinding(db: SqlDb, projectId: string, input: InvestigationFindingInput, options?: {
|
|
1122
|
+
logger?: Logger;
|
|
1123
|
+
}): Promise<InvestigationFinding>;
|
|
1120
1124
|
declare function getInvestigationFinding(db: SqlDb, projectId: string, findingId: string, environment?: string): Promise<InvestigationFinding | null>;
|
|
1121
1125
|
declare function searchInvestigationFindings(db: SqlDb, options: {
|
|
1122
1126
|
projectId: string;
|
|
@@ -1274,17 +1278,37 @@ declare function acquireIncidentLease(db: SqlDb, projectId: string, incidentId:
|
|
|
1274
1278
|
}>;
|
|
1275
1279
|
/**
|
|
1276
1280
|
* SSRF-safe, HMAC-SHA256 signed AutoSRE incident webhook outbox delivery
|
|
1277
|
-
* (RFC 0013 § 6.A).
|
|
1281
|
+
* (RFC 0013 § 6.A). Mirrors the alert webhook delivery: literal host check,
|
|
1282
|
+
* DNS classification (fail closed on private/unresolvable), no redirects and
|
|
1283
|
+
* a 5s timeout.
|
|
1284
|
+
*
|
|
1285
|
+
* Grant handoff (RFC 0013 § 6.B): when `webhook.grant` is supplied the payload
|
|
1286
|
+
* is an `autosre.remediation.grant_issued` event carrying `proposalId`,
|
|
1287
|
+
* `status` and the plaintext `grantToken`. The plaintext token exists only at
|
|
1288
|
+
* approve/propose time (the DB keeps its hash), so the caller that mints it
|
|
1289
|
+
* must hand it to this function in the same request. Grant deliveries do not
|
|
1290
|
+
* count against the incident's 5-attempt dispatch budget.
|
|
1278
1291
|
*/
|
|
1279
1292
|
declare function dispatchIncidentWebhook(db: SqlDb, projectId: string, incidentId: string, webhook: {
|
|
1280
1293
|
url: string;
|
|
1281
1294
|
secret?: string;
|
|
1295
|
+
environment?: string;
|
|
1282
1296
|
allowPrivateNetworks?: boolean;
|
|
1297
|
+
hostLookup?: HostLookup;
|
|
1283
1298
|
fetchImpl?: typeof fetch;
|
|
1299
|
+
logger?: Logger;
|
|
1300
|
+
grant?: {
|
|
1301
|
+
proposalId: string;
|
|
1302
|
+
status: string;
|
|
1303
|
+
grantToken: string;
|
|
1304
|
+
};
|
|
1305
|
+
/** Test hook: replaces the real backoff sleep between grant attempts. */
|
|
1306
|
+
sleep?: (ms: number) => Promise<void>;
|
|
1284
1307
|
}): Promise<{
|
|
1285
1308
|
delivered: boolean;
|
|
1286
1309
|
status?: number;
|
|
1287
1310
|
error?: string;
|
|
1311
|
+
attempts: number;
|
|
1288
1312
|
}>;
|
|
1289
1313
|
declare class RemediationGateError extends Error {
|
|
1290
1314
|
readonly statusCode: number;
|
|
@@ -1293,7 +1317,14 @@ declare class RemediationGateError extends Error {
|
|
|
1293
1317
|
/**
|
|
1294
1318
|
* Pre-execution remediation proposal gate (RFC 0013 § 6.B & P1 #4).
|
|
1295
1319
|
*/
|
|
1296
|
-
declare function proposeRemediationAction(db: SqlDb, projectId: string, input: RemediationProposalInput
|
|
1320
|
+
declare function proposeRemediationAction(db: SqlDb, projectId: string, input: RemediationProposalInput, options?: {
|
|
1321
|
+
logger?: Logger;
|
|
1322
|
+
/** Persisted on a pending proposal and used when it is approved later. */
|
|
1323
|
+
callback?: {
|
|
1324
|
+
url: string;
|
|
1325
|
+
secret?: string;
|
|
1326
|
+
};
|
|
1327
|
+
}): Promise<RemediationProposal>;
|
|
1297
1328
|
declare function getRemediationProposal(db: SqlDb, projectId: string, proposalId: string, environment?: string): Promise<RemediationProposal | null>;
|
|
1298
1329
|
declare function listRemediationProposals(db: SqlDb, options: {
|
|
1299
1330
|
projectId: string;
|
|
@@ -1306,6 +1337,10 @@ declare function listRemediationProposals(db: SqlDb, options: {
|
|
|
1306
1337
|
/**
|
|
1307
1338
|
* Human dashboard session approval for `pending_human_approval` proposals
|
|
1308
1339
|
* (RFC 0013 § 6.B & P1 #4). Mints a single-use `grantToken`.
|
|
1340
|
+
*
|
|
1341
|
+
* The status precondition lives in the UPDATE's WHERE clause (D1 and Postgres
|
|
1342
|
+
* have no multi-statement transactions beyond `batch`), so two concurrent
|
|
1343
|
+
* approvals cannot both mint a token: only one sees `meta.changes === 1`.
|
|
1309
1344
|
*/
|
|
1310
1345
|
declare function approveRemediationProposal(db: SqlDb, projectId: string, proposalId: string, options: {
|
|
1311
1346
|
environment?: string;
|
|
@@ -1319,19 +1354,31 @@ declare function rejectRemediationProposal(db: SqlDb, projectId: string, proposa
|
|
|
1319
1354
|
/**
|
|
1320
1355
|
* Single-use grant redemption with optimistic `expectedCurrentState` verification
|
|
1321
1356
|
* against `ontology_nodes` (RFC 0013 § 6.B & P1 #4).
|
|
1357
|
+
*
|
|
1358
|
+
* The status, token-hash and expiry preconditions are part of the claiming
|
|
1359
|
+
* UPDATE, and the outcome is read from `meta.changes`: of N concurrent claims
|
|
1360
|
+
* for one token exactly one sees `changes === 1`; the rest get a 409. The
|
|
1361
|
+
* incident transition rides in the same `batch` and is guarded by an EXISTS on
|
|
1362
|
+
* the winning claim, so a losing claim cannot touch the incident.
|
|
1322
1363
|
*/
|
|
1323
1364
|
declare function claimRemediationGrant(db: SqlDb, projectId: string, proposalId: string, options: {
|
|
1324
1365
|
environment?: string;
|
|
1325
1366
|
grantToken: string;
|
|
1326
1367
|
runnerId?: string;
|
|
1327
1368
|
nowIso?: string;
|
|
1369
|
+
logger?: Logger;
|
|
1328
1370
|
}): Promise<RemediationProposal>;
|
|
1329
1371
|
/**
|
|
1330
1372
|
* Records remediation completion, updates `ontology_nodes` to desired state,
|
|
1331
1373
|
* and arms the +2m/+5m/+15m post-remediation watchdog on the finding.
|
|
1374
|
+
*
|
|
1375
|
+
* Only the runner that holds the claim may complete, and only while the claim
|
|
1376
|
+
* window is open (`now <= claim_expires_at`); otherwise 409. All follow-up
|
|
1377
|
+
* writes are guarded on the completion UPDATE having applied.
|
|
1332
1378
|
*/
|
|
1333
1379
|
declare function completeRemediationProposal(db: SqlDb, projectId: string, proposalId: string, options: {
|
|
1334
1380
|
environment?: string;
|
|
1381
|
+
runnerId?: string;
|
|
1335
1382
|
resultingChangeId?: string | null;
|
|
1336
1383
|
remediationAgentRunId?: string | null;
|
|
1337
1384
|
baselineRequestRatePerMin?: number | null;
|
|
@@ -1339,6 +1386,7 @@ declare function completeRemediationProposal(db: SqlDb, projectId: string, propo
|
|
|
1339
1386
|
}): Promise<RemediationProposal>;
|
|
1340
1387
|
declare function failRemediationProposal(db: SqlDb, projectId: string, proposalId: string, options: {
|
|
1341
1388
|
environment?: string;
|
|
1389
|
+
runnerId?: string;
|
|
1342
1390
|
failureReason: string;
|
|
1343
1391
|
nowIso?: string;
|
|
1344
1392
|
}): Promise<RemediationProposal>;
|
|
@@ -1349,6 +1397,8 @@ interface WatchdogEvaluationSummary {
|
|
|
1349
1397
|
stillDegraded: number;
|
|
1350
1398
|
timedOutClaims: number;
|
|
1351
1399
|
expiredGrants: number;
|
|
1400
|
+
/** Findings whose evaluation threw; logged and retried on the next tick. */
|
|
1401
|
+
failed: number;
|
|
1352
1402
|
}
|
|
1353
1403
|
/**
|
|
1354
1404
|
* Post-Remediation Watchdog & Lease/Claim Timeout Evaluator (RFC 0013 § 5.B, § 6.B & P1 #3).
|
|
@@ -1357,10 +1407,18 @@ interface WatchdogEvaluationSummary {
|
|
|
1357
1407
|
* - `recovery_status`: whether the service recovered under comparable traffic (`N >= minComparableRequests`)
|
|
1358
1408
|
* - `causal_verification_status`: `"operationally_corroborated"` (never promoted to `"counterfactual_verified"`
|
|
1359
1409
|
* by telemetry recovery alone!).
|
|
1410
|
+
*
|
|
1411
|
+
* Checkpoints are scheduled at +2m/+5m/+15m measured from the remediation's
|
|
1412
|
+
* completion time (not from whenever a tick happened to run), and a finding
|
|
1413
|
+
* only becomes `recovered` after two consecutive healthy checkpoints that each
|
|
1414
|
+
* had sufficient traffic. Pass `projectId` to scope the sweep to one tenant
|
|
1415
|
+
* (HTTP path); the cron path omits it and sweeps every project.
|
|
1360
1416
|
*/
|
|
1361
1417
|
declare function evaluateRemediationWatchdogs(db: SqlDb, options?: {
|
|
1362
1418
|
nowIso?: string;
|
|
1363
1419
|
minComparableRequests?: number;
|
|
1420
|
+
projectId?: string;
|
|
1421
|
+
logger?: Logger;
|
|
1364
1422
|
}): Promise<WatchdogEvaluationSummary>;
|
|
1365
1423
|
|
|
1366
1424
|
/**
|