@obsunified/collector 2.0.1 → 2.1.1
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.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-UGWDOGGH.js → chunk-KMEZKGSO.js} +22 -10
- package/dist/chunk-KMEZKGSO.js.map +1 -0
- package/dist/index.d.ts +322 -26
- package/dist/index.js +1478 -415
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-HFVO1zIp.d.ts → ingest-auth-C5-8Mly5.d.ts} +9 -0
- package/package.json +5 -5
- package/src/analyses/derive.ts +26 -25
- package/src/analyses/investigations.ts +8 -8
- package/src/analyses/tier0.ts +22 -22
- package/src/auth/dashboard-auth.test.ts +791 -0
- package/src/auth/dashboard-auth.ts +187 -38
- package/src/auth/login-rate-limit.test.ts +129 -0
- package/src/auth/login-rate-limit.ts +143 -0
- package/src/framework/body-limit.test.ts +165 -0
- package/src/framework/collector.ts +355 -115
- package/src/framework/env.ts +9 -0
- package/src/framework/limits.ts +31 -0
- package/src/framework/scheduled-handlers.test.ts +342 -0
- package/src/index.ts +16 -1
- package/src/lib/action-retention.test.ts +472 -0
- package/src/lib/action-retention.ts +420 -0
- package/src/lib/actions-store.ts +6 -12
- package/src/lib/ai-store/calls.ts +2 -2
- package/src/lib/ai-store/sessions.ts +3 -3
- package/src/lib/ai-store/spans.ts +1 -1
- package/src/lib/ai-store/types.ts +2 -1
- package/src/lib/alerts-store.test.ts +153 -0
- package/src/lib/alerts-store.ts +122 -21
- package/src/lib/analyses-runner.test.ts +75 -0
- package/src/lib/analyses-runner.ts +60 -3
- package/src/lib/analyses-store-latest.test.ts +61 -0
- package/src/lib/analyses-store.ts +17 -10
- package/src/lib/blob-store.ts +1 -1
- package/src/lib/housekeeping.test.ts +73 -0
- package/src/lib/housekeeping.ts +93 -0
- package/src/lib/identity-index/action-lookups.ts +6 -6
- package/src/lib/identity-index/key-lookups.ts +2 -2
- package/src/lib/identity-index/user-lookup.ts +8 -5
- package/src/lib/identity-index.test.ts +2 -3
- package/src/lib/metrics-store-series.test.ts +99 -0
- package/src/lib/metrics-store.test.ts +23 -0
- package/src/lib/metrics-store.ts +27 -3
- package/src/lib/projects-store.test.ts +25 -0
- package/src/lib/projects-store.ts +4 -3
- package/src/lib/sql-db-dialect.test.ts +12 -3
- package/src/lib/sql-db-postgres.integration.test.ts +1225 -0
- package/src/lib/sql-db-postgres.test.ts +314 -0
- package/src/lib/sql-db-postgres.ts +392 -54
- package/src/lib/sql-db-sqlite-time-windows.test.ts +194 -0
- package/src/lib/sql-db.ts +40 -6
- package/src/lib/test-utils/action-graph-fixtures.ts +307 -0
- package/src/lib/test-utils/node-sqlite-db.ts +188 -0
- package/src/lib/webhook-url.test.ts +122 -0
- package/src/lib/webhook-url.ts +155 -0
- package/src/migrations/041_ingest_key_hash_text.sql +5 -0
- package/src/migrations/042_session_replay_project_composite_pk.sql +52 -0
- package/src/migrations/043_metric_exemplars_text_timestamps.sql +5 -0
- package/src/migrations/044_actions_started_at_index.sql +10 -0
- package/src/migrations/045_user_profiles_project_composite_pk.sql +40 -0
- package/src/migrations/046_action_graph_project_composite_pk.sql +255 -0
- package/src/migrations/047_ai_span_payloads_project_composite_pk.sql +61 -0
- package/src/migrations/048_telemetry_spans_project_composite_pk.sql +92 -0
- package/src/migrations/049_alert_state_notified.sql +17 -0
- package/src/migrations/alert-state-notified.test.ts +111 -0
- package/src/migrations/parity.test.ts +64 -0
- package/src/migrations/project-scoped-keys.test.ts +432 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +4 -4
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +1 -1
- package/src/migrations-postgres/035_metric_exemplars.sql +5 -2
- package/src/migrations-postgres/041_ingest_key_hash_text.sql +32 -0
- package/src/migrations-postgres/042_session_replay_project_composite_pk.sql +16 -0
- package/src/migrations-postgres/043_metric_exemplars_text_timestamps.sql +47 -0
- package/src/migrations-postgres/044_actions_started_at_index.sql +8 -0
- package/src/migrations-postgres/045_user_profiles_project_composite_pk.sql +33 -0
- package/src/migrations-postgres/046_action_graph_project_composite_pk.sql +39 -0
- package/src/migrations-postgres/047_ai_span_payloads_project_composite_pk.sql +30 -0
- package/src/migrations-postgres/048_telemetry_spans_project_composite_pk.sql +37 -0
- package/src/migrations-postgres/049_alert_state_notified.sql +10 -0
- package/src/migrations-postgres/README.md +58 -5
- package/src/otlp/decode/body.test.ts +76 -0
- package/src/otlp/decode/body.ts +67 -6
- package/src/plugins/_json.ts +21 -0
- package/src/plugins/action-graph-processor.ts +6 -12
- package/src/plugins/ai-receiver.ts +5 -12
- package/src/plugins/ai-span-payloads-processor.ts +1 -2
- package/src/plugins/alerts-evaluator-retention.test.ts +170 -0
- package/src/plugins/alerts-evaluator.test.ts +464 -3
- package/src/plugins/alerts-evaluator.ts +165 -41
- package/src/plugins/alerts-routes.ts +10 -6
- package/src/plugins/ask-routes.ts +4 -5
- package/src/plugins/connected-routes.test.ts +106 -0
- package/src/plugins/connected-routes.ts +14 -5
- package/src/plugins/dashboard-routes.test.ts +120 -0
- package/src/plugins/dashboard-routes.ts +11 -2
- package/src/plugins/eval-cases-routes.ts +8 -17
- package/src/plugins/identity-receiver.ts +9 -11
- package/src/plugins/json-body.test.ts +91 -0
- package/src/plugins/onboarding-routes.test.ts +105 -0
- package/src/plugins/onboarding-routes.ts +26 -11
- package/src/plugins/projects-routes.ts +2 -1
- package/src/plugins/replay-isolation.test.ts +922 -0
- package/src/plugins/replay-receiver.ts +8 -8
- package/src/plugins/usage-receiver.ts +3 -6
- package/dist/chunk-UGWDOGGH.js.map +0 -1
package/dist/auth.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import 'hono';
|
|
2
|
-
export { e as createIngestAuth, r as resetIngestAuthCache } from './ingest-auth-
|
|
2
|
+
export { e as createIngestAuth, r as resetIngestAuthCache } from './ingest-auth-C5-8Mly5.js';
|
package/dist/auth.js
CHANGED
|
@@ -114,10 +114,11 @@ var ProjectsStore = class {
|
|
|
114
114
|
warning: "This key will not be shown again. Store it somewhere safe."
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
async revokeKey(keyId) {
|
|
117
|
+
async revokeKey(keyId, projectId) {
|
|
118
118
|
const result = await this.db.prepare(
|
|
119
|
-
`UPDATE ingest_keys SET revoked_at = ?
|
|
120
|
-
|
|
119
|
+
`UPDATE ingest_keys SET revoked_at = ?
|
|
120
|
+
WHERE id = ? AND project_id = ? AND revoked_at IS NULL`
|
|
121
|
+
).bind((/* @__PURE__ */ new Date()).toISOString(), keyId, projectId).run();
|
|
121
122
|
return (result.meta?.changes ?? 0) > 0;
|
|
122
123
|
}
|
|
123
124
|
async findByKeyHash(keyHash) {
|
|
@@ -170,18 +171,27 @@ var D1Adapter = class {
|
|
|
170
171
|
}
|
|
171
172
|
};
|
|
172
173
|
var sqlDbFor = (env) => isSqlDb(env.DB) ? env.DB : new D1Adapter(env.DB);
|
|
174
|
+
var SQLITE_ISO_FORMAT = "%Y-%m-%dT%H:%M:%fZ";
|
|
175
|
+
var sqliteIsoNow = (modifier) => modifier ? `strftime('${SQLITE_ISO_FORMAT}', 'now', ${modifier})` : `strftime('${SQLITE_ISO_FORMAT}', 'now')`;
|
|
173
176
|
var sqliteDialect = {
|
|
174
177
|
name: "sqlite",
|
|
175
|
-
now: () =>
|
|
176
|
-
sinceHours: (param) => `
|
|
177
|
-
sinceMinutes: (param) => `
|
|
178
|
+
now: () => sqliteIsoNow(),
|
|
179
|
+
sinceHours: (param) => sqliteIsoNow(`'-' || ${param} || ' hours'`),
|
|
180
|
+
sinceMinutes: (param) => sqliteIsoNow(`'-' || ${param} || ' minutes'`),
|
|
178
181
|
jsonText: (expr, path) => `json_extract(${expr}, '${path}')`
|
|
179
182
|
};
|
|
183
|
+
var POSTGRES_ISO_TEXT_FORMAT = `'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'`;
|
|
184
|
+
var POSTGRES_UTC_NOW = "(CURRENT_TIMESTAMP AT TIME ZONE 'UTC')";
|
|
185
|
+
var postgresIsoText = (expr) => `to_char(${expr}, ${POSTGRES_ISO_TEXT_FORMAT})`;
|
|
180
186
|
var postgresDialect = {
|
|
181
187
|
name: "postgres",
|
|
182
|
-
now: () =>
|
|
183
|
-
sinceHours: (param) =>
|
|
184
|
-
|
|
188
|
+
now: () => postgresIsoText(POSTGRES_UTC_NOW),
|
|
189
|
+
sinceHours: (param) => postgresIsoText(
|
|
190
|
+
`${POSTGRES_UTC_NOW} - (${param}::text || ' hours')::interval`
|
|
191
|
+
),
|
|
192
|
+
sinceMinutes: (param) => postgresIsoText(
|
|
193
|
+
`${POSTGRES_UTC_NOW} - (${param}::text || ' minutes')::interval`
|
|
194
|
+
),
|
|
185
195
|
jsonText: (expr, path) => {
|
|
186
196
|
const key = path.replace(/^\$\./, "").replace(/^"|"$/g, "").replace(/\\u002E/g, ".");
|
|
187
197
|
return `(${expr}::jsonb ->> '${key.replace(/'/g, "''")}')`;
|
|
@@ -308,6 +318,8 @@ function createIngestAuth(config) {
|
|
|
308
318
|
export {
|
|
309
319
|
D1Adapter,
|
|
310
320
|
sqlDbFor,
|
|
321
|
+
SQLITE_ISO_FORMAT,
|
|
322
|
+
postgresIsoText,
|
|
311
323
|
postgresDialect,
|
|
312
324
|
dialectFor,
|
|
313
325
|
sha256Hex,
|
|
@@ -316,4 +328,4 @@ export {
|
|
|
316
328
|
resetIngestAuthCache,
|
|
317
329
|
createIngestAuth
|
|
318
330
|
};
|
|
319
|
-
//# sourceMappingURL=chunk-
|
|
331
|
+
//# sourceMappingURL=chunk-KMEZKGSO.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\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 existing = await this.db\n\t\t\t.prepare(`SELECT id FROM ingest_keys WHERE key_hash = ?`)\n\t\t\t.bind(keyHash)\n\t\t\t.first<{ id: string }>();\n\t\tif (existing) return;\n\n\t\tconst id = randomHex(16);\n\t\tconst keyPrefix = plaintext.slice(0, Math.min(16, plaintext.length));\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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as hono from 'hono';
|
|
2
|
-
import { MiddlewareHandler, Hono } from 'hono';
|
|
2
|
+
import { MiddlewareHandler, Hono, Context } from 'hono';
|
|
3
3
|
import * as hono_types from 'hono/types';
|
|
4
4
|
import * as _obsunified_types from '@obsunified/types';
|
|
5
5
|
import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationsListResponse, AlertComparison, JsonValue, EvidenceReference, EvidenceReferenceContract, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
|
|
6
|
-
import { b as CollectorEnv,
|
|
7
|
-
export { a as CollectorApp, d as consoleLogger, e as createIngestAuth } from './ingest-auth-
|
|
6
|
+
import { b as CollectorEnv, L as Logger, C as ChildSpanRunner, c as CollectorRouteContext } from './ingest-auth-C5-8Mly5.js';
|
|
7
|
+
export { a as CollectorApp, d as consoleLogger, e as createIngestAuth } from './ingest-auth-C5-8Mly5.js';
|
|
8
8
|
import { ActionConfidence } from '@obsunified/types/constants';
|
|
9
9
|
import { Pool } from 'pg';
|
|
10
10
|
|
|
@@ -81,6 +81,77 @@ declare class D1Adapter implements SqlDb {
|
|
|
81
81
|
}>>;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* RFC 0002 Stage 1 — runner that executes SQL-only Analyses on the existing
|
|
86
|
+
* scheduled handler.
|
|
87
|
+
*
|
|
88
|
+
* runSqlAnalysis — execute one definition's `sql` against D1 and shape the
|
|
89
|
+
* row(s) into an AnalysisResult.
|
|
90
|
+
* runAllDueAnalyses — orchestrator: refresh the in-process registry into
|
|
91
|
+
* analysis_definitions, find which ones are due, run them,
|
|
92
|
+
* persist the results. Per-analysis errors are isolated
|
|
93
|
+
* via Promise.allSettled so one bad SQL doesn't kill the
|
|
94
|
+
* batch.
|
|
95
|
+
*
|
|
96
|
+
* Result shape convention: an Analysis SQL is expected to return a single
|
|
97
|
+
* row with the columns:
|
|
98
|
+
*
|
|
99
|
+
* status TEXT | NULL -- 'ok' | 'warn' | 'critical' | 'unknown'
|
|
100
|
+
* primary_value REAL | NULL
|
|
101
|
+
* baseline_value REAL | NULL
|
|
102
|
+
* payload TEXT | NULL -- JSON-serialized object, optional
|
|
103
|
+
*
|
|
104
|
+
* If `status` is NULL we derive it from the magnitude of delta_pct
|
|
105
|
+
* (>=25% → critical, >=10% → warn, else ok). delta_pct itself is computed
|
|
106
|
+
* here from primary/baseline so individual SQL queries don't have to.
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Orchestrator. Refreshes the analysis_definitions registry from the
|
|
111
|
+
* in-process registry (so newly-derived Tier 1 analyses become visible
|
|
112
|
+
* to the dashboard within one tick), then runs whatever's due.
|
|
113
|
+
*
|
|
114
|
+
* Runs once per project in the `projects` table (falling back to the
|
|
115
|
+
* `default` project when the table is empty or unreadable), so analyses and
|
|
116
|
+
* the alert rules bound to them refresh for every tenant, not just the
|
|
117
|
+
* first one.
|
|
118
|
+
*/
|
|
119
|
+
declare function runAllDueAnalyses(ctx: {
|
|
120
|
+
env: CollectorEnv;
|
|
121
|
+
retentionHours: number;
|
|
122
|
+
logger?: Logger;
|
|
123
|
+
tracer?: ChildSpanRunner;
|
|
124
|
+
}, _runtime?: unknown): Promise<{
|
|
125
|
+
ran: number;
|
|
126
|
+
failed: number;
|
|
127
|
+
refreshed: number;
|
|
128
|
+
narrated: number;
|
|
129
|
+
skipped?: number;
|
|
130
|
+
}>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Retention for the secondary tables the hourly sweep did not cover. Each
|
|
134
|
+
* rule is tied to an existing contract rather than a new policy:
|
|
135
|
+
*
|
|
136
|
+
* - `alert_evaluations`: `/internal/alerts/evaluations` accepts at most a
|
|
137
|
+
* 720-hour window, so rows older than that are unreachable through the API.
|
|
138
|
+
* - `ask_evidence_events`: feeds `getTopAskedAnalyses`, whose trailing window
|
|
139
|
+
* is 7 days; rows older than 30 days cannot influence it.
|
|
140
|
+
* - `metric_series`: identity rows whose points have all expired and that
|
|
141
|
+
* were created before the retention window. They hold no data; the next
|
|
142
|
+
* ingest of that identity recreates the row.
|
|
143
|
+
* - `evidence_retrieval_refs`: rows carry their own `expires_at` (copied from
|
|
144
|
+
* the signal they reference); expansions follow their ref.
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
interface HousekeepingSummary {
|
|
148
|
+
alert_evaluations: number;
|
|
149
|
+
ask_evidence_events: number;
|
|
150
|
+
metric_series: number;
|
|
151
|
+
evidence_refs: number;
|
|
152
|
+
evidence_ref_expansions: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
84
155
|
interface ServiceMapOptions {
|
|
85
156
|
projectId: string;
|
|
86
157
|
hours: number;
|
|
@@ -298,6 +369,18 @@ interface CollectorConfig {
|
|
|
298
369
|
* a tracing layer at the storage seam.
|
|
299
370
|
*/
|
|
300
371
|
sqlDb?: SqlDbFactory;
|
|
372
|
+
/**
|
|
373
|
+
* Raw request body cap for `/v1/*` and `/internal/*` in bytes. Defaults
|
|
374
|
+
* to `MAX_BODY_BYTES` from the environment, then 10 MiB. Oversized
|
|
375
|
+
* requests get a 413 before any handler buffers the body.
|
|
376
|
+
*/
|
|
377
|
+
maxBodyBytes?: number;
|
|
378
|
+
/**
|
|
379
|
+
* Body cap for the unauthenticated `/auth/*` routes (login/check/logout).
|
|
380
|
+
* Defaults to 16 KiB and is independent of `maxBodyBytes`, so an anonymous
|
|
381
|
+
* client can never make the host buffer an ingest-sized body.
|
|
382
|
+
*/
|
|
383
|
+
maxAuthBodyBytes?: number;
|
|
301
384
|
}
|
|
302
385
|
declare class CollectorRuntime implements CollectorPluginContext {
|
|
303
386
|
private readonly spanProcessors;
|
|
@@ -327,30 +410,56 @@ declare const createTelemetryCollectorApp: (pluginsOrConfig: CollectorPlugin[] |
|
|
|
327
410
|
Bindings: CollectorEnv;
|
|
328
411
|
}>;
|
|
329
412
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
413
|
+
* Runtime-neutral entry point for a background maintenance task.
|
|
414
|
+
*
|
|
415
|
+
* `run(env)` does the work and needs nothing but a `CollectorEnv`, so any
|
|
416
|
+
* host (Cloudflare cron, a Node `setInterval`, a Kubernetes CronJob hitting
|
|
417
|
+
* a one-shot script) can drive it. `scheduled(event, env, ctx)` is the thin
|
|
418
|
+
* Cloudflare Workers adapter over `run` — the event and context are accepted
|
|
419
|
+
* for signature compatibility with `ExportedHandler.scheduled` but are never
|
|
420
|
+
* consulted, so non-Workers hosts should call `run` directly rather than
|
|
421
|
+
* fabricating placeholder event objects.
|
|
333
422
|
*/
|
|
423
|
+
interface ScheduledTaskHandler<TSummary = void> {
|
|
424
|
+
run(env: CollectorEnv): Promise<TSummary>;
|
|
425
|
+
scheduled(event: ScheduledEvent, env: CollectorEnv, ctx: ExecutionContext): Promise<void>;
|
|
426
|
+
}
|
|
427
|
+
/** Per-table purge counts reported by the retention cleanup task. */
|
|
428
|
+
interface RetentionCleanupSummary {
|
|
429
|
+
spans: number;
|
|
430
|
+
usage: number;
|
|
431
|
+
logs: number;
|
|
432
|
+
ai_calls: number;
|
|
433
|
+
metric_points: number;
|
|
434
|
+
analysis_results: number;
|
|
435
|
+
profile_blobs: number;
|
|
436
|
+
/** RFC 0010 action spine rows removed (trees older than RETENTION_HOURS). */
|
|
437
|
+
actions: number;
|
|
438
|
+
/** Leaf rows removed with their actions: agent_runs, tool_calls,
|
|
439
|
+
* retrieval_events, eval_results and artifacts. */
|
|
440
|
+
action_leaves: number;
|
|
441
|
+
/** Secondary tables (see lib/housekeeping.ts); zeros when that step failed. */
|
|
442
|
+
housekeeping: HousekeepingSummary;
|
|
443
|
+
}
|
|
334
444
|
declare const createRetentionCleanupHandler: (options?: {
|
|
335
445
|
logger?: Logger;
|
|
336
|
-
}) =>
|
|
337
|
-
scheduled(_event: ScheduledEvent, env: CollectorEnv, _ctx: ExecutionContext): Promise<void>;
|
|
338
|
-
};
|
|
446
|
+
}) => ScheduledTaskHandler<RetentionCleanupSummary>;
|
|
339
447
|
/**
|
|
340
448
|
* RFC 0002 Stage 1 — run any due Analyses. Cheap to call frequently because
|
|
341
449
|
* `runAllDueAnalyses` filters by `last_run + refreshSeconds < now` before
|
|
342
450
|
* touching D1.
|
|
343
451
|
*
|
|
344
|
-
* Wire to a per-minute trigger in wrangler.toml: `crons = ["* * * * *"]
|
|
345
|
-
*
|
|
346
|
-
* retention table every
|
|
452
|
+
* Wire to a per-minute trigger in wrangler.toml: `crons = ["* * * * *"]`,
|
|
453
|
+
* or call `.run(env)` from a non-Workers host scheduler. Kept distinct from
|
|
454
|
+
* retention so the per-minute tick doesn't sweep every retention table every
|
|
455
|
+
* cycle.
|
|
347
456
|
*/
|
|
348
457
|
declare const createAnalysesRunHandler: (options?: {
|
|
349
458
|
logger?: Logger;
|
|
350
459
|
tracer?: ChildSpanRunner;
|
|
351
|
-
}) =>
|
|
352
|
-
|
|
353
|
-
|
|
460
|
+
}) => ScheduledTaskHandler<AnalysesRunSummary | undefined>;
|
|
461
|
+
/** Shape returned by `runAllDueAnalyses` and surfaced via the run handler. */
|
|
462
|
+
type AnalysesRunSummary = Awaited<ReturnType<typeof runAllDueAnalyses>>;
|
|
354
463
|
|
|
355
464
|
declare const actionRoutesPlugin: CollectorPlugin;
|
|
356
465
|
|
|
@@ -371,15 +480,20 @@ declare const aiReceiverPlugin: CollectorPlugin;
|
|
|
371
480
|
|
|
372
481
|
declare const aiSpanPayloadsProcessorPlugin: CollectorPlugin;
|
|
373
482
|
|
|
483
|
+
/** Resolve a hostname to its addresses (e.g. `dns.lookup(host, { all: true })`). */
|
|
484
|
+
type HostLookup = (hostname: string) => Promise<string[]>;
|
|
485
|
+
|
|
374
486
|
declare function evaluateAllRules(env: CollectorEnv, options?: {
|
|
375
487
|
logger?: Logger;
|
|
376
488
|
tracer?: ChildSpanRunner;
|
|
377
489
|
concurrency?: number;
|
|
378
490
|
ruleTimeoutMs?: number;
|
|
491
|
+
hostLookup?: HostLookup;
|
|
379
492
|
}): Promise<{
|
|
380
493
|
evaluated: number;
|
|
381
494
|
fired: number;
|
|
382
495
|
resolved: number;
|
|
496
|
+
redelivered: number;
|
|
383
497
|
}>;
|
|
384
498
|
/**
|
|
385
499
|
* Scheduled handler suitable for wrangler cron triggers. Dispatches on the
|
|
@@ -666,18 +780,61 @@ declare const usersQueryRoutesPlugin: CollectorPlugin;
|
|
|
666
780
|
declare const utmEnrichmentPlugin: CollectorPlugin;
|
|
667
781
|
|
|
668
782
|
/**
|
|
669
|
-
*
|
|
783
|
+
* Fixed-window failure limiter for `POST /auth/login`. Only failed attempts
|
|
784
|
+
* count; a successful login clears the client's bucket. State is in-memory,
|
|
785
|
+
* so on Cloudflare Workers it is per isolate and on Node per process — it
|
|
786
|
+
* slows a single-origin brute force without any shared storage, and it can
|
|
787
|
+
* never lock an operator out for longer than one window.
|
|
788
|
+
*/
|
|
789
|
+
interface LoginRateLimitOptions {
|
|
790
|
+
/** Failed attempts allowed per window before 429. Default 10. */
|
|
791
|
+
maxFailures?: number;
|
|
792
|
+
/** Window length in milliseconds. Default 15 minutes. */
|
|
793
|
+
windowMs?: number;
|
|
794
|
+
/** Upper bound on tracked clients; the oldest bucket is evicted past it. */
|
|
795
|
+
maxClients?: number;
|
|
796
|
+
/** Clock override for tests. */
|
|
797
|
+
now?: () => number;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
interface DashboardAuthConfig {
|
|
801
|
+
password?: string;
|
|
802
|
+
token?: string;
|
|
803
|
+
/**
|
|
804
|
+
* Failed-login limiter for POST /auth/login (see login-rate-limit.ts).
|
|
805
|
+
* Pass `false` to disable; defaults to 10 failures per 15 minutes.
|
|
806
|
+
* `clientAddress` lets the host supply the connection's peer address;
|
|
807
|
+
* `trustProxyHeaders` honours forwarding headers from a trusted proxy.
|
|
808
|
+
* Without either, Workers use the edge-set client IP and every other
|
|
809
|
+
* runtime has no trustworthy identity: limiting is then skipped (never a
|
|
810
|
+
* shared bucket, which would let one client lock everyone out) and a
|
|
811
|
+
* warning is logged once via `warn` (default `console.warn`).
|
|
812
|
+
*/
|
|
813
|
+
loginRateLimit?: (LoginRateLimitOptions & {
|
|
814
|
+
clientAddress?: (c: Context<{
|
|
815
|
+
Bindings: CollectorEnv;
|
|
816
|
+
}>) => string | undefined;
|
|
817
|
+
trustProxyHeaders?: boolean;
|
|
818
|
+
/** Sink for the one-time "limiter inactive" warning. */
|
|
819
|
+
warn?: (message: string) => void;
|
|
820
|
+
}) | false;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Creates auth routes and middleware for dashboard password login and
|
|
824
|
+
* programmatic read/admin bearer token authentication.
|
|
670
825
|
*
|
|
671
826
|
* Routes added:
|
|
672
827
|
* POST /auth/login — accepts { password }, sets session cookie
|
|
673
|
-
* GET /auth/check — returns { authenticated: true/false }
|
|
828
|
+
* GET /auth/check — returns { authenticated: true/false } (supports cookie and Bearer token)
|
|
674
829
|
* POST /auth/logout — clears session cookie
|
|
675
830
|
*
|
|
676
|
-
* The returned middleware validates
|
|
831
|
+
* The returned middleware validates either:
|
|
832
|
+
* 1. Authorization: Bearer <token> (programmatic read/admin token for MCP/API)
|
|
833
|
+
* 2. obs_session cookie (human dashboard user session)
|
|
834
|
+
*
|
|
835
|
+
* Ingest keys are rejected on dashboard/internal routes — they are write-only.
|
|
677
836
|
*/
|
|
678
|
-
declare function createDashboardAuth(config: {
|
|
679
|
-
password: string;
|
|
680
|
-
}): {
|
|
837
|
+
declare function createDashboardAuth(config?: DashboardAuthConfig): {
|
|
681
838
|
middleware: MiddlewareHandler<{
|
|
682
839
|
Bindings: CollectorEnv;
|
|
683
840
|
Variables: {
|
|
@@ -718,6 +875,107 @@ declare class TailHub {
|
|
|
718
875
|
private broadcast;
|
|
719
876
|
}
|
|
720
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Retention for the RFC 0010 action graph (`actions` spine plus the
|
|
880
|
+
* `agent_runs`, `tool_calls`, `retrieval_events`, `eval_results` and
|
|
881
|
+
* `artifacts` leaves).
|
|
882
|
+
*
|
|
883
|
+
* Unlike the telemetry tables these rows carry no `expires_at`; the sweep
|
|
884
|
+
* derives expiry from `actions.started_at` and the configured
|
|
885
|
+
* `RETENTION_HOURS`. The unit of deletion is the causal tree
|
|
886
|
+
* (`project_id`, `root_action_id`): a tree is selected, rechecked and
|
|
887
|
+
* removed as a whole inside one `SqlDb.batch` (a D1 batch or a Postgres
|
|
888
|
+
* transaction), never action by action. A tree is kept whole while any of
|
|
889
|
+
* its actions:
|
|
890
|
+
*
|
|
891
|
+
* - started inside the retention window,
|
|
892
|
+
* - is still in progress (`ended_at IS NULL`),
|
|
893
|
+
* - belongs to an `agent_runs` row whose `status` is `running`,
|
|
894
|
+
* - is referenced by an `eval_cases` row in the same project via
|
|
895
|
+
* `source_action_id`, `source_agent_run_id`, or (through a `tool_calls`
|
|
896
|
+
* row) `source_tool_call_id`.
|
|
897
|
+
*
|
|
898
|
+
* Work is bounded per invocation: each batch packs whole trees up to
|
|
899
|
+
* `maxActionsPerBatch` actions and `maxTreesPerBatch` trees, and the sweep
|
|
900
|
+
* stops after `maxBatches`. A tree with more than `maxActionsPerBatch`
|
|
901
|
+
* actions can never be deleted atomically within those bounds, so it is
|
|
902
|
+
* retained intact and reported as `oversized_trees` (and logged by the
|
|
903
|
+
* handler) instead of being deleted in pieces. Raise `maxActionsPerBatch`
|
|
904
|
+
* to reclaim such trees.
|
|
905
|
+
*
|
|
906
|
+
* Concurrency: every DELETE repeats the eligibility predicate, so a tree
|
|
907
|
+
* that gained a fresh or unfinished action, a `running` run, or an eval
|
|
908
|
+
* case between selection and deletion is left alone. On D1 the batch is a
|
|
909
|
+
* single serialized SQLite transaction, so that recheck is airtight. On
|
|
910
|
+
* Postgres (READ COMMITTED) each statement sees writes committed before it
|
|
911
|
+
* started, so a commit landing between the leaf deletes and the final spine
|
|
912
|
+
* delete of the same tree could leave that tree's spine without its leaves.
|
|
913
|
+
* That needs an ingest for a tree whose newest action is already older than
|
|
914
|
+
* the retention window, inside a sub-second window; it is documented rather
|
|
915
|
+
* than locked against because `SqlDb` has no row-locking primitive.
|
|
916
|
+
*
|
|
917
|
+
* Intentionally **not** deleted here:
|
|
918
|
+
*
|
|
919
|
+
* - `artifacts.storage_ref` blobs — only the metadata row goes; the blob
|
|
920
|
+
* store is not consulted (the collector never wrote those blobs).
|
|
921
|
+
* - `agent_runs` rows whose `id` is not an action in the tree — nothing
|
|
922
|
+
* proves when they started.
|
|
923
|
+
* - `ai_span_payloads.action_id` and `evidence_retrieval_refs` anchors —
|
|
924
|
+
* those tables have their own `expires_at` retention.
|
|
925
|
+
* - `eval_cases`, `eval_runs`, `eval_case_results` — curated fixtures,
|
|
926
|
+
* never retention-bound.
|
|
927
|
+
*/
|
|
928
|
+
|
|
929
|
+
interface ActionGraphRetentionOptions {
|
|
930
|
+
/** Retention window; trees whose newest action is older than this expire. */
|
|
931
|
+
retentionHours: number;
|
|
932
|
+
/** Clock override for tests. Defaults to `Date.now`. */
|
|
933
|
+
now?: () => number;
|
|
934
|
+
/**
|
|
935
|
+
* Upper bound on actions deleted per batch/transaction. Also the largest
|
|
936
|
+
* tree the sweep will delete; bigger trees are retained and reported.
|
|
937
|
+
* Defaults to 500.
|
|
938
|
+
*/
|
|
939
|
+
maxActionsPerBatch?: number;
|
|
940
|
+
/** Upper bound on trees per batch (bounds statements per transaction). Defaults to 25. */
|
|
941
|
+
maxTreesPerBatch?: number;
|
|
942
|
+
/** Upper bound on batches per invocation. Defaults to 10. */
|
|
943
|
+
maxBatches?: number;
|
|
944
|
+
}
|
|
945
|
+
/** Per-table delete counts for one `purgeExpiredActionGraph` call. */
|
|
946
|
+
interface ActionGraphPurgeSummary {
|
|
947
|
+
actions: number;
|
|
948
|
+
agent_runs: number;
|
|
949
|
+
tool_calls: number;
|
|
950
|
+
retrieval_events: number;
|
|
951
|
+
eval_results: number;
|
|
952
|
+
artifacts: number;
|
|
953
|
+
/** Whole trees deleted. */
|
|
954
|
+
trees: number;
|
|
955
|
+
/** Batches (transactions) executed. */
|
|
956
|
+
batches: number;
|
|
957
|
+
/** `true` when `maxBatches` was hit while deletable trees remained. */
|
|
958
|
+
truncated: boolean;
|
|
959
|
+
/**
|
|
960
|
+
* Expired, otherwise eligible trees retained because they hold more than
|
|
961
|
+
* `maxActionsPerBatch` actions. Capped at `OVERSIZED_REPORT_LIMIT`.
|
|
962
|
+
*/
|
|
963
|
+
oversized_trees: number;
|
|
964
|
+
/** Identities of the reported oversized trees, for operators. */
|
|
965
|
+
oversized: Array<{
|
|
966
|
+
project_id: string;
|
|
967
|
+
root_action_id: string;
|
|
968
|
+
actions: number;
|
|
969
|
+
}>;
|
|
970
|
+
}
|
|
971
|
+
/** Cutoff as the same ISO text shape the collector writes to `started_at`. */
|
|
972
|
+
declare const actionRetentionCutoff: (retentionHours: number, nowMs?: number) => string;
|
|
973
|
+
/**
|
|
974
|
+
* Purge expired action trees. Safe to call repeatedly: a second call right
|
|
975
|
+
* after the first finds no deletable trees and changes nothing.
|
|
976
|
+
*/
|
|
977
|
+
declare function purgeExpiredActionGraph(db: SqlDb, options: ActionGraphRetentionOptions): Promise<ActionGraphPurgeSummary>;
|
|
978
|
+
|
|
721
979
|
type ActionRecord = Omit<ActionRef, "projectId"> & {
|
|
722
980
|
projectId: string;
|
|
723
981
|
};
|
|
@@ -756,10 +1014,15 @@ declare class ActionStore {
|
|
|
756
1014
|
listArtifactsByAction(projectId: string, actionId: string, limit?: number): Promise<ArtifactRef[]>;
|
|
757
1015
|
}
|
|
758
1016
|
|
|
1017
|
+
interface AlertsStoreOptions {
|
|
1018
|
+
/** Permit webhook targets on loopback / private / link-local hosts. */
|
|
1019
|
+
allowPrivateWebhookTargets?: boolean;
|
|
1020
|
+
}
|
|
759
1021
|
declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
|
|
760
1022
|
declare class AlertsStore {
|
|
761
1023
|
private readonly db;
|
|
762
|
-
|
|
1024
|
+
private readonly options;
|
|
1025
|
+
constructor(db: SqlDb, options?: AlertsStoreOptions);
|
|
763
1026
|
validateInput(input: AlertRuleInput): void;
|
|
764
1027
|
listRules(projectId: string): Promise<AlertRule[]>;
|
|
765
1028
|
getRule(id: string, projectId: string): Promise<AlertRule | null>;
|
|
@@ -767,8 +1030,24 @@ declare class AlertsStore {
|
|
|
767
1030
|
updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
|
|
768
1031
|
deleteRule(id: string, projectId: string): Promise<boolean>;
|
|
769
1032
|
listEnabledRules(): Promise<AlertRule[]>;
|
|
770
|
-
getState(ruleId: string): Promise<AlertStateRow | null>;
|
|
1033
|
+
getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
|
|
1034
|
+
/**
|
|
1035
|
+
* Record a state transition with its notification still outstanding
|
|
1036
|
+
* (`notified = 0`); `markNotified` flips it once a channel accepts the
|
|
1037
|
+
* alert. On a database that has not applied migration 049 the column is
|
|
1038
|
+
* missing: the transition is still written (legacy statement) so alerting
|
|
1039
|
+
* keeps working through a rolling deploy, and the evaluator treats a row
|
|
1040
|
+
* without the column as delivered.
|
|
1041
|
+
*/
|
|
771
1042
|
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
1043
|
+
/**
|
|
1044
|
+
* Mark the transition `(state, lastStateChange)` as delivered. Scoped to
|
|
1045
|
+
* the exact transition the caller notified for: if the rule changed state
|
|
1046
|
+
* again while the webhook was in flight, the newer transition keeps its
|
|
1047
|
+
* own outstanding flag. Returns false when nothing matched (stale or
|
|
1048
|
+
* already marked). On a pre-049 schema there is nothing to mark.
|
|
1049
|
+
*/
|
|
1050
|
+
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
772
1051
|
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean): Promise<void>;
|
|
773
1052
|
listEvaluations(params: {
|
|
774
1053
|
ruleId: string;
|
|
@@ -801,7 +1080,7 @@ declare class AlertsStore {
|
|
|
801
1080
|
* - `R2BlobStore` — Cloudflare R2 binding (default on Workers).
|
|
802
1081
|
* - `S3BlobStore` (sibling file) — AWS S3 / MinIO / any S3-compatible.
|
|
803
1082
|
*
|
|
804
|
-
* Replay chunks live under `replays/<session_id>/<chunk_id>`; pprof
|
|
1083
|
+
* Replay chunks live under `replays/<project_id>/<session_id>/<chunk_id>`; pprof
|
|
805
1084
|
* profiles live under `profiles/<service>/<trace_id>/<span_id>`. The
|
|
806
1085
|
* adapter never sees these paths — they're constructed by the callers
|
|
807
1086
|
* (`replay-receiver`, `profile-routes`).
|
|
@@ -1152,7 +1431,7 @@ declare class ProjectsStore {
|
|
|
1152
1431
|
ensureDefaultProject(): Promise<void>;
|
|
1153
1432
|
listKeys(projectId: string): Promise<IngestKey[]>;
|
|
1154
1433
|
createKey(projectId: string, name: string): Promise<IngestKeyWithPlaintext>;
|
|
1155
|
-
revokeKey(keyId: string): Promise<boolean>;
|
|
1434
|
+
revokeKey(keyId: string, projectId: string): Promise<boolean>;
|
|
1156
1435
|
findByKeyHash(keyHash: string): Promise<{
|
|
1157
1436
|
projectId: string;
|
|
1158
1437
|
keyId: string;
|
|
@@ -1184,6 +1463,23 @@ declare class PostgresAdapter implements SqlDb {
|
|
|
1184
1463
|
};
|
|
1185
1464
|
}>>;
|
|
1186
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Install `pg` result-type parsers so Postgres rows match the shape D1
|
|
1468
|
+
* returns and the shared stores keep working unchanged:
|
|
1469
|
+
*
|
|
1470
|
+
* - BOOLEAN → 0 / 1 (D1 stores flags as INTEGER; stores compare `=== 1`)
|
|
1471
|
+
* - BIGINT → number (`COUNT(*)`, `SUM(bigint)` would otherwise be strings;
|
|
1472
|
+
* integral values above MAX_SAFE_INTEGER throw)
|
|
1473
|
+
* - NUMERIC → number (`SUM`, `EXTRACT(EPOCH …)`; same overflow rule)
|
|
1474
|
+
* - JSON/JSONB → raw text (stores `JSON.parse` the column themselves)
|
|
1475
|
+
*
|
|
1476
|
+
* Call once per process before issuing queries, e.g.
|
|
1477
|
+
* `configurePostgresTypeParsers(pg.types)` in the Node entrypoint.
|
|
1478
|
+
*/
|
|
1479
|
+
interface PostgresTypeParserRegistry {
|
|
1480
|
+
setTypeParser(oid: number, parser: (value: string) => unknown): void;
|
|
1481
|
+
}
|
|
1482
|
+
declare const configurePostgresTypeParsers: (types: PostgresTypeParserRegistry) => void;
|
|
1187
1483
|
|
|
1188
1484
|
/** All built-in plugins in recommended registration order */
|
|
1189
1485
|
declare const allPlugins: CollectorPlugin[];
|
|
@@ -1194,4 +1490,4 @@ declare const _default: hono.Hono<{
|
|
|
1194
1490
|
Bindings: CollectorEnv;
|
|
1195
1491
|
}, hono_types.BlankSchema, "/">;
|
|
1196
1492
|
|
|
1197
|
-
export { type ActionRecord, ActionStore, type AgentRunRecord, AlertsStore, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, D1Adapter, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, ProjectsStore, R2BlobStore, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, askRoutesPlugin, botFilterPlugin, compareValue, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, evalCasesRoutesPlugin, evaluateAllRules, evidenceRetrievalRoutesPlugin, genAiNormalizerPlugin, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, queryRoutesPlugin, redactionProcessorPlugin, replayQueryRoutesPlugin, replayReceiverPlugin, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin };
|
|
1493
|
+
export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, askRoutesPlugin, botFilterPlugin, compareValue, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, evalCasesRoutesPlugin, evaluateAllRules, evidenceRetrievalRoutesPlugin, genAiNormalizerPlugin, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, purgeExpiredActionGraph, queryRoutesPlugin, redactionProcessorPlugin, replayQueryRoutesPlugin, replayReceiverPlugin, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin };
|