diff --git a/apps/api/src/routes/internal/audit-trail.http.test.ts b/apps/api/src/routes/internal/audit-trail.http.test.ts index 14ebd32529..d9f3942468 100644 --- a/apps/api/src/routes/internal/audit-trail.http.test.ts +++ b/apps/api/src/routes/internal/audit-trail.http.test.ts @@ -1,8 +1,10 @@ -import { describe, expect, it } from "vitest" +import { afterEach, describe, expect, it } from "vitest" import { AlertMemberDirectoryUnavailableError, ApiSchemaErrors, ApiUnexpectedErrors, + AuditDetection, + AuditDetectionListResponse, AuditTrailApiGroup, type AuditTrailEntry, AuditTrailListResponse, @@ -11,10 +13,14 @@ import { RoleName, UserId, } from "@maple/domain/http" -import { Context, Effect, Layer, Schema, Array as Arr, String as Str } from "effect" +import { Context, Effect, Layer, Order, Schema, Array as Arr, String as Str } from "effect" +import { encodePublicId, PublicIdPrefixes } from "@maple/domain/http/v2" import { HttpRouter } from "effect/http" import { HttpApi, HttpApiBuilder } from "effect/http-api" import { ApiErrorBoundaryLive } from "@maple/backend/http/error-boundary" +import { AuditLogService, makeMemoryAuditLog } from "@maple/backend/services/audit/AuditLogService" +import { AuditDetectionsService } from "@maple/backend/services/audit-trail/AuditDetectionsService" +import { cleanupTestDbs, createTestDb, type TestDb } from "@maple/backend/testing/test-pglite" import { type AuditTrailListFilters, type AuditTrailPage, @@ -34,6 +40,12 @@ const USER_ID = Schema.decodeSync(UserId)("user_trail_route") const unused = Effect.die("not used by the audit trail route") const asRole = Schema.decodeSync(RoleName) const asPage = Schema.decodeUnknownSync(AuditTrailListResponse) +const asDetection = Schema.decodeUnknownSync(AuditDetection) +const asDetections = Schema.decodeUnknownSync(AuditDetectionListResponse) +const asFailure = Schema.decodeUnknownSync(Schema.Struct({ _tag: Schema.String })) + +const trackedDbs: TestDb[] = [] +afterEach(() => cleanupTestDbs(trackedDbs)) const event = (overrides: Partial = {}): AuditTrailEntry => ({ id: Str.repeat(32)("a"), @@ -55,15 +67,28 @@ const event = (overrides: Partial = {}): AuditTrailEntry => ({ ...overrides, }) -/** The route over a trail that answers with `page` and records the filters it was asked for. */ -const makeHarness = (roles: ReadonlyArray, page: AuditTrailPage, directoryDown = false) => { +/** + * The route over a trail that answers with `page` and records the filters it was asked for, + * and the real detections service over an embedded database (`testDb`, shared to read back + * as another caller). + */ +const makeHarness = ( + roles: ReadonlyArray, + page: AuditTrailPage, + directoryDown = false, + testDb: TestDb = createTestDb(trackedDbs), +) => { const asked: Array = [] let directoryReads = 0 + const auditLog = makeMemoryAuditLog() const services = Layer.mergeAll( Layer.succeed(AuditTrailService, { write: () => unused, list: (_orgId, filters) => Effect.sync(() => (asked.push(filters), page)), + count: () => unused, }), + Layer.effect(AuditDetectionsService, AuditDetectionsService.make).pipe(Layer.provide(testDb.layer)), + Layer.succeed(AuditLogService, auditLog), Layer.succeed(OrgMembersService, { listMembers: () => directoryDown @@ -105,17 +130,40 @@ const makeHarness = (roles: ReadonlyArray, page: AuditTrailPage, directo ) const { handler, dispose } = HttpRouter.toWebHandler(routes, { disableLogger: true }) - const list = async (search = "") => { - const response = await handler( + const send = async (request: Request) => { + const response = await handler(request, Context.empty() as never) + return { status: response.status, body: await response.json() } + } + + const list = (search = "") => + send( new Request(`http://maple.test/internal/audit-trail/events${search}`, { headers: { authorization: "Bearer test-token" }, }), - Context.empty() as never, ) - return { status: response.status, body: await response.json() } - } - return { list, asked, directoryReads: () => directoryReads, dispose } + /** A call to a detections endpoint: `path` is what follows `/detections`. */ + const detections = (method: string, path = "", body?: unknown) => + send( + new Request(`http://maple.test/internal/audit-trail/detections${path}`, { + method, + headers: { + authorization: "Bearer test-token", + ...(body === undefined ? undefined : { "content-type": "application/json" }), + }, + ...(body === undefined ? undefined : { body: JSON.stringify(body) }), + }), + ) + + /** What Maple's own audit log recorded: each entry's action, resource and changed fields. */ + const audited = async () => + Arr.map(await Effect.runPromise(auditLog.list(ORG_ID, { limit: 50, offset: 0 })), (entry) => ({ + action: entry.action, + resourceId: entry.resourceId, + changedFields: entry.changedFields, + })) + + return { list, detections, audited, asked, directoryReads: () => directoryReads, dispose } } const ADMIN = ["org:admin"] @@ -287,3 +335,159 @@ describe("GET /internal/audit-trail/events", () => { } }) }) + +const EMPTY_PAGE: AuditTrailPage = { events: [], next: null } + +const detectionBody = (overrides: Record = {}) => ({ + name: "Members added", + filters: { provider: "vercel", category: "membership" }, + windowMinutes: 10, + threshold: 3, + severity: "critical", + ...overrides, +}) + +const UNKNOWN_ID = "00000000-0000-4000-8000-000000000000" + +describe("/internal/audit-trail/detections", () => { + // A detection says what the audit log holds, so it sits behind the audit log permission. + it("refuses an engineer on every endpoint and stores nothing", async () => { + const testDb = createTestDb(trackedDbs) + const admin = makeHarness(ADMIN, EMPTY_PAGE, false, testDb) + const engineer = makeHarness(["org:engineer"], EMPTY_PAGE, false, testDb) + try { + const existing = asDetection((await admin.detections("POST", "", detectionBody())).body) + + const statuses = [ + await engineer.detections("GET"), + await engineer.detections("POST", "", detectionBody({ name: "Theirs" })), + await engineer.detections("PATCH", `/${existing.id}`, { threshold: 9 }), + await engineer.detections("DELETE", `/${existing.id}`), + ] + + expect(Arr.map(statuses, (response) => response.status)).toEqual([403, 403, 403, 403]) + expect(await engineer.audited()).toEqual([]) + expect(asDetections((await admin.detections("GET")).body).detections).toEqual([existing]) + } finally { + await admin.dispose() + await engineer.dispose() + } + }) + + it("lets an admin create, list, edit and delete a detection, and audits each change", async () => { + const harness = makeHarness(ADMIN, EMPTY_PAGE) + try { + const created = await harness.detections("POST", "", detectionBody({ name: " Members added " })) + expect(created.status).toBe(200) + const detection = asDetection(created.body) + expect(detection).toMatchObject({ + name: "Members added", + enabled: true, + filters: { provider: "vercel", category: "membership" }, + windowMinutes: 10, + threshold: 3, + severity: "critical", + destinationIds: [], + }) + expect(asDetections((await harness.detections("GET")).body).detections).toEqual([detection]) + + const edited = await harness.detections("PATCH", `/${detection.id}`, { + threshold: 5, + enabled: false, + }) + expect(edited.status).toBe(200) + expect(asDetection(edited.body)).toMatchObject({ + id: detection.id, + name: "Members added", + threshold: 5, + enabled: false, + }) + + const deleted = await harness.detections("DELETE", `/${detection.id}`) + expect(deleted).toEqual({ status: 200, body: { id: detection.id } }) + expect(asDetections((await harness.detections("GET")).body).detections).toEqual([]) + + // The audit log names a resource by its public id. + const resourceId = encodePublicId(PublicIdPrefixes.alertRule, detection.id) + expect(Arr.sortWith(await harness.audited(), (entry) => entry.action, Order.String)).toEqual([ + { action: "alert_rule.created", resourceId, changedFields: null }, + { action: "alert_rule.deleted", resourceId, changedFields: null }, + { action: "alert_rule.updated", resourceId, changedFields: ["enabled", "threshold"] }, + ]) + } finally { + await harness.dispose() + } + }) + + it("answers 400 for a detection outside its bounds, before anything is stored", async () => { + const harness = makeHarness(ADMIN, EMPTY_PAGE) + try { + const responses = [ + await harness.detections("POST", "", detectionBody({ name: " " })), + await harness.detections("POST", "", detectionBody({ name: Str.repeat(121)("x") })), + await harness.detections("POST", "", detectionBody({ windowMinutes: 4 })), + await harness.detections("POST", "", detectionBody({ windowMinutes: 1441 })), + await harness.detections("POST", "", detectionBody({ threshold: 0 })), + await harness.detections("POST", "", detectionBody({ threshold: 2.5 })), + await harness.detections("POST", "", detectionBody({ severity: "info" })), + await harness.detections("POST", "", detectionBody({ filters: { provider: "nowhere" } })), + await harness.detections("POST", "", detectionBody({ filters: { action: " " } })), + await harness.detections( + "POST", + "", + detectionBody({ destinationIds: Arr.makeBy(21, () => UNKNOWN_ID) }), + ), + ] + + expect(Arr.dedupe(Arr.map(responses, (response) => response.status))).toEqual([400]) + expect(asDetections((await harness.detections("GET")).body).detections).toEqual([]) + } finally { + await harness.dispose() + } + }) + + // The unique index on org and name would otherwise answer a 500. + it("answers 400 for an unknown destination and for a name already taken", async () => { + const harness = makeHarness(ADMIN, EMPTY_PAGE) + try { + const first = asDetection((await harness.detections("POST", "", detectionBody())).body) + const other = asDetection( + (await harness.detections("POST", "", detectionBody({ name: "Tokens created" }))).body, + ) + + const responses = [ + await harness.detections("POST", "", detectionBody({ name: "Members added" })), + await harness.detections("PATCH", `/${other.id}`, { name: first.name }), + await harness.detections( + "POST", + "", + detectionBody({ name: "Third", destinationIds: [UNKNOWN_ID] }), + ), + ] + + expect( + Arr.map(responses, (response) => [response.status, asFailure(response.body)._tag]), + ).toEqual([ + [400, "@maple/http/errors/AuditDetectionInvalidError"], + [400, "@maple/http/errors/AuditDetectionInvalidError"], + [400, "@maple/http/errors/AuditDetectionInvalidError"], + ]) + expect(asDetections((await harness.detections("GET")).body).detections).toHaveLength(2) + } finally { + await harness.dispose() + } + }) + + it("answers 404 for a detection that does not exist", async () => { + const harness = makeHarness(ADMIN, EMPTY_PAGE) + try { + const edited = await harness.detections("PATCH", `/${UNKNOWN_ID}`, { threshold: 5 }) + const deleted = await harness.detections("DELETE", `/${UNKNOWN_ID}`) + + expect([edited.status, deleted.status]).toEqual([404, 404]) + expect(await harness.audited()).toEqual([]) + } finally { + await harness.dispose() + } + }) +}) diff --git a/apps/api/src/routes/internal/audit-trail.http.ts b/apps/api/src/routes/internal/audit-trail.http.ts index a29c5acd75..ddfec81108 100644 --- a/apps/api/src/routes/internal/audit-trail.http.ts +++ b/apps/api/src/routes/internal/audit-trail.http.ts @@ -7,11 +7,14 @@ import { } from "@maple/domain/http" import type { AuditTrailEntry } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" +import { recordHttpAudit } from "@maple/backend/services/audit/AuditLogService" +import { AuditDetectionsService } from "@maple/backend/services/audit-trail/AuditDetectionsService" import { AuditTrailService } from "@maple/backend/services/audit-trail/AuditTrailService" import type { OrgMember } from "@maple/backend/services/org/OrgMembersService" import { OrgMembersService } from "@maple/backend/services/org/OrgMembersService" import { Effect, Array as Arr, Option, String as Str } from "effect" import { orgMemberDirectory, orgMemberDirectoryOrFail } from "@/routes/org-member-directory" +import { auditDiff } from "@/routes/v2/audit-changes" const DEFAULT_PAGE_LIMIT = 50 @@ -50,70 +53,132 @@ const memberByEmail = (directory: ReadonlyMap, email: string) (member) => Str.toLowerCase(member.email) === Str.toLowerCase(email), ) +const detectionAuditDiff = auditDiff({ + fields: ["name", "enabled", "filters", "windowMinutes", "threshold", "severity", "destinationIds"], +}) + +/** + * The tenant, once it may read the audit log. Detections sit behind the same gate as the events: + * a rule that counts them says what the log holds, so only the people who may read it write one. + */ +const requireAuditLogRead = Effect.gen(function* () { + const tenant = yield* CurrentTenant.Context + yield* Policy.check( + tenant.roles, + "audit_log:read", + (denial) => new AuditTrailForbiddenError({ message: denial.message }), + ) + return tenant +}) + export const HttpAuditTrailLive = HttpApiBuilder.group(MapleInternalApi, "auditTrail", (handlers) => Effect.gen(function* () { const trail = yield* AuditTrailService + const detections = yield* AuditDetectionsService const members = yield* OrgMembersService - return handlers.handle("list", ({ query }) => - Effect.gen(function* () { - const tenant = yield* CurrentTenant.Context - // Every platform's actor emails and origin IPs: the same gate as Maple's own audit log. - yield* Policy.check( - tenant.roles, - "audit_log:read", - (denial) => new AuditTrailForbiddenError({ message: denial.message }), - ) - // Only Maple's own events store a user id where the page shows an email. - const emailActor = - query.actor !== undefined && - Str.includes("@")(query.actor) && - (query.provider === undefined || query.provider === "maple") - ? query.actor - : undefined - // The filter's answer depends on this lookup, so an outage fails the request: a - // page that silently left the member's Maple events out would read as complete. - const filterDirectory = - emailActor === undefined - ? undefined - : yield* orgMemberDirectoryOrFail(members, tenant.orgId).pipe( - Effect.mapError( - (error) => - new AuditTrailPersistenceError({ - message: - "The member directory is unavailable, so an email cannot be matched to a member", - cause: error._tag, - }), - ), - ) - const actorAlias = - emailActor === undefined || filterDirectory === undefined - ? Option.none() - : memberByEmail(filterDirectory, emailActor) - const page = yield* trail.list(tenant.orgId, { - ...(query.provider !== undefined ? { provider: query.provider } : undefined), - ...(query.account !== undefined ? { account: query.account } : undefined), - ...(query.category !== undefined ? { category: query.category } : undefined), - ...(query.outcome !== undefined ? { outcome: query.outcome } : undefined), - ...(query.action !== undefined ? { action: query.action } : undefined), - ...(query.actor !== undefined ? { actor: query.actor } : undefined), - ...(Option.isSome(actorAlias) ? { actorAlias: actorAlias.value.userId } : undefined), - ...(query.since !== undefined ? { sinceMs: query.since } : undefined), - ...(query.until !== undefined ? { untilMs: query.until } : undefined), - ...(query.cursor !== undefined ? { before: parseCursor(query.cursor) } : undefined), - limit: query.limit ?? DEFAULT_PAGE_LIMIT, - }) - // Names are decoration: without a directory the page still shows every id. - const directory = - filterDirectory ?? - (Arr.some(page.events, (event) => unnamedMapleUserId(event) !== null) - ? yield* orgMemberDirectory(members, tenant.orgId) - : new Map()) - return { - events: nameMapleUsers(page.events, directory), - nextCursor: page.next === null ? null : `${page.next.occurredAtMs}:${page.next.id}`, - } - }), - ) + return handlers + .handle("list", ({ query }) => + Effect.gen(function* () { + // Every platform's actor emails and origin IPs: the same gate as Maple's own audit log. + const tenant = yield* requireAuditLogRead + // Only Maple's own events store a user id where the page shows an email. + const emailActor = + query.actor !== undefined && + Str.includes("@")(query.actor) && + (query.provider === undefined || query.provider === "maple") + ? query.actor + : undefined + // The filter's answer depends on this lookup, so an outage fails the request: a + // page that silently left the member's Maple events out would read as complete. + const filterDirectory = + emailActor === undefined + ? undefined + : yield* orgMemberDirectoryOrFail(members, tenant.orgId).pipe( + Effect.mapError( + (error) => + new AuditTrailPersistenceError({ + message: + "The member directory is unavailable, so an email cannot be matched to a member", + cause: error._tag, + }), + ), + ) + const actorAlias = + emailActor === undefined || filterDirectory === undefined + ? Option.none() + : memberByEmail(filterDirectory, emailActor) + const page = yield* trail.list(tenant.orgId, { + ...(query.provider !== undefined ? { provider: query.provider } : undefined), + ...(query.account !== undefined ? { account: query.account } : undefined), + ...(query.category !== undefined ? { category: query.category } : undefined), + ...(query.outcome !== undefined ? { outcome: query.outcome } : undefined), + ...(query.action !== undefined ? { action: query.action } : undefined), + ...(query.actor !== undefined ? { actor: query.actor } : undefined), + ...(Option.isSome(actorAlias) ? { actorAlias: actorAlias.value.userId } : undefined), + ...(query.since !== undefined ? { sinceMs: query.since } : undefined), + ...(query.until !== undefined ? { untilMs: query.until } : undefined), + ...(query.cursor !== undefined ? { before: parseCursor(query.cursor) } : undefined), + limit: query.limit ?? DEFAULT_PAGE_LIMIT, + }) + // Names are decoration: without a directory the page still shows every id. + const directory = + filterDirectory ?? + (Arr.some(page.events, (event) => unnamedMapleUserId(event) !== null) + ? yield* orgMemberDirectory(members, tenant.orgId) + : new Map()) + return { + events: nameMapleUsers(page.events, directory), + nextCursor: page.next === null ? null : `${page.next.occurredAtMs}:${page.next.id}`, + } + }), + ) + .handle("listDetections", () => + Effect.gen(function* () { + const tenant = yield* requireAuditLogRead + return { detections: yield* detections.list(tenant.orgId) } + }), + ) + .handle("createDetection", ({ payload }) => + Effect.gen(function* () { + const tenant = yield* requireAuditLogRead + const created = yield* detections.create(tenant.orgId, { + ...payload, + createdBy: tenant.userId, + }) + yield* recordHttpAudit("alert_rule.created", { + resourceId: created.id, + metadata: { name: created.name, signalType: "audit" }, + }) + return created + }), + ) + .handle("updateDetection", ({ params, payload }) => + Effect.gen(function* () { + const tenant = yield* requireAuditLogRead + const current = yield* detections.get(tenant.orgId, params.id) + const updated = yield* detections.update(tenant.orgId, params.id, { + ...payload, + updatedBy: tenant.userId, + }) + yield* recordHttpAudit("alert_rule.updated", { + resourceId: updated.id, + changes: detectionAuditDiff(payload, current, updated), + metadata: { name: updated.name, signalType: "audit" }, + }) + return updated + }), + ) + .handle("deleteDetection", ({ params }) => + Effect.gen(function* () { + const tenant = yield* requireAuditLogRead + const id = yield* detections.remove(tenant.orgId, params.id) + yield* recordHttpAudit("alert_rule.deleted", { + resourceId: id, + metadata: { signalType: "audit" }, + }) + return { id } + }), + ) }), ) diff --git a/apps/api/src/routes/internal/query-engine.http.ts b/apps/api/src/routes/internal/query-engine.http.ts index 5116a41498..7ce5ac0c4c 100644 --- a/apps/api/src/routes/internal/query-engine.http.ts +++ b/apps/api/src/routes/internal/query-engine.http.ts @@ -21,7 +21,6 @@ import { PlanetScaleInfraTimeseriesResponse, RailwayInfraRowsResponse, VercelAiGatewayResponse, - VercelAuditLogResponse, VercelInfraDependenciesResponse, VercelInfraFunctionsResponse, VercelInfraFunctionTimeseriesResponse, @@ -1000,18 +999,6 @@ export const HttpQueryEngineLive = HttpApiBuilder.group(MapleInternalApi, "query }) }), ) - .handle("vercelAuditLog", ({ payload }) => - Effect.gen(function* () { - const tenant = yield* CurrentTenant.Context - const rows = yield* runQuery(Queries.vercelAuditLog, tenant, payload) - return new VercelAuditLogResponse({ - events: Arr.map(rows, (row) => ({ - ...row, - timestamp: DateTime.formatIso(row.timestamp), - })), - }) - }), - ) .handle("gcpInfraPresence", ({ payload }) => Effect.gen(function* () { const tenant = yield* CurrentTenant.Context diff --git a/apps/api/src/routes/v2/alchemy-provider.integration.test.ts b/apps/api/src/routes/v2/alchemy-provider.integration.test.ts index 8eabd3e17d..7bd7182ae7 100644 --- a/apps/api/src/routes/v2/alchemy-provider.integration.test.ts +++ b/apps/api/src/routes/v2/alchemy-provider.integration.test.ts @@ -47,6 +47,7 @@ import { AlertRuntime, AlertsService } from "@maple/backend/services/alerts/Aler import { UptimeAlertSource } from "@maple/backend/services/uptime/UptimeAlertSource" import { AnomalyAlertSource } from "@maple/backend/services/alerts/anomaly/AnomalyAlertSource" import { SloAlertSource } from "@maple/backend/services/slos/SloAlertSource" +import { AuditTrailService } from "@maple/backend/services/audit-trail/AuditTrailService" import { AlertDestinationsService } from "@maple/backend/services/alerts/AlertDestinationsService" import { AlertReadModelsService } from "@maple/backend/services/alerts/AlertReadModelsService" import { AlertRulesService } from "@maple/backend/services/alerts/AlertRulesService" @@ -206,6 +207,7 @@ const makeHarness = () => { Layer.provide(Layer.effect(SloReadService, SloReadService.make)), Layer.provide(Layer.mergeAll(testDb.layer, warehouseLive)), ), + Layer.effect(AuditTrailService, AuditTrailService.make).pipe(Layer.provide(warehouseLive)), ), ), ) diff --git a/apps/api/src/routes/v2/alert-rule-read-families.test.ts b/apps/api/src/routes/v2/alert-rule-read-families.test.ts index 5e7cbe5385..5bd6b0a6a4 100644 --- a/apps/api/src/routes/v2/alert-rule-read-families.test.ts +++ b/apps/api/src/routes/v2/alert-rule-read-families.test.ts @@ -40,7 +40,7 @@ describe("ruleReadFamilies", () => { expect(ruleReadFamilies({ signalType: "raw_query" })).toEqual(["traces", "logs", "metrics"]) }) - it.each(["anomaly", "uptime", "slo_burn_rate"] as const)( + it.each(["anomaly", "uptime", "slo_burn_rate", "audit"] as const)( "%s runs no query of its own and needs none", (signalType) => { expect(ruleReadFamilies({ signalType })).toEqual([]) diff --git a/apps/api/src/routes/v2/alert-rule-read-families.ts b/apps/api/src/routes/v2/alert-rule-read-families.ts index 8b92667da9..c759e6ce6b 100644 --- a/apps/api/src/routes/v2/alert-rule-read-families.ts +++ b/apps/api/src/routes/v2/alert-rule-read-families.ts @@ -18,11 +18,12 @@ export const ruleReadFamilies = ( case "apdex": case "throughput": return ["traces"] - // No query of their own: an anomaly rule reads open incidents, and uptime and SLO rules - // are refused by the generic rule APIs before anything runs. + // No query of their own: an anomaly rule reads open incidents, and uptime, SLO and + // audit detection rules are refused by the generic rule APIs before anything runs. case "anomaly": case "uptime": case "slo_burn_rate": + case "audit": return [] case "builder_query": { const source = rule.queryBuilderDraft?.dataSource diff --git a/apps/api/src/routes/v2/alert-rules.http.ts b/apps/api/src/routes/v2/alert-rules.http.ts index 8f32418c11..6428eeac29 100644 --- a/apps/api/src/routes/v2/alert-rules.http.ts +++ b/apps/api/src/routes/v2/alert-rules.http.ts @@ -652,6 +652,7 @@ export const HttpV2AlertRulesLive = HttpApiBuilder.group(MapleApiV2, "alertRules } : undefined), limit: limit + 1, + publicApi: true, }) const hasMore = response.checks.length > limit @@ -671,6 +672,7 @@ export const HttpV2AlertRulesLive = HttpApiBuilder.group(MapleApiV2, "alertRules const summary = yield* readModels.summarizeRuleChecks(tenant.orgId, params.id, { since: query.since, until: query.until, + publicApi: true, }) return { diff --git a/apps/api/src/routes/v2/alerts.http.test.ts b/apps/api/src/routes/v2/alerts.http.test.ts index 3870879674..65bc9f6550 100644 --- a/apps/api/src/routes/v2/alerts.http.test.ts +++ b/apps/api/src/routes/v2/alerts.http.test.ts @@ -45,6 +45,7 @@ import { AlertRuntime, AlertsService } from "@maple/backend/services/alerts/Aler import { UptimeAlertSource } from "@maple/backend/services/uptime/UptimeAlertSource" import { AnomalyAlertSource } from "@maple/backend/services/alerts/anomaly/AnomalyAlertSource" import { SloAlertSource } from "@maple/backend/services/slos/SloAlertSource" +import { AuditTrailService } from "@maple/backend/services/audit-trail/AuditTrailService" import { AlertDestinationsService } from "@maple/backend/services/alerts/AlertDestinationsService" import { AlertReadModelsService } from "@maple/backend/services/alerts/AlertReadModelsService" import { AlertRulesService } from "@maple/backend/services/alerts/AlertRulesService" @@ -219,6 +220,7 @@ const makeHarness = ( Layer.provide(Layer.effect(SloReadService, SloReadService.make)), Layer.provide(Layer.mergeAll(testDb.layer, warehouseLive)), ), + Layer.effect(AuditTrailService, AuditTrailService.make).pipe(Layer.provide(warehouseLive)), ), ), ) diff --git a/apps/api/src/runtime/service-graph.ts b/apps/api/src/runtime/service-graph.ts index c679e0d7cf..e35c1eb43d 100644 --- a/apps/api/src/runtime/service-graph.ts +++ b/apps/api/src/runtime/service-graph.ts @@ -110,6 +110,7 @@ import { ProductEventsService } from "@maple/backend/services/product-events/Pro import { AuditLogService } from "@maple/backend/services/audit/AuditLogService" import { AuditTrailService } from "@maple/backend/services/audit-trail/AuditTrailService" +import { AuditDetectionsService } from "@maple/backend/services/audit-trail/AuditDetectionsService" import { WarehouseQueryService } from "@maple/backend/services/warehouse/WarehouseQueryService" import { QueryEngineService } from "@maple/backend/services/warehouse/QueryEngineService" import { TelemetryReadService } from "@maple/backend/services/warehouse/TelemetryReadService" @@ -196,6 +197,7 @@ export const HttpServicesLive = Layer.mergeAll( ServiceMapWorldService.layer, AuditLogService.layer, AuditTrailService.layer, + AuditDetectionsService.layer, WarehouseQueryService.layer, QueryEngineService.layer, RuntimeReleaseCatalog.layer, diff --git a/apps/ingest/alchemy.run.ts b/apps/ingest/alchemy.run.ts index bb92b22d32..728a867f3e 100644 --- a/apps/ingest/alchemy.run.ts +++ b/apps/ingest/alchemy.run.ts @@ -522,6 +522,9 @@ export const createMapleIngest = ({ stage, region, hostname, profile, dbLogin }: ...(yield* blockedIngestKeysEnv), // The org Maple's own telemetry is filed under. Required, no fallback. MAPLE_INTERNAL_ORG_ID: yield* requiredPlain("MAPLE_INTERNAL_ORG_ID"), + // On wherever a stage is deployed whole and never rolled back to an older gateway; prd + // turns it on in its environment once `audit_trail_events` is there. + ...(stage.kind !== "prd" && { INGEST_AUDIT_TRAIL_ENABLED: "true" }), ...(yield* gatewayTuningEnv), // `INGEST_COMMIT_SHA` (prd) is the image's own revision, so a deploy that leaves // `apps/ingest` alone does not change the task definition and roll the hosts. diff --git a/apps/ingest/alchemy/gateway-env.ts b/apps/ingest/alchemy/gateway-env.ts index fc93c3c21a..be53f58776 100644 --- a/apps/ingest/alchemy/gateway-env.ts +++ b/apps/ingest/alchemy/gateway-env.ts @@ -24,6 +24,9 @@ export const gatewayTuningEnv: Config.Config = Config.all([ // switch rule application off in an emergency. optionalPlain("INGEST_RULES_ENABLED"), optionalPlain("INGEST_RULES_CACHE_TTL_SECS"), + // Provider audit events go to the audit trail instead of the logs. The gateway defaults to off: + // set "true" once `audit_trail_events` exists and every task runs a build that knows the switch. + optionalPlain("INGEST_AUDIT_TRAIL_ENABLED"), optionalPlain("INGEST_MAX_REQUEST_BODY_BYTES"), optionalPlain("INGEST_MAX_DECODED_BODY_BYTES"), optionalPlain("INGEST_MAX_DECODE_HEAP_BYTES"), diff --git a/apps/ingest/src/audit_trail.rs b/apps/ingest/src/audit_trail.rs new file mode 100644 index 0000000000..57fdd02f35 --- /dev/null +++ b/apps/ingest/src/audit_trail.rs @@ -0,0 +1,839 @@ +//! Audit events from provider feeds as rows of `audit_trail_events`, the table +//! behind the Security hub's audit log. They are written here, never as OTLP +//! logs: a log can be sent by anything holding an ingest key and read by every +//! member, and an audit record must be neither. +//! +//! The row shape and the id derivation mirror the TypeScript writer +//! (`packages/backend/src/services/audit-trail/audit-trail-row.ts`). The two +//! must agree: an event a provider redelivers has to land on the same key. + +use crate::vercel_logs::text; +use chrono::{DateTime, Utc}; +use serde_json::{json, Map as JsonMap, Value as JsonValue}; +use sha2::{Digest, Sha256}; +use std::borrow::Cow; + +type JsonObject = JsonMap; + +pub const DATASOURCE: &str = "audit_trail_events"; + +/// Above any ingest time in ms, so `Version` stays positive and falls as time +/// rises: the table keeps the highest version, which is the first write. +const VERSION_CEILING_MS: i64 = 9_999_999_999_999; + +/// A payload larger than this is not kept; the row's columns say what happened. +const RAW_LIMIT_BYTES: usize = 32 * 1024; + +/// The table drops a row this long after it happened. +const RETENTION_MS: i64 = 400 * 24 * 60 * 60 * 1000; +/// How far ahead of this gateway's clock a sender's may run. +const MAX_AHEAD_MS: i64 = 24 * 60 * 60 * 1000; + +/// The most bytes a column keeps. The sender sets every one of them, and the +/// table is shared by every org. +const NAME_LIMIT_BYTES: usize = 256; +const TEXT_LIMIT_BYTES: usize = 1024; + +/// One audit event, normalized. Empty strings are absent values. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct AuditEvent { + pub provider: &'static str, + pub account: String, + pub provider_event_id: String, + pub occurred_at_ms: i64, + pub actor_type: &'static str, + pub actor_id: String, + pub actor_name: String, + pub actor_email: String, + pub action: String, + pub category: &'static str, + pub outcome: &'static str, + pub outcome_reason: String, + pub target_type: String, + pub target_id: String, + pub target_name: String, + pub origin_ip: String, + pub user_agent: String, + pub raw: String, +} + +/// The row's key: the first 32 hex characters of sha256 over +/// `provider\naccount\neventId`. +pub fn event_id(provider: &str, account: &str, provider_event_id: &str) -> String { + sha256_prefix(format!("{provider}\n{account}\n{provider_event_id}").as_bytes()) +} + +const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef"; + +/// The first 16 bytes of a sha256, as 32 hex characters. +fn sha256_prefix(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut hex = String::with_capacity(32); + for byte in &digest[..16] { + hex.push(char::from(HEX_DIGITS[usize::from(byte >> 4)])); + hex.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)])); + } + hex +} + +/// `YYYY-MM-DD HH:MM:SS.mmm` in UTC, the form the warehouse's DateTime64(3) takes. +fn warehouse_datetime(epoch_ms: i64) -> String { + DateTime::::from_timestamp_millis(epoch_ms) + .unwrap_or_default() + .format("%Y-%m-%d %H:%M:%S%.3f") + .to_string() +} + +/// `value` cut to at most `max_bytes`, on a character boundary. +fn clip(value: &str, max_bytes: usize) -> &str { + if value.len() <= max_bytes { + return value; + } + let mut end = max_bytes; + while !value.is_char_boundary(end) { + end -= 1; + } + &value[..end] +} + +/// An id is part of the row's key, so one too long to keep is hashed, not cut: +/// two long ids sharing a prefix stay two events. +fn bounded_id(id: &str) -> Cow<'_, str> { + if id.len() <= NAME_LIMIT_BYTES { + Cow::Borrowed(id) + } else { + Cow::Owned(sha256_prefix(id.as_bytes())) + } +} + +impl AuditEvent { + /// The event as one NDJSON row for `org_id`, stored at `ingested_at_ms`. + pub fn to_row(&self, org_id: &str, ingested_at_ms: i64) -> Vec { + let name = |value: &'_ str| clip(value, NAME_LIMIT_BYTES).to_owned(); + let long = |value: &'_ str| clip(value, TEXT_LIMIT_BYTES).to_owned(); + let account = name(&self.account); + let provider_event_id = bounded_id(&self.provider_event_id); + json!({ + "OrgId": org_id, + "Id": event_id(self.provider, &account, &provider_event_id), + "OccurredAt": warehouse_datetime(self.occurred_at_ms), + "IngestedAt": warehouse_datetime(ingested_at_ms), + "Provider": self.provider, + "Account": account, + "ProviderEventId": provider_event_id, + "ActorType": self.actor_type, + "ActorId": name(&self.actor_id), + "ActorName": name(&self.actor_name), + "ActorEmail": name(&self.actor_email), + "Action": name(&self.action), + "Category": self.category, + "Outcome": self.outcome, + "OutcomeReason": long(&self.outcome_reason), + "TargetType": name(&self.target_type), + "TargetId": name(&self.target_id), + "TargetName": long(&self.target_name), + "OriginIp": name(&self.origin_ip), + "OriginCountry": "", + "UserAgent": long(&self.user_agent), + "Raw": self.raw, + "Version": VERSION_CEILING_MS - ingested_at_ms, + }) + .to_string() + .into_bytes() + } +} + +fn owned(value: Option<&str>) -> String { + value.unwrap_or_default().to_owned() +} + +fn object<'a>(parent: &'a JsonObject, key: &str) -> Option<&'a JsonObject> { + parent.get(key)?.as_object() +} + +/// The payload as JSON text, or a marker of its size when it is too large to keep. +fn raw_of(payload: &JsonObject) -> String { + let full = serde_json::to_string(payload).unwrap_or_default(); + if full.len() <= RAW_LIMIT_BYTES { + full + } else { + json!({ "truncated": true, "bytes": full.len() }).to_string() + } +} + +/// A key for an event the provider gave no id: the same payload is the same event. +fn content_id(payload: &JsonObject) -> String { + sha256_prefix( + serde_json::to_string(payload) + .unwrap_or_default() + .as_bytes(), + ) +} + +/// When the event happened, as the sender says, if that is a time a row can +/// hold: inside the table's retention and not ahead of this clock. Any other +/// time is replaced by now, so a sender cannot mint partitions on the shared +/// table, plant a row that never expires, or send one that expires on arrival. +fn occurred_at_ms(claimed: Option, now_ms: i64) -> i64 { + claimed + .filter(|ms| (now_ms - RETENTION_MS..=now_ms + MAX_AHEAD_MS).contains(ms)) + .unwrap_or(now_ms) +} + +/// The words of an action name, lowercased: split at punctuation and at a +/// camelCase hump. Matching whole words keeps `environment` from reading as `env`. +fn words(name: &str) -> Vec { + let mut words = Vec::new(); + let mut word = String::new(); + let mut after_lowercase = false; + for character in name.chars() { + let hump = character.is_ascii_uppercase() && after_lowercase; + if (hump || !character.is_ascii_alphanumeric()) && !word.is_empty() { + words.push(std::mem::take(&mut word)); + } + if character.is_ascii_alphanumeric() { + word.push(character.to_ascii_lowercase()); + } + after_lowercase = character.is_ascii_lowercase() || character.is_ascii_digit(); + } + if !word.is_empty() { + words.push(word); + } + words +} + +/// Whether any of `words` is one of `needles`, or its plural. +fn has_word(words: &[String], needles: &[&str]) -> bool { + words.iter().any(|word| { + needles + .iter() + .any(|needle| word == needle || word.strip_suffix('s') == Some(*needle)) + }) +} + +/// What a Vercel action is about, read off its name. Vercel publishes no closed +/// list, so this is by the words the names use; anything unrecognised is a +/// change to a resource. The order decides a name with two readings: a member's +/// role change is about permission, and password protection is a setting. +fn vercel_category(action: &str) -> &'static str { + let words = words(action); + let has = |needles: &[&str]| has_word(&words, needles); + if has(&[ + "login", "logout", "sso", "saml", "mfa", "2fa", "passkey", "signin", + ]) { + "authentication" + } else if has(&["protection"]) { + "configuration" + } else if has(&["role", "permission", "acl"]) || (has(&["access"]) && has(&["group"])) { + "permission" + } else if has(&[ + "token", + "secret", + "env", + "variable", + "certificate", + "cert", + "password", + "key", + ]) { + "credential" + } else if has(&["member", "invite", "invitation", "join", "leave"]) { + "membership" + } else if has(&["billing", "invoice", "payment", "plan", "subscription"]) { + "billing" + } else if has(&[ + "setting", + "drain", + "integration", + "webhook", + "firewall", + "config", + "configuration", + ]) { + "configuration" + } else { + "resource" + } +} + +/// What a Vercel action was done to: the action without its closing verb +/// (`project.env-var.created` is about a `project.env-var`). +fn vercel_target_type(action: &str) -> &str { + action + .rfind(['.', '-', '_']) + .map_or("", |verb_at| &action[..verb_at]) +} + +/// The id of what a Vercel action was done to, from the event's `payload`: the +/// id named after the action's subject (`drainId` for `drain-created`), or the +/// only id there is. +fn vercel_target_id<'a>(action: &str, payload: &'a JsonObject) -> Option<&'a str> { + let subject = words(action).into_iter().next().unwrap_or_default(); + if let Some(id) = text(payload, &format!("{subject}Id")) { + return Some(id); + } + let mut ids = payload + .iter() + .filter(|(key, _)| key.ends_with("Id")) + .filter_map(|(_, value)| value.as_str()); + let only = ids.next()?; + ids.next().is_none().then_some(only) +} + +/// One event of Vercel's Audit Log drain. `None` for an event with no action: +/// there is nothing to say happened. +pub fn vercel_event(event: &JsonObject, now_ms: i64) -> Option { + let action = text(event, "action")?; + let actor = object(event, "actor"); + let actor_text = |key: &str| owned(actor.and_then(|actor| text(actor, key))); + let actor_type = match actor.and_then(|actor| text(actor, "type")) { + Some("user") => "user", + Some("system") => "system", + Some(_) => "service", + None => "unknown", + }; + // Epoch milliseconds, whole in practice; a fractional one is cut to its millisecond. + #[expect( + clippy::cast_possible_truncation, + reason = "a finite positive epoch-ms value is far inside i64" + )] + let claimed_ms = event + .get("timestamp") + .and_then(JsonValue::as_f64) + .filter(|ms| ms.is_finite() && *ms > 0.0) + .map(|ms| ms.trunc() as i64); + let payload = object(event, "payload"); + Some(AuditEvent { + provider: "vercel", + account: owned(text(event, "teamId")), + provider_event_id: text(event, "id").map_or_else(|| content_id(event), str::to_owned), + occurred_at_ms: occurred_at_ms(claimed_ms, now_ms), + actor_type, + actor_id: actor_text("id"), + actor_name: actor_text("name"), + actor_email: actor_text("email"), + action: action.to_owned(), + category: vercel_category(action), + // The drain reports what was done; Vercel does not log refused attempts in it. + outcome: "success", + target_type: vercel_target_type(action).to_owned(), + target_id: owned(payload.and_then(|payload| vercel_target_id(action, payload))), + target_name: owned(payload.and_then(|payload| text(payload, "name"))), + origin_ip: owned(text(event, "ipAddress")), + user_agent: owned(text(event, "userAgent")), + raw: raw_of(event), + ..AuditEvent::default() + }) +} + +/// Which of Google's audit logs an entry is from, by its `logName`. +fn gcp_audit_log(log_name: &str) -> Option<&'static str> { + let (_, log) = log_name.rsplit_once("/logs/")?; + match log.replace("%2F", "/").as_str() { + "cloudaudit.googleapis.com/activity" => Some("activity"), + "cloudaudit.googleapis.com/policy" => Some("policy"), + _ => None, + } +} + +/// Kubernetes API writes reach the Admin Activity log under this service. Most +/// are a cluster's own controllers at work, thousands an hour: they stay logs. +const GCP_KUBERNETES_SERVICE: &str = "k8s.io"; + +/// Services whose every change is to how the organization itself is set up. +const GCP_CONFIGURATION_SERVICES: [&str; 6] = [ + "orgpolicy", + "logging", + "serviceusage", + "accesscontextmanager", + "essentialcontacts", + "securitycenter", +]; + +/// What a Google API call is about, read off its service and method. Only the +/// call itself is read (`SetIamPolicy`), not the package it sits in. +fn gcp_category(service: &str, method: &str) -> &'static str { + let call = words(method.rsplit('.').next().unwrap_or(method)); + let has = |needles: &[&str]| has_word(&call, needles); + let service_is = |names: &[&str]| names.iter().any(|name| service.contains(name)); + if has(&["iam"]) + || (service == "iam.googleapis.com" && has(&["role", "policy", "binding", "deny"])) + { + "permission" + } else if has(&["key"]) || service_is(&["secretmanager", "apikeys", "cloudkms"]) { + "credential" + } else if service_is(&["cloudbilling", "billingbudgets"]) { + "billing" + } else if has(&["login", "signin"]) { + "authentication" + } else if service_is(&GCP_CONFIGURATION_SERVICES) || (has(&["org"]) && has(&["policy"])) { + "configuration" + } else { + "resource" + } +} + +/// Who made a Google API call: `(type, id, email)`. A federated workload has +/// no email, only a `principalSubject`. +fn gcp_actor(payload: &JsonObject) -> (&'static str, &str, &str) { + let info = object(payload, "authenticationInfo"); + let email = info + .and_then(|info| text(info, "principalEmail")) + .unwrap_or_default(); + if !email.is_empty() { + let service = email.ends_with("gserviceaccount.com") || email.starts_with("system:"); + let address = if email.contains('@') { email } else { "" }; + return (if service { "service" } else { "user" }, email, address); + } + let subject = info + .and_then(|info| text(info, "principalSubject")) + .unwrap_or_default(); + if subject.is_empty() { + ("unknown", "", "") + } else if subject.starts_with("user:") { + ("user", subject, "") + } else { + ("service", subject, "") + } +} + +/// gRPC status codes Google uses for a refusal: PERMISSION_DENIED and UNAUTHENTICATED. +const GCP_DENIED_CODES: [i64; 2] = [7, 16]; + +/// A Cloud Logging entry from the Admin Activity or Policy Denied audit log. +/// `None` for every other entry, System Event included: those are Google's own +/// actions on a workload and stay with its logs. +pub fn gcp_entry(entry: &JsonObject, now_ms: i64) -> Option { + let log_name = text(entry, "logName")?; + let log = gcp_audit_log(log_name)?; + let payload = object(entry, "protoPayload")?; + let method = text(payload, "methodName")?; + let service = text(payload, "serviceName").unwrap_or_default(); + if service == GCP_KUBERNETES_SERVICE { + return None; + } + let (actor_type, actor_id, actor_email) = gcp_actor(payload); + + let status = object(payload, "status"); + let code = status + .and_then(|status| status.get("code")) + .and_then(JsonValue::as_i64) + .unwrap_or(0); + let outcome = if log == "policy" || GCP_DENIED_CODES.contains(&code) { + "denied" + } else if code == 0 { + "success" + } else { + "failure" + }; + + let resource = object(entry, "resource"); + let project = resource + .and_then(|resource| object(resource, "labels")) + .and_then(|labels| text(labels, "project_id")); + // `projects//logs/...`, or a folder's or organization's own log. + let account = project.map_or_else( + || { + log_name + .split("/logs/") + .next() + .unwrap_or_default() + .to_owned() + }, + str::to_owned, + ); + let request = object(payload, "requestMetadata"); + let claimed_ms = text(entry, "timestamp") + .and_then(|timestamp| DateTime::parse_from_rfc3339(timestamp).ok()) + .map(|timestamp| timestamp.timestamp_millis()); + + Some(AuditEvent { + provider: "gcp", + account, + provider_event_id: text(entry, "insertId").map_or_else(|| content_id(entry), str::to_owned), + occurred_at_ms: occurred_at_ms(claimed_ms, now_ms), + actor_type, + actor_id: actor_id.to_owned(), + actor_email: actor_email.to_owned(), + action: method.to_owned(), + category: if log == "policy" { + "permission" + } else { + gcp_category(service, method) + }, + outcome, + outcome_reason: owned(status.and_then(|status| text(status, "message"))), + target_type: owned(resource.and_then(|resource| text(resource, "type"))), + target_name: owned(text(payload, "resourceName")), + origin_ip: owned(request.and_then(|request| text(request, "callerIp"))), + user_agent: owned(request.and_then(|request| text(request, "callerSuppliedUserAgent"))), + raw: raw_of(entry), + ..AuditEvent::default() + }) +} + +/// Now, as the warehouse and this module count it. +pub fn now_ms() -> i64 { + Utc::now().timestamp_millis() +} + +#[cfg(test)] +mod tests { + use super::*; + + const NOW: i64 = 1_700_000_060_000; + + fn parse(json: &str) -> JsonObject { + serde_json::from_str(json).expect("fixture is a JSON object") + } + + fn row(event: &AuditEvent) -> JsonObject { + serde_json::from_slice(&event.to_row("org_1", NOW)).expect("a row is a JSON object") + } + + // The TypeScript writer derives the same key (`auditTrailEventId`); these two + // vectors are asserted there as well. A provider that moved between the two + // writers would otherwise stop collapsing its redeliveries. + #[test] + fn the_row_key_matches_the_typescript_writer() { + assert_eq!( + event_id("maple", "", "evt_1"), + "851ca0eff076aad987a0192777b176cd" + ); + assert_eq!( + event_id("maple", "acme", "evt_1"), + "1d19ea3d78217e97b45a7a9d28300146" + ); + } + + const VERCEL_EVENT: &str = r#"{"schema":"vercel.audit_log.v1","id":"evt_1","teamId":"team_1","action":"project.env-var.created","timestamp":1700000000123,"ipAddress":"203.0.113.7","userAgent":"Mozilla/5.0","actor":{"type":"user","id":"usr_1","name":"Ada","email":"ada@acme.test"},"payload":{"projectId":"prj_1"}}"#; + + #[test] + fn a_vercel_event_becomes_a_row_with_its_actor_action_and_origin() { + let event = vercel_event(&parse(VERCEL_EVENT), NOW).expect("an event with an action"); + let row = row(&event); + + assert_eq!(row["OrgId"], "org_1"); + assert_eq!(row["Provider"], "vercel"); + assert_eq!(row["Account"], "team_1"); + assert_eq!(row["ProviderEventId"], "evt_1"); + assert_eq!(row["Id"], event_id("vercel", "team_1", "evt_1")); + assert_eq!(row["OccurredAt"], "2023-11-14 22:13:20.123"); + assert_eq!(row["IngestedAt"], "2023-11-14 22:14:20.000"); + assert_eq!(row["ActorType"], "user"); + assert_eq!(row["ActorEmail"], "ada@acme.test"); + assert_eq!(row["Action"], "project.env-var.created"); + assert_eq!(row["Category"], "credential"); + assert_eq!(row["Outcome"], "success"); + assert_eq!(row["OriginIp"], "203.0.113.7"); + assert_eq!(row["TargetType"], "project.env-var"); + assert_eq!(row["TargetId"], "prj_1"); + assert!(row["Raw"].as_str().is_some_and(|raw| raw.contains("prj_1"))); + } + + // The table keeps the highest version: a redelivery must lose to the first write. + #[test] + fn a_later_write_of_the_same_event_has_the_same_key_and_a_lower_version() { + let event = vercel_event(&parse(VERCEL_EVENT), NOW).expect("an event with an action"); + let first: JsonObject = serde_json::from_slice(&event.to_row("org_1", NOW)).unwrap(); + let later: JsonObject = + serde_json::from_slice(&event.to_row("org_1", NOW + 60_000)).unwrap(); + + assert_eq!(first["Id"], later["Id"]); + assert!(later["Version"].as_i64() < first["Version"].as_i64()); + } + + #[test] + fn a_vercel_event_without_an_id_keys_on_its_content_and_one_without_an_action_is_skipped() { + let no_id = + parse(r#"{"teamId":"team_1","action":"drain-created","timestamp":1700000000000}"#); + let first = vercel_event(&no_id, NOW).expect("an event with an action"); + let again = vercel_event(&no_id, NOW + 5).expect("an event with an action"); + + assert!(!first.provider_event_id.is_empty()); + assert_eq!(first.provider_event_id, again.provider_event_id); + assert_eq!(first.category, "configuration"); + assert_eq!(first.actor_type, "unknown"); + assert_eq!(vercel_event(&parse(r#"{"id":"evt_2"}"#), NOW), None); + } + + const GCP_ACTIVITY: &str = r#"{"logName":"projects/acme-prod/logs/cloudaudit.googleapis.com%2Factivity","insertId":"abc123","timestamp":"2023-11-14T22:13:20.123Z","resource":{"type":"service_account","labels":{"project_id":"acme-prod"}},"protoPayload":{"serviceName":"iam.googleapis.com","methodName":"google.iam.admin.v1.CreateServiceAccountKey","resourceName":"projects/-/serviceAccounts/123","authenticationInfo":{"principalEmail":"ada@acme.test"},"requestMetadata":{"callerIp":"203.0.113.7","callerSuppliedUserAgent":"gcloud"}}}"#; + + #[test] + fn an_admin_activity_entry_becomes_a_row() { + let event = gcp_entry(&parse(GCP_ACTIVITY), NOW).expect("an Admin Activity entry"); + let row = row(&event); + + assert_eq!(row["Provider"], "gcp"); + assert_eq!(row["Account"], "acme-prod"); + assert_eq!(row["ProviderEventId"], "abc123"); + assert_eq!(row["OccurredAt"], "2023-11-14 22:13:20.123"); + assert_eq!(row["ActorType"], "user"); + assert_eq!(row["ActorEmail"], "ada@acme.test"); + assert_eq!(row["Action"], "google.iam.admin.v1.CreateServiceAccountKey"); + assert_eq!(row["Category"], "credential"); + assert_eq!(row["Outcome"], "success"); + assert_eq!(row["TargetType"], "service_account"); + assert_eq!(row["TargetName"], "projects/-/serviceAccounts/123"); + assert_eq!(row["OriginIp"], "203.0.113.7"); + assert_eq!(row["UserAgent"], "gcloud"); + } + + #[test] + fn a_refused_call_is_denied_with_its_reason_and_a_service_account_is_a_service() { + let mut entry = parse(GCP_ACTIVITY); + let payload = entry["protoPayload"].as_object_mut().unwrap(); + payload.insert( + "status".into(), + json!({ "code": 7, "message": "Permission iam.serviceAccountKeys.create denied" }), + ); + payload.insert( + "authenticationInfo".into(), + json!({ "principalEmail": "ci@acme-prod.iam.gserviceaccount.com" }), + ); + let event = gcp_entry(&entry, NOW).expect("an Admin Activity entry"); + + assert_eq!(event.outcome, "denied"); + assert_eq!( + event.outcome_reason, + "Permission iam.serviceAccountKeys.create denied" + ); + assert_eq!(event.actor_type, "service"); + } + + #[test] + fn a_failed_call_that_was_not_refused_is_a_failure() { + let mut entry = parse(GCP_ACTIVITY); + entry["protoPayload"].as_object_mut().unwrap().insert( + "status".into(), + json!({ "code": 9, "message": "Precondition failed" }), + ); + + assert_eq!( + gcp_entry(&entry, NOW).map(|event| event.outcome), + Some("failure") + ); + } + + #[test] + fn an_iam_policy_change_is_a_permission_event_and_a_policy_denial_is_denied() { + let mut policy_change = parse(GCP_ACTIVITY); + policy_change["protoPayload"] + .as_object_mut() + .unwrap() + .insert("methodName".into(), json!("SetIamPolicy")); + let mut denial = parse(GCP_ACTIVITY); + denial.insert( + "logName".into(), + json!("projects/acme-prod/logs/cloudaudit.googleapis.com%2Fpolicy"), + ); + + assert_eq!( + gcp_entry(&policy_change, NOW).map(|event| event.category), + Some("permission") + ); + let denied = gcp_entry(&denial, NOW).expect("a Policy Denied entry"); + assert_eq!((denied.category, denied.outcome), ("permission", "denied")); + } + + // System Event and Data Access entries, and a workload's own log lines, are not + // audit events of the org's people: they stay logs. + #[test] + fn only_admin_activity_and_policy_denied_entries_are_audit_events() { + for log in [ + "cloudaudit.googleapis.com%2Fsystem_event", + "cloudaudit.googleapis.com%2Fdata_access", + "run.googleapis.com%2Fstdout", + ] { + let mut entry = parse(GCP_ACTIVITY); + entry.insert( + "logName".into(), + json!(format!("projects/acme-prod/logs/{log}")), + ); + assert_eq!(gcp_entry(&entry, NOW), None, "{log}"); + } + let mut no_payload = parse(GCP_ACTIVITY); + no_payload.remove("protoPayload"); + assert_eq!(gcp_entry(&no_payload, NOW), None); + } + + #[test] + fn an_oversized_payload_is_not_kept_and_the_row_still_says_what_happened() { + let mut entry = parse(GCP_ACTIVITY); + entry["protoPayload"] + .as_object_mut() + .unwrap() + .insert("request".into(), json!("x".repeat(RAW_LIMIT_BYTES))); + let event = gcp_entry(&entry, NOW).expect("an Admin Activity entry"); + let raw: JsonObject = serde_json::from_str(&event.raw).expect("raw is JSON"); + + assert_eq!(raw["truncated"], true); + assert!(event.raw.len() < 100); + assert_eq!(event.action, "google.iam.admin.v1.CreateServiceAccountKey"); + } + + // Every column is the sender's to fill, and the table is every org's. + #[test] + fn a_row_keeps_no_more_of_a_field_than_its_column_takes() { + let long = "é".repeat(4000); + let mut entry = parse(GCP_ACTIVITY); + entry.insert("insertId".into(), json!(long)); + let payload = entry["protoPayload"].as_object_mut().unwrap(); + payload.insert("methodName".into(), json!(long)); + payload.insert("resourceName".into(), json!(long)); + payload.insert("status".into(), json!({ "code": 9, "message": long })); + let row = row(&gcp_entry(&entry, NOW).expect("an Admin Activity entry")); + let bytes = |column: &str| row[column].as_str().map_or(0, str::len); + + assert_eq!(bytes("Action"), NAME_LIMIT_BYTES); + assert_eq!(bytes("TargetName"), TEXT_LIMIT_BYTES); + assert_eq!(bytes("OutcomeReason"), TEXT_LIMIT_BYTES); + // An id is hashed rather than cut: it is part of the row's key. + assert_eq!(bytes("ProviderEventId"), 32); + assert!(bytes("Raw") <= RAW_LIMIT_BYTES); + } + + #[test] + fn two_long_ids_that_share_a_prefix_stay_two_events() { + let id = |tail: &str| format!("{}{tail}", "a".repeat(NAME_LIMIT_BYTES)); + let key = |id: String| { + let event = AuditEvent { + provider: "vercel", + provider_event_id: id, + ..AuditEvent::default() + }; + row(&event)["Id"].clone() + }; + + assert_ne!(key(id("1")), key(id("2"))); + } + + // The table is partitioned by the month an event happened and drops rows by + // age: a sender's own clock must not pick the partition or the expiry. + #[test] + fn a_time_the_table_could_not_hold_is_replaced_by_the_time_of_receipt() { + let at = |timestamp: i64| { + let mut event = parse(VERCEL_EVENT); + event.insert("timestamp".into(), json!(timestamp)); + vercel_event(&event, NOW).map(|event| event.occurred_at_ms) + }; + let day = 24 * 60 * 60 * 1000; + + assert_eq!(at(NOW - 399 * day), Some(NOW - 399 * day)); + assert_eq!(at(NOW + day / 2), Some(NOW + day / 2)); + assert_eq!(at(NOW - 401 * day), Some(NOW)); + assert_eq!(at(NOW + 2 * day), Some(NOW)); + // Seconds where milliseconds belong: 1970, which would expire on arrival. + assert_eq!(at(NOW / 1000), Some(NOW)); + } + + #[test] + fn a_vercel_action_is_filed_by_the_words_in_its_name() { + for (action, category) in [ + ("team.member.role.updated", "permission"), + ("team.member.added", "membership"), + ("access-group.created", "permission"), + ("project.env-var.created", "credential"), + ("project.environment.created", "resource"), + ("password_protection.enabled", "configuration"), + ("token.created", "credential"), + ("user.login", "authentication"), + ("drain-created", "configuration"), + ("deployment.deleted", "resource"), + ] { + assert_eq!(vercel_category(action), category, "{action}"); + } + } + + #[test] + fn a_vercel_event_points_at_what_its_payload_names() { + let event = parse( + r#"{"id":"evt_3","action":"drain-created","payload":{"drainId":"drn_1","projectId":"prj_1","name":"Maple logs"}}"#, + ); + let event = vercel_event(&event, NOW).expect("an event with an action"); + + assert_eq!( + ( + event.target_type.as_str(), + event.target_id.as_str(), + event.target_name.as_str() + ), + ("drain", "drn_1", "Maple logs") + ); + } + + #[test] + fn a_google_call_is_filed_by_its_service_and_the_call_itself() { + for (service, method, category) in [ + ( + "storage.googleapis.com", + "storage.setIamPermissions", + "permission", + ), + ( + "iam.googleapis.com", + "google.iam.admin.v1.CreateRole", + "permission", + ), + ( + "iam.googleapis.com", + "google.iam.admin.v1.CreateServiceAccount", + "resource", + ), + ( + "cloudresourcemanager.googleapis.com", + "SetOrgPolicy", + "configuration", + ), + ( + "logging.googleapis.com", + "google.logging.v2.ConfigServiceV2.DeleteSink", + "configuration", + ), + ( + "secretmanager.googleapis.com", + "google.cloud.secretmanager.v1.SecretManagerService.DeleteSecret", + "credential", + ), + ( + "compute.googleapis.com", + "v1.compute.instances.insert", + "resource", + ), + ] { + assert_eq!(gcp_category(service, method), category, "{method}"); + } + } + + // A cluster's controllers write to the Kubernetes API all day, and Google + // files every write under Admin Activity. + #[test] + fn a_kubernetes_api_write_stays_a_log() { + let mut entry = parse(GCP_ACTIVITY); + let payload = entry["protoPayload"].as_object_mut().unwrap(); + payload.insert("serviceName".into(), json!("k8s.io")); + payload.insert("methodName".into(), json!("io.k8s.core.v1.pods.create")); + + assert_eq!(gcp_entry(&entry, NOW), None); + } + + #[test] + fn a_federated_workload_is_a_service_named_by_its_subject() { + let subject = "principal://iam.googleapis.com/projects/1/locations/global/workloadIdentityPools/ci/subject/repo"; + let mut entry = parse(GCP_ACTIVITY); + entry["protoPayload"].as_object_mut().unwrap().insert( + "authenticationInfo".into(), + json!({ "principalSubject": subject }), + ); + let event = gcp_entry(&entry, NOW).expect("an Admin Activity entry"); + + assert_eq!( + ( + event.actor_type, + event.actor_id.as_str(), + event.actor_email.as_str() + ), + ("service", subject, "") + ); + } +} diff --git a/apps/ingest/src/lib.rs b/apps/ingest/src/lib.rs index a2395a69f5..0ab28003d3 100644 --- a/apps/ingest/src/lib.rs +++ b/apps/ingest/src/lib.rs @@ -1,4 +1,5 @@ pub mod ai_session; +pub mod audit_trail; pub mod aws; pub mod aws_metric_stream; pub mod clickhouse_insert_mappings; diff --git a/apps/ingest/src/main.rs b/apps/ingest/src/main.rs index 64ae6185f1..e3006dfddd 100644 --- a/apps/ingest/src/main.rs +++ b/apps/ingest/src/main.rs @@ -46,6 +46,7 @@ use flate2::write::GzEncoder; use flate2::Compression; use hmac::{Hmac, Mac}; use maple_ingest::ai_session; +use maple_ingest::audit_trail; use maple_ingest::aws::CredentialsProvider as AwsCredentialsProvider; use maple_ingest::aws_metric_stream; use maple_ingest::clickhouse_insert_mappings::SCHEMA_VERSION as CLICKHOUSE_SCHEMA_VERSION; @@ -205,6 +206,9 @@ struct AppConfig { org_routing_cache_ttl_secs: u64, /// `INGEST_RULES_ENABLED` (default on): apply customer drop/redact rules. Off reads nothing and costs nothing. ingest_rules_enabled: bool, + /// `INGEST_AUDIT_TRAIL_ENABLED` (default off): keep provider audit events in + /// the audit trail instead of as logs. See `stores_audit_trail`. + audit_trail_enabled: bool, /// `INGEST_RULES_CACHE_TTL_SECS`: how long an org's compiled rules are reused. ingest_rules_cache_ttl_secs: u64, /// Ceiling on the total decompressed rrweb payload a single replay session @@ -303,6 +307,12 @@ enum KeyStoreBackend { } impl AppConfig { + /// Whether provider audit events go to the audit trail. Off, or on a gateway + /// that only forwards and so has no such table, they stay logs. + fn stores_audit_trail(&self) -> bool { + self.audit_trail_enabled && self.write_mode.uses_tinybird() + } + #[expect( clippy::too_many_lines, reason = "one branch per environment variable, read in the order the deployment docs list \ @@ -628,6 +638,14 @@ impl AppConfig { std::env::var("INGEST_RULES_ENABLED").ok(), true, )?; + // Off by default. Turn it on once the table exists and every running task + // is a build that reads the audit trail's WAL frames: an older build that + // recovers one drops the rest of that segment. + let audit_trail_enabled = parse_bool( + "INGEST_AUDIT_TRAIL_ENABLED", + std::env::var("INGEST_AUDIT_TRAIL_ENABLED").ok(), + false, + )?; let ingest_rules_cache_ttl_secs = parse_u64( "INGEST_RULES_CACHE_TTL_SECS", std::env::var("INGEST_RULES_CACHE_TTL_SECS").ok(), @@ -789,6 +807,7 @@ impl AppConfig { ingest_key_cache_ttl_secs, org_routing_cache_ttl_secs, ingest_rules_enabled, + audit_trail_enabled, ingest_rules_cache_ttl_secs, replay_max_session_bytes, replay_blob_store, @@ -921,6 +940,8 @@ enum SourceClaim { struct VerifiedClaims { active: Vec, paused: Vec, + /// The store could not be asked: a claim in neither list may still be the org's. + unresolved: bool, } fn source_claims_cache(ttl: Duration) -> Cache<(String, String), SourceClaim> { @@ -1730,6 +1751,8 @@ enum GcpPushOutcome { SourcePaused, /// The org's ingest rules need more scanning than one entry may use; redelivery would too. RulesScanBudget, + /// An audit log entry, stored in the audit trail instead of the logs. + AuditEvent, } #[derive(Clone, Copy)] @@ -4103,9 +4126,295 @@ async fn handle_vercel_event_drain( let Some(drain) = EventDrain::from_path(&drain) else { return StatusCode::NOT_FOUND.into_response(); }; + if drain == EventDrain::AuditLog && state.config.stores_audit_trail() { + return handle_vercel_audit_trail(state, headers, body).await; + } handle_signal(state, headers, body, Ingress::VercelEventDrain(drain)).await } +/// Vercel's Audit Log drain as rows of `audit_trail_events`. +async fn handle_vercel_audit_trail( + state: Arc, + headers: HeaderMap, + body: Bytes, +) -> Response { + metrics::request_started(); + let _guard = InFlightGuard; + let span = tracing::info_span!( + "ingest_vercel_audit_trail", + otel.name = "POST /v1/vercel/audit-log", + otel.kind = "server", + otel.status_code = tracing::field::Empty, + otel.status_description = tracing::field::Empty, + "maple.ingest.reject_reason" = tracing::field::Empty, + "http.request.method" = "POST", + "http.route" = "/v1/vercel/audit-log", + "http.request.body.size" = body.len(), + "http.response.status_code" = tracing::field::Empty, + "error.type" = tracing::field::Empty, + "maple.signal" = "audit_trail", + "maple.org_id" = tracing::field::Empty, + "maple.audit_trail.events" = tracing::field::Empty, + "maple.audit_trail.skipped" = tracing::field::Empty, + ); + let span_handle = span.clone(); + let start = Instant::now(); + let result = handle_vercel_audit_trail_inner(&state, &headers, &body) + .instrument(span) + .await; + let duration = start.elapsed().as_secs_f64(); + let signal = TelemetrySignal::AuditTrail.as_str(); + match result { + Ok(count) => { + span_handle.record("http.response.status_code", 200u16); + span_handle.record("otel.status_code", "Ok"); + metrics::request_completed(signal, "ok", "none", duration); + (StatusCode::OK, axum::Json(AcceptedBody { accepted: count })).into_response() + } + Err(error) => { + let status = error.status.as_u16(); + span_handle.record("http.response.status_code", status); + span_handle.record("error.type", error.error_kind()); + // A drain Vercel keeps retrying has to move an error rate, like any other ingest route. + metrics::request_completed(signal, "error", error.error_kind(), duration); + record_rejection_reason( + &span_handle, + status, + error.error_kind(), + error.message.as_str(), + ); + error.into_response() + } + } +} + +/// Audit deliveries one org may make each second, across the fleet. Well above +/// what the people of a team or a cloud organization do; past it the sender is +/// told to retry, so a burst is delayed and not lost. +const AUDIT_TRAIL_VERCEL_ORG_RPS: u32 = 10; +const AUDIT_TRAIL_GCP_ORG_RPS: u32 = 100; + +/// The most a Vercel audit delivery may be once inflated. A batch of audit +/// events is kilobytes; the telemetry ceiling is for bodies that are metered. +const AUDIT_TRAIL_MAX_DECODED_BYTES: usize = 4 * 1024 * 1024; + +/// Refuses an org's audit delivery past `rps`. The rows are not metered, so +/// this is what bounds how many a sender can store. +async fn audit_trail_rate_gate( + state: &AppState, + org_id: &str, + provider: &'static str, + rps: u32, +) -> Result<(), ApiError> { + let key = format!("audit_trail:{provider}:{org_id}"); + if state.key_rate_limiter.try_acquire(&key, rps).await { + return Ok(()); + } + metrics::org_throttled(org_id, "audit_trail_rate"); + warn!(org_id, provider, "Audit event rate limit exceeded"); + Err(ApiError::too_many_requests( + "Audit event rate limit exceeded", + )) +} + +async fn handle_vercel_audit_trail_inner( + state: &AppState, + headers: &HeaderMap, + body: &Bytes, +) -> Result { + let Some(resolved_key) = resolve_replay_key(state, headers, &[SourceSignal::Logs]).await? + else { + return Ok(0); + }; + let org_id = resolved_key.org_id.clone(); + Span::current().record("maple.org_id", org_id.as_str()); + + // The drain Maple creates sends the org's private key and names the Vercel + // connection's source. Anything else posting here could write the org an + // audit event of its own invention: a source token, the public key (it + // ships in browser bundles), or a private key naming no integration source. + let unknown_sender = || { + ApiError::unauthorized( + "Audit events are accepted only from the drain Maple creates on the Vercel team", + ) + }; + if !matches!(resolved_key.key_type, IngestKeyType::Private) + || !resolved_key.accepts_source_claims() + { + return Err(unknown_sender()); + } + let claim = headers + .get(SOURCE_ID_HEADER) + .and_then(|value| value.to_str().ok()) + .map(str::trim) + .filter(|claim| !claim.is_empty() && claim.len() <= MAX_PUBLIC_ID_LEN) + .ok_or_else(unknown_sender)?; + let verified = state + .resolver + .verify_source_claims(&org_id, vec![claim.to_owned()]) + .await; + // Paused, the delivery is acked and dropped like the source's logs are. + if verified.paused.iter().any(|paused| paused == claim) { + metrics::source_paused_drop(&org_id, TelemetrySignal::AuditTrail.as_str()); + Span::current().record("maple.ingest.reject_reason", "source_paused"); + return Ok(0); + } + if !verified.active.iter().any(|active| active == claim) { + // The store being down says nothing about the sender: Vercel retries a 503. + if verified.unresolved { + return Err(ApiError::service_unavailable( + "Ingest authentication unavailable", + )); + } + return Err(unknown_sender()); + } + + audit_trail_rate_gate(state, &org_id, "vercel", AUDIT_TRAIL_VERCEL_ORG_RPS).await?; + let _org_inflight_permit = + state + .org_inflight_limiter + .try_acquire(&org_id) + .ok_or_else(|| { + warn!(org_id = %org_id, "Per-org in-flight ingest limit exceeded"); + ApiError::too_many_requests("Per-org ingest limit exceeded") + })?; + let pipeline = native_rows_pipeline_for( + state, + ExportDestination::Tinybird, + "Audit event storage is not configured", + )?; + + let gzip = match headers + .get(CONTENT_ENCODING) + .and_then(|value| value.to_str().ok()) + .map(|value| value.trim().to_ascii_lowercase()) + .as_deref() + { + None | Some("" | "identity") => false, + Some("gzip") => true, + Some(_) => { + return Err(ApiError::unsupported_media_type( + "Unsupported content-encoding", + )) + } + }; + let (rows, skipped) = + decode_vercel_audit_drain(state, body.clone(), gzip, org_id.clone()).await?; + Span::current().record("maple.audit_trail.events", rows.len()); + Span::current().record("maple.audit_trail.skipped", skipped); + if skipped > 0 { + warn!(org_id = %org_id, skipped, "Vercel audit drain events without an action were skipped"); + } + let count = store_audit_trail_rows(pipeline, &org_id, rows).await?; + state.resolver.note_source_use(&resolved_key, false); + Ok(count) +} + +/// A Vercel audit delivery as rows, and how many of its events were skipped. +/// Inflated and parsed on the blocking pool inside the process-wide decode +/// memory budget, like an OTLP body: a small gzip can inflate into a body that +/// parses into gigabytes. +async fn decode_vercel_audit_drain( + state: &AppState, + body: Bytes, + gzip: bool, + org_id: String, +) -> Result<(Vec>, usize), ApiError> { + let mut reservation = state.decode_memory.reservation(); + let heap_budget = state.config.max_decode_heap_bytes; + let max_decoded = state + .config + .max_decoded_body_bytes + .min(AUDIT_TRAIL_MAX_DECODED_BYTES); + tokio::task::spawn_blocking(move || { + let busy = || { + ApiError::tagged( + StatusCode::SERVICE_UNAVAILABLE, + &INGEST_DECODE_BUSY, + "Ingest gateway is busy decoding other requests; retry shortly", + ) + }; + let too_large = || ApiError::payload_too_large("Audit delivery too large"); + if body.len() > max_decoded { + return Err(too_large()); + } + if !reservation.try_add(body.len() as u64) { + return Err(busy()); + } + let payload = if gzip { + let invalid = || ApiError::bad_request("Invalid gzip body"); + let declared = decode_budget::gzip_declared_len(&body).ok_or_else(invalid)?; + let declared = usize::try_from(declared).unwrap_or(usize::MAX); + if declared > max_decoded { + return Err(too_large()); + } + if !reservation.try_add(declared as u64) { + return Err(busy()); + } + let inflated = gunzip_declared(&body, declared).map_err(|error| match error { + GunzipError::Invalid(_) => invalid(), + GunzipError::TooLarge => too_large(), + })?; + drop(body); + Bytes::from(inflated) + } else { + body + }; + let estimate = decode_budget::json_cost(&payload, heap_budget) + .map_err(|decode_budget::OverBudget| over_decode_budget(heap_budget))?; + if !reservation.try_add(estimate) { + return Err(busy()); + } + let parsed = vercel_logs::parse(&payload).map_err(|e| { + ApiError::bad_request(format!("Invalid Vercel Audit Log drain payload: {e}")) + })?; + let now_ms = audit_trail::now_ms(); + let rows: Vec> = parsed + .entries + .iter() + .filter_map(|event| audit_trail::vercel_event(event, now_ms)) + .map(|event| event.to_row(&org_id, now_ms)) + .collect(); + let skipped = parsed.entries.len() - rows.len() + parsed.malformed_lines; + Ok((rows, skipped)) + }) + .await + .unwrap_or_else(|join_error| { + error!(error = %join_error, "Audit delivery decode task failed"); + Err(ApiError::service_unavailable( + "Ingest gateway failed to decode the request", + )) + }) +} + +/// Audit rows to the managed warehouse: the table is pinned there, whatever +/// warehouse the org reads its telemetry from. Not metered: an audit event is +/// evidence, not usage. +async fn store_audit_trail_rows( + pipeline: &TelemetryPipeline, + org_id: &str, + rows: Vec>, +) -> Result { + if rows.is_empty() { + return Ok(0); + } + let count = rows.len(); + pipeline + .accept_rows_to( + org_id, + audit_trail::DATASOURCE.to_owned(), + rows, + TelemetrySignal::AuditTrail, + ExportDestination::Tinybird, + ) + .await + .map_err(|e| { + warn!(org_id = %org_id, error = %e, "audit trail enqueue rejected"); + api_error_from_pipeline(&e) + })?; + Ok(count) +} + // Session replay ingest /// Running total of decompressed rrweb bytes per in-flight replay session, used @@ -6182,6 +6491,18 @@ async fn handle_gcp_logpush( span_handle.record("maple.ingest.reject_reason", "source_paused"); metrics::request_completed("logs", "ok", "none", duration); } + GcpPushOutcome::AuditEvent => { + span_handle.record("otel.status_code", "Ok"); + span_handle.record("maple.ingest.item_count", 1); + span_handle.record("maple.signal", TelemetrySignal::AuditTrail.as_str()); + metrics::request_completed( + TelemetrySignal::AuditTrail.as_str(), + "ok", + "none", + duration, + ); + metrics::gcp_entry(); + } GcpPushOutcome::RulesScanBudget => { // Acked, but the entry is gone: an `Error`, like a decode failure. let kind = INGEST_RULES_SCAN_BUDGET.error_kind; @@ -6376,6 +6697,37 @@ async fn handle_gcp_logpush_inner( return Ok((StatusCode::OK.into_response(), GcpPushOutcome::NotALogEntry)); }; + // An Admin Activity or Policy Denied entry is an audit event, not a log: + // it goes to the audit trail, unmetered, ahead of the plan check on logs + // and of the org's ingest rules, which are about what it keeps of its logs. + if state.config.stores_audit_trail() { + let now_ms = audit_trail::now_ms(); + if let Some(event) = audit_trail::gcp_entry(&entry, now_ms) { + // Refused, Pub/Sub redelivers with backoff: delayed, not lost. + audit_trail_rate_gate(state, org_id, "gcp", AUDIT_TRAIL_GCP_ORG_RPS) + .await + .map_err(|error| (error, "throttle"))?; + let pipeline = native_rows_pipeline_for( + state, + ExportDestination::Tinybird, + "Audit event storage is not configured", + ) + .map_err(|error| (error, "forward"))?; + if let Err(error) = + store_audit_trail_rows(pipeline, org_id, vec![event.to_row(org_id, now_ms)]).await + { + state + .gcp_resolver + .record_health(connector_id, Some(gcp_logging::ENTRY_NOT_STORED)) + .await; + return Err((error, "forward")); + } + state.gcp_resolver.record_health(connector_id, None).await; + state.resolver.note_source_use(&resolved.key, false); + return Ok((StatusCode::OK.into_response(), GcpPushOutcome::AuditEvent)); + } + } + if let Some(error) = entitlement_rejection(state, org_id, Signal::Logs.path()).await { state .gcp_resolver @@ -8419,6 +8771,7 @@ impl IngestKeyResolver { org_id, error, "Source claim lookup failed; replacing the claims" ); + verified.unresolved = true; return verified; } }; @@ -12400,6 +12753,7 @@ mod tests { ingest_key_cache_ttl_secs: 60, org_routing_cache_ttl_secs: 5, ingest_rules_enabled: false, + audit_trail_enabled: false, ingest_rules_cache_ttl_secs: 30, replay_max_session_bytes: 1024 * 1024 * 1024, replay_blob_store: None, @@ -12952,6 +13306,298 @@ mod tests { let _ = std::fs::remove_dir_all(&queue_dir); } + const VERCEL_AUDIT_DELIVERY: &str = concat!( + r#"{"id":"evt_1","teamId":"team_1","action":"project.created","timestamp":1700000000000,"actor":{"type":"user","id":"usr_1"}}"#, + "\n", + r#"{"id":"evt_2","teamId":"team_1","timestamp":1700000000000}"#, + "\n", + ); + + /// The Vercel connection's source in the fixtures, as its uuid and as the + /// public id a drain names in `x-maple-source-id`. + const AUDIT_SOURCE_UUID: &str = "0f8e6c1a-2b3d-4e5f-8a9b-0c1d2e3f4a5b"; + const AUDIT_SOURCE_ID: &str = "src_aXunrkWtzvXSkw9Z2smcft"; + + /// What the fake Tinybird is sent: `(datasource, NDJSON rows)`. + type TinybirdImports = tokio::sync::mpsc::UnboundedReceiver<(String, String)>; + + async fn fake_tinybird_import( + State(tx): State>, + Query(query): Query>, + body: Bytes, + ) -> StatusCode { + let rows = gunzip_bounded(&body, usize::MAX).unwrap_or_else(|_| body.to_vec()); + drop(tx.send(( + query.get("name").cloned().unwrap_or_default(), + String::from_utf8(rows).expect("Tinybird should receive UTF-8 NDJSON"), + ))); + StatusCode::OK + } + + /// Turns the audit trail on, as the hosted gateway has it, with a pipeline + /// that writes to a fake Tinybird the test can read. + async fn store_audit_trail( + state: &mut AppState, + queue_dir: &std::path::Path, + ) -> TinybirdImports { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) + .await + .unwrap(); + let endpoint = format!("http://{}", listener.local_addr().unwrap()); + let app = Router::new() + .route("/v0/events", post(fake_tinybird_import)) + .with_state(tx); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + let pipeline = TelemetryPipeline::new( + TinybirdConfig { + endpoint, + token: "token".to_owned(), + ..test_tinybird_config(queue_dir.join("audit-trail")) + }, + Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .unwrap(), + ) + .await + .expect("the audit trail pipeline should start"); + state.config.write_mode = WriteMode::Tinybird; + state.config.audit_trail_enabled = true; + state.telemetry_pipeline = Some(pipeline); + rx + } + + /// An org with a private key and a Vercel connection whose source is + /// `AUDIT_SOURCE_ID`, on a gateway that stores the audit trail. + async fn audit_trail_test_state( + raw_key: &str, + org_id: &str, + queue_dir: PathBuf, + ) -> (AppState, Arc, TinybirdImports) { + let store = Arc::new(FakeKeyStore::default()); + store.insert_private( + raw_key, + KeyRow { + org_id: org_id.to_owned(), + self_managed: false, + clickhouse_ready: false, + }, + ); + store.insert_source_row(org_id, AUDIT_SOURCE_UUID, "integration"); + let mut state = test_app_state( + Arc::clone(&store), + queue_dir.clone(), + "http://127.0.0.1:1".to_owned(), + Duration::from_secs(30), + ) + .await; + let imports = store_audit_trail(&mut state, &queue_dir).await; + (state, store, imports) + } + + /// The headers of the drain Maple creates: the key and the connection's source. + fn vercel_drain_headers(raw_key: &str, source_id: &str) -> HeaderMap { + let mut headers = bearer_headers(raw_key); + headers.insert(SOURCE_ID_HEADER, source_id.parse().unwrap()); + headers + } + + async fn vercel_audit_delivery( + state: &AppState, + headers: &HeaderMap, + ) -> Result { + handle_vercel_audit_trail_inner( + state, + headers, + &Bytes::from_static(VERCEL_AUDIT_DELIVERY.as_bytes()), + ) + .await + } + + async fn nothing_reaches_the_audit_trail(imports: &mut TinybirdImports) -> bool { + tokio::time::timeout(Duration::from_millis(200), imports.recv()) + .await + .is_err() + } + + #[tokio::test] + async fn vercel_audit_drain_stores_each_event_that_names_an_action() { + let queue_dir = unique_main_test_dir("vercel-audit-trail"); + let (state, _store, mut imports) = + audit_trail_test_state("maple_sk_test_audit", "org_audit", queue_dir.clone()).await; + + let accepted = vercel_audit_delivery( + &state, + &vercel_drain_headers("maple_sk_test_audit", AUDIT_SOURCE_ID), + ) + .await + .expect("the drain's delivery should be accepted"); + + // The second event names no action: there is nothing to say happened. + assert_eq!(accepted, 1); + let (datasource, rows) = tokio::time::timeout(Duration::from_secs(2), imports.recv()) + .await + .expect("the event should reach Tinybird") + .expect("the Tinybird channel should stay open"); + assert_eq!(datasource, "audit_trail_events"); + let row: serde_json::Value = serde_json::from_str(rows.trim()).expect("one NDJSON row"); + assert_eq!( + ( + row["OrgId"].as_str(), + row["ProviderEventId"].as_str(), + row["Action"].as_str() + ), + (Some("org_audit"), Some("evt_1"), Some("project.created")) + ); + drop(std::fs::remove_dir_all(&queue_dir)); + } + + // The drain Maple creates sends the private key and names the connection's + // source. Any other sender could write the org an audit event it made up. + #[tokio::test] + async fn vercel_audit_drain_refuses_a_sender_that_is_not_the_orgs_drain() { + let queue_dir = unique_main_test_dir("vercel-audit-trail-senders"); + let (state, store, mut imports) = + audit_trail_test_state("maple_sk_test_senders", "org_senders", queue_dir.clone()).await; + // The public key ships in browser bundles. + let public_hash = hash_ingest_key("maple_pk_test_senders", "test-hmac-key").unwrap(); + store.keys.lock().unwrap().insert( + (public_hash, "public_key_hash"), + KeyRow { + org_id: "org_senders".to_owned(), + self_managed: false, + clickhouse_ready: false, + }, + ); + // A log shipper's token: allowed to send logs, and nothing else. + let token = "maple_src_audit_senders_0000000000000000000000"; + store.insert_source( + token, + source_row( + "org_senders", + source_config(Some(r#"["logs"]"#), None, "[]"), + ), + ); + // Another org's integration source, named by this org's key. + let foreign = "5b8f2c1e-7a55-4c0e-9a3b-0d2c7e5b9a11"; + store.insert_source_row("org_other", foreign, "integration"); + let foreign_id = sources::public_source_id(foreign).expect("a uuid has a public id"); + + for (sender, headers) in [ + ( + "the public key", + vercel_drain_headers("maple_pk_test_senders", AUDIT_SOURCE_ID), + ), + ( + "a source token", + vercel_drain_headers(token, AUDIT_SOURCE_ID), + ), + ( + "the private key naming no source", + bearer_headers("maple_sk_test_senders"), + ), + ( + "the private key naming another org's source", + vercel_drain_headers("maple_sk_test_senders", &foreign_id), + ), + ] { + let refused = vercel_audit_delivery(&state, &headers) + .await + .expect_err(sender); + assert_eq!(refused.status, StatusCode::UNAUTHORIZED, "{sender}"); + } + assert!(nothing_reaches_the_audit_trail(&mut imports).await); + drop(std::fs::remove_dir_all(&queue_dir)); + } + + #[tokio::test] + async fn vercel_audit_drain_acks_and_drops_while_the_integration_is_paused() { + let queue_dir = unique_main_test_dir("vercel-audit-trail-paused"); + let (state, store, mut imports) = + audit_trail_test_state("maple_sk_test_paused", "org_paused", queue_dir.clone()).await; + store.pause_source(AUDIT_SOURCE_UUID, current_time_millis() - 1000, None); + + let accepted = vercel_audit_delivery( + &state, + &vercel_drain_headers("maple_sk_test_paused", AUDIT_SOURCE_ID), + ) + .await + .expect("a paused source's delivery is acked"); + + assert_eq!(accepted, 0); + assert!(nothing_reaches_the_audit_trail(&mut imports).await); + drop(std::fs::remove_dir_all(&queue_dir)); + } + + // A store that cannot be asked has not said the sender is unknown: the drain + // must read as retryable, not as misconfigured. + #[tokio::test] + async fn vercel_audit_drain_asks_for_a_retry_while_its_source_cannot_be_checked() { + let queue_dir = unique_main_test_dir("vercel-audit-trail-store-down"); + let (state, store, mut imports) = + audit_trail_test_state("maple_sk_test_down", "org_down", queue_dir.clone()).await; + let headers = vercel_drain_headers("maple_sk_test_down", AUDIT_SOURCE_ID); + store.source_errors.store(true, Ordering::Relaxed); + + let refused = vercel_audit_delivery(&state, &headers) + .await + .expect_err("an unchecked source must not be stored"); + assert_eq!(refused.status, StatusCode::SERVICE_UNAVAILABLE); + assert!(nothing_reaches_the_audit_trail(&mut imports).await); + + // The store back, the same delivery is taken. + store.source_errors.store(false, Ordering::Relaxed); + assert_eq!(vercel_audit_delivery(&state, &headers).await.ok(), Some(1)); + drop(std::fs::remove_dir_all(&queue_dir)); + } + + // A few hundred bytes of gzip can inflate into a body that parses into + // gigabytes; an audit delivery has no reason to be large. + #[tokio::test] + async fn vercel_audit_drain_refuses_a_delivery_that_inflates_past_its_limit() { + let queue_dir = unique_main_test_dir("vercel-audit-trail-bomb"); + let (state, _store, mut imports) = + audit_trail_test_state("maple_sk_test_bomb", "org_bomb", queue_dir.clone()).await; + let mut headers = vercel_drain_headers("maple_sk_test_bomb", AUDIT_SOURCE_ID); + headers.insert(CONTENT_ENCODING, "gzip".parse().unwrap()); + let body = gzip_bytes("[],".repeat(AUDIT_TRAIL_MAX_DECODED_BYTES / 2)); + + let refused = handle_vercel_audit_trail_inner(&state, &headers, &body) + .await + .expect_err("a delivery past the limit must be refused"); + + assert_eq!(refused.status, StatusCode::PAYLOAD_TOO_LARGE); + assert!(nothing_reaches_the_audit_trail(&mut imports).await); + drop(std::fs::remove_dir_all(&queue_dir)); + } + + // Audit rows are not metered, so the rate is what bounds a sender. + #[tokio::test] + async fn an_orgs_audit_deliveries_are_refused_past_their_rate() { + let queue_dir = unique_main_test_dir("audit-trail-rate"); + let (state, _store, _imports) = + audit_trail_test_state("maple_sk_test_rate", "org_rate", queue_dir.clone()).await; + + assert!(audit_trail_rate_gate(&state, "org_rate", "gcp", 1) + .await + .is_ok()); + let refused = audit_trail_rate_gate(&state, "org_rate", "gcp", 1) + .await + .expect_err("a second delivery inside the second is past a rate of one"); + assert_eq!(refused.status, StatusCode::TOO_MANY_REQUESTS); + // Another org, and the same org's other provider, have their own allowance. + assert!(audit_trail_rate_gate(&state, "org_other", "gcp", 1) + .await + .is_ok()); + assert!(audit_trail_rate_gate(&state, "org_rate", "vercel", 1) + .await + .is_ok()); + drop(std::fs::remove_dir_all(&queue_dir)); + } + #[tokio::test] async fn custom_session_events_are_metered_as_product_events_but_gated_on_browser_sessions() { let queue_dir = unique_main_test_dir("session-events-custom-meter"); @@ -14540,6 +15186,65 @@ mod tests { drop(std::fs::remove_dir_all(queue_dir)); } + const GCP_AUDIT_ENTRY: &str = r#"{"logName":"projects/acme-prod/logs/cloudaudit.googleapis.com%2Factivity","insertId":"abc123","timestamp":"2026-10-09T10:00:00Z","resource":{"type":"service_account","labels":{"project_id":"acme-prod"}},"protoPayload":{"serviceName":"iam.googleapis.com","methodName":"google.iam.admin.v1.CreateServiceAccountKey","authenticationInfo":{"principalEmail":"ada@acme.test"}}}"#; + + // An Admin Activity entry names a person and what they changed. It goes to + // the audit trail, which only admins read, and never to the org's logs. + #[tokio::test] + async fn gcp_logpush_stores_an_audit_entry_in_the_audit_trail_and_not_as_a_log() { + let (ch_url, mut ch_rx) = spawn_fake_clickhouse().await; + let (mut state, store, queue_dir) = gcp_logpush_state("gcp-logpush-audit", &ch_url).await; + let mut imports = store_audit_trail( + Arc::get_mut(&mut state).expect("the state is not shared yet"), + &queue_dir, + ) + .await; + + let accepted = gcp_push(&state, "gcp_conn_1", Some("gcp-secret"), GCP_AUDIT_ENTRY).await; + + assert_eq!(accepted.status(), StatusCode::OK); + assert_eq!(store.connector_successes.load(Ordering::Relaxed), 1); + let (datasource, rows) = tokio::time::timeout(Duration::from_secs(2), imports.recv()) + .await + .expect("the entry should reach Tinybird") + .expect("the Tinybird channel should stay open"); + assert_eq!(datasource, "audit_trail_events"); + let row: serde_json::Value = serde_json::from_str(rows.trim()).expect("one NDJSON row"); + assert_eq!( + ( + row["OrgId"].as_str(), + row["ProviderEventId"].as_str(), + row["ActorEmail"].as_str() + ), + (Some("org_gcp"), Some("abc123"), Some("ada@acme.test")) + ); + assert!( + tokio::time::timeout(Duration::from_millis(200), ch_rx.recv()) + .await + .is_err(), + "an audit entry must not be written to the org's logs" + ); + drop(std::fs::remove_dir_all(queue_dir)); + } + + // A gateway that only forwards has no audit trail table, and one where the + // trail is not turned on does not write to it: there the entry stays a log. + #[tokio::test] + async fn gcp_logpush_keeps_an_audit_entry_as_a_log_where_the_gateway_only_forwards() { + let (ch_url, mut ch_rx) = spawn_fake_clickhouse().await; + let (state, _store, queue_dir) = gcp_logpush_state("gcp-logpush-audit-fwd", &ch_url).await; + + let accepted = gcp_push(&state, "gcp_conn_1", Some("gcp-secret"), GCP_AUDIT_ENTRY).await; + + assert_eq!(accepted.status(), StatusCode::OK); + let stored = tokio::time::timeout(Duration::from_secs(2), ch_rx.recv()) + .await + .expect("the entry should reach the org's ClickHouse") + .expect("ClickHouse channel should stay open"); + assert!(stored.query.starts_with("INSERT INTO logs")); + drop(std::fs::remove_dir_all(queue_dir)); + } + #[tokio::test] async fn gcp_logpush_acks_an_entry_past_the_rule_scan_budget() { let (ch_url, mut ch_rx) = spawn_fake_clickhouse().await; diff --git a/apps/ingest/src/telemetry.rs b/apps/ingest/src/telemetry.rs index 66bf1c50fa..81babf62db 100644 --- a/apps/ingest/src/telemetry.rs +++ b/apps/ingest/src/telemetry.rs @@ -533,6 +533,9 @@ pub enum TelemetrySignal { /// `POST /v1/events` (NDJSON, gateway-written). Lands in `product_events` /// next to the browser rows the `session_events` MV materializes there. ProductEvents, + /// Audit events from provider feeds (a Vercel audit drain, GCP audit logs), + /// gateway-written into `audit_trail_events`, never as OTLP logs. + AuditTrail, } impl TelemetrySignal { @@ -549,6 +552,7 @@ impl TelemetrySignal { Self::SessionReplays => "session_replays", Self::SessionEvents => "session_events", Self::ProductEvents => "product_events", + Self::AuditTrail => "audit_trail", } } } @@ -3068,6 +3072,7 @@ fn signal_tag(signal: TelemetrySignal) -> u8 { TelemetrySignal::SessionReplays => 4, TelemetrySignal::SessionEvents => 5, TelemetrySignal::ProductEvents => 6, + TelemetrySignal::AuditTrail => 7, } } @@ -3079,6 +3084,7 @@ fn signal_from_tag(tag: u8) -> Option { 4 => Some(TelemetrySignal::SessionReplays), 5 => Some(TelemetrySignal::SessionEvents), 6 => Some(TelemetrySignal::ProductEvents), + 7 => Some(TelemetrySignal::AuditTrail), _ => None, } } @@ -5713,6 +5719,71 @@ mod tests { drop(std::fs::remove_dir_all(queue_dir)); } + // Audit rows are built by the gateway, not derived from OTLP: they have to + // reach the warehouse under their own datasource, never as a log. + #[tokio::test] + async fn audit_trail_rows_reach_tinybird_under_their_own_datasource() { + let (tx, mut rx) = mpsc::unbounded_channel(); + let listener = tokio::net::TcpListener::bind(("127.0.0.1", 0)) + .await + .unwrap(); + let addr = listener.local_addr().unwrap(); + let app = Router::new() + .route("/v0/events", post(fake_tinybird_import)) + .with_state(tx); + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let queue_dir = unique_test_dir("fake-tinybird-audit-trail"); + let mut cfg = test_cfg(); + cfg.endpoint = format!("http://{addr}"); + cfg.queue_dir = queue_dir.clone(); + cfg.wal_shards = 1; + cfg.batch_max_wait = Duration::from_millis(1); + let pipeline = TelemetryPipeline::new( + cfg, + Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .unwrap(), + ) + .await + .unwrap(); + + let event: serde_json::Map = serde_json::from_str( + r#"{"id":"evt_1","teamId":"team_1","action":"project.created","timestamp":1700000000000}"#, + ) + .unwrap(); + let row = crate::audit_trail::vercel_event(&event, 1_700_000_060_000) + .expect("an event with an action") + .to_row("org_e2e", 1_700_000_060_000); + let stats = pipeline + .accept_rows_to( + "org_e2e", + crate::audit_trail::DATASOURCE.to_owned(), + vec![row], + TelemetrySignal::AuditTrail, + ExportDestination::Tinybird, + ) + .await + .unwrap(); + assert_eq!(stats.rows, 1); + + let import = tokio::time::timeout(Duration::from_secs(2), rx.recv()) + .await + .expect("fake Tinybird should receive an import") + .expect("fake Tinybird channel should stay open"); + assert_eq!(import.datasource, "audit_trail_events"); + let stored: Value = serde_json::from_str(import.body.trim()).unwrap(); + assert_eq!(stored["OrgId"], "org_e2e"); + assert_eq!(stored["Provider"], "vercel"); + assert_eq!(stored["Action"], "project.created"); + + wait_for_export_drain(queue_dir.clone(), 0, ExportDestination::Tinybird).await; + drop(std::fs::remove_dir_all(queue_dir)); + } + #[tokio::test] #[expect( clippy::too_many_lines, @@ -6457,6 +6528,7 @@ mod tests { assert_eq!(TelemetrySignal::SessionReplays.as_str(), "session_replays"); assert_eq!(TelemetrySignal::SessionEvents.as_str(), "session_events"); assert_eq!(TelemetrySignal::ProductEvents.as_str(), "product_events"); + assert_eq!(TelemetrySignal::AuditTrail.as_str(), "audit_trail"); } #[test] @@ -6470,6 +6542,7 @@ mod tests { TelemetrySignal::SessionReplays, TelemetrySignal::SessionEvents, TelemetrySignal::ProductEvents, + TelemetrySignal::AuditTrail, ] { assert_eq!(signal_from_tag(signal_tag(signal)), Some(signal)); } diff --git a/apps/ios/Maple/Components/AlertFormatting.swift b/apps/ios/Maple/Components/AlertFormatting.swift index 3e63f24353..1d977b569e 100644 --- a/apps/ios/Maple/Components/AlertFormatting.swift +++ b/apps/ios/Maple/Components/AlertFormatting.swift @@ -56,6 +56,8 @@ struct SignalDisplay: Codable { case .sloBurnRate: self.init(label: "SLO burn rate", unit: .burnRate) // An anomaly rule: the value is a severity code, shown as Warning or Critical. case .anomaly: self.init(label: "Anomaly", unit: .anomalySeverity) + // An audit log detection's hidden rule: the value is how many matching events arrived. + case .audit: self.init(label: "Audit log", unit: .count) } } diff --git a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json index cdb95c5afb..ba4063fd46 100644 --- a/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json +++ b/apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json @@ -7224,7 +7224,8 @@ "raw_query", "uptime", "slo_burn_rate", - "anomaly" + "anomaly", + "audit" ], "examples": [ "error_rate" diff --git a/apps/landing/src/content/docs/integrations/gcp.md b/apps/landing/src/content/docs/integrations/gcp.md index 1cfa691a86..5f9cdd3ac1 100644 --- a/apps/landing/src/content/docs/integrations/gcp.md +++ b/apps/landing/src/content/docs/integrations/gcp.md @@ -76,7 +76,7 @@ Turning something off takes effect in Maple at once. Google Cloud keeps the sink ### Log filter -Platform, request and audit logs are forwarded. The default filter leaves out what costs a lot and says little: +Platform, request and audit logs are forwarded. Admin Activity and Policy Denied audit entries do not land in logs: they go to the audit log in the Security section, which only organization admins can read. System Event audit entries stay in logs, and so do Kubernetes API writes (`k8s.io`), which are mostly a cluster's own controllers. On a self-hosted gateway that forwards telemetry to a collector, every audit entry stays a log. The default filter leaves out what costs a lot and says little: | Left out | Reason | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | diff --git a/apps/landing/src/content/docs/integrations/vercel.md b/apps/landing/src/content/docs/integrations/vercel.md index 35d219d618..9149058fee 100644 --- a/apps/landing/src/content/docs/integrations/vercel.md +++ b/apps/landing/src/content/docs/integrations/vercel.md @@ -69,13 +69,20 @@ Request logs carry `http.request.method`, `url.path` (and `url.query` when the r Vercel sends deployment traces with the `vercel.projectId` and `vercel.deploymentId` resource attributes. Its platform spans come in two services: `vercel.edge-network` for routing and caching, and `vercel.serverless-runtime` for functions. Maple moves each function invocation (`Invoke Function`) and its outbound calls to a service named after the project, the same name its logs use, so every project is its own service on the service map with its own dependencies. Cold-start steps (`Start VM`, `Spawn Node.js`, `Init User Code`) and `waitUntil` stay under `vercel.serverless-runtime`. Every function span keeps `maple_vercel.platform_service = vercel.serverless-runtime` as a resource attribute. -### Speed Insights, Web Analytics and audit log +### Speed Insights and Web Analytics -These drains arrive as logs, each under its own `service.name`: `vercel-speed-insights`, `vercel-web-analytics` and `vercel-audit-log`. The resource carries `cloud.provider` set to `vercel`, the team id and, where Vercel sends them, the project id, deployment id and environment. +These drains arrive as logs, each under its own `service.name`: `vercel-speed-insights` and `vercel-web-analytics`. The resource carries `cloud.provider` set to `vercel`, the team id and, where Vercel sends them, the project id, deployment id and environment. - **Speed Insights**: one log per measurement, such as `LCP 2.41s /dashboard`. The metric is in `vercel.speed_insights.metric` (`LCP`, `INP`, `CLS`, `FCP`, `TTFB` or `FID`) and the value in `vercel.speed_insights.value`, in milliseconds (CLS is a score). Each log also carries `url.path`, `vercel.route`, the visitor's country, region and city (`geo.*`), browser and OS (`user_agent.*`) and device. - **Web Analytics**: one log per page view or custom event, such as `pageview /pricing`. It carries `vercel.web_analytics.event.type` and `.event.name`, the event data as JSON in `vercel.web_analytics.event.data`, `url.path`, `url.query`, the referrer, and the same visitor fields as Speed Insights. -- **Audit log**: one log per change, such as `drain-created by Jane Doe`. It carries `vercel.audit.action`, the actor (`vercel.audit.actor.type`, `.id`, `.name`, `.email`), `client.address`, `user_agent.original`, and the event's details as JSON in `vercel.audit.payload`. + +### Audit log + +Audit events do not arrive as logs. They go to the audit log in the Security section, which only organization admins can read: an audit event names a person, their email and their IP address, and every member can read logs. + +Each event shows who acted (name, email and id), the action as Vercel names it, such as `drain-created`, what it was done to, the time, the IP address and user agent, and the event's payload as Vercel sent it. A payload over 32 KB is not kept. Maple accepts audit events only from the drain it creates on your team; a drain you add by hand on Vercel is refused. + +A self-hosted gateway that forwards telemetry to a collector has no audit log table. There, audit events arrive as logs under the `vercel-audit-log` service. ## AI Gateway @@ -105,13 +112,14 @@ The trace drain forwards these spans to Maple. You do not need to configure an e - **Infrastructure → Vercel → Overview**: requests, error rate, cache hit rate and cold-start rate across the team, AI Gateway spend by project, recent builds and failed builds, and, when those drains are on, Web Vitals, page views and audit log events. - **Infrastructure → Vercel → Projects**: each project with its last build, requests, 5xx rate, cache hit rate, cold-start rate and latency. Click a project for its own page. - **A project's page** has a tab for each part: - - **Traffic**: requests by status class, top paths with their cache hit rate, and top errors. - - **Functions**: invocations, cold-start rate and cold-start init time per route. - - **Dependencies**: the hosts its functions call, with calls, error rate and latency, and a link to the project on the service map. - - **Builds**: recent deployments with build time, state and why a failed build stopped. **Build logs** opens the logs explorer on that build's output. - - **AI Gateway**, **Speed Insights** (p75 Web Vitals by route and device) and **Web Analytics** (page views, devices, top pages, referrers, countries and custom events). + - **Traffic**: requests by status class, top paths with their cache hit rate, and top errors. + - **Functions**: invocations, cold-start rate and cold-start init time per route. + - **Dependencies**: the hosts its functions call, with calls, error rate and latency, and a link to the project on the service map. + - **Builds**: recent deployments with build time, state and why a failed build stopped. **Build logs** opens the logs explorer on that build's output. + - **AI Gateway**, **Speed Insights** (p75 Web Vitals by route and device) and **Web Analytics** (page views, devices, top pages, referrers, countries and custom events). + + A section whose drain is off says so and links to the setting that turns it on. - A section whose drain is off says so and links to the setting that turns it on. - **Service map**: each project with its outbound calls, from its function spans, and an edge from `vercel.edge-network` to every project it invokes. A project's Infrastructure tab links to its Vercel project page. - [Logs](/docs/explore/logs): filter by `service.name` (the project name) or `vercel.project.id`. Speed Insights, Web Analytics and audit log events are under their own `service.name`. - [Traces](/docs/explore/traces): your `@vercel/otel` service, the deployment traces, and `ai-gateway`. diff --git a/apps/landing/src/content/docs/reference/webhooks.md b/apps/landing/src/content/docs/reference/webhooks.md index 9ca45fc2c0..e6413c6e65 100644 --- a/apps/landing/src/content/docs/reference/webhooks.md +++ b/apps/landing/src/content/docs/reference/webhooks.md @@ -64,29 +64,29 @@ Respond with any `2xx` within **15 seconds**. The response body is ignored. } ``` -| Field | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `eventType` | Same as the `x-maple-event-type` header | -| `incidentId` | The incident this event belongs to. `null` for tests. | -| `incidentStatus` | `open` or `resolved` | -| `dedupeKey` | Stable for one incident across `trigger`, `renotify` and `resolve`. Use it to thread events together. | -| `rule.signalType` | `error_rate`, `p95_latency`, `p99_latency`, `apdex`, `throughput`, `builder_query`, `raw_query` or `anomaly`; `uptime` for an uptime monitor and `slo_burn_rate` for an SLO burn-rate alert | -| `rule.severity` | `warning` or `critical` | -| `rule.groupKey` | The group that breached, such as a service name. `__total__` for an ungrouped rule. | -| `rule.comparator` | `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `between` or `not_between` | -| `rule.threshold` | The threshold, or the lower bound for `between` and `not_between` | -| `rule.thresholdUpper` | The upper bound for `between` and `not_between`, otherwise `null` | -| `rule.windowMinutes` | The evaluation window | -| `rule.severities` | The severities of the rule's threshold tiers, such as `["warning", "critical"]`. May be absent | -| `peakSeverity` | The highest severity the incident reached, or `null` | -| `observed.value` | The value the check measured | -| `observed.sampleCount` | How many data points the value was computed from | -| `template` | The rule's rendered notification template, or `null` | -| `chart` | `url` of a chart image and/or a `sparkline`, or `null` | -| `linkUrl` | The incident in the Maple dashboard | -| `chatUrl` | Opens the alert rule in Maple, or the alerts list for a test | -| `sentAt` | ISO-8601 time the event was first generated. Retries keep the original value. | -| `event` | The same event as a [CloudEvents 1.0](https://cloudevents.io) envelope, for routers that speak CloudEvents | +| Field | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `eventType` | Same as the `x-maple-event-type` header | +| `incidentId` | The incident this event belongs to. `null` for tests. | +| `incidentStatus` | `open` or `resolved` | +| `dedupeKey` | Stable for one incident across `trigger`, `renotify` and `resolve`. Use it to thread events together. | +| `rule.signalType` | `error_rate`, `p95_latency`, `p99_latency`, `apdex`, `throughput`, `builder_query`, `raw_query` or `anomaly`; `uptime` for an uptime monitor, `slo_burn_rate` for an SLO burn-rate alert and `audit` for an audit log detection | +| `rule.severity` | `warning` or `critical` | +| `rule.groupKey` | The group that breached, such as a service name. `__total__` for an ungrouped rule. | +| `rule.comparator` | `gt`, `gte`, `lt`, `lte`, `eq`, `neq`, `between` or `not_between` | +| `rule.threshold` | The threshold, or the lower bound for `between` and `not_between` | +| `rule.thresholdUpper` | The upper bound for `between` and `not_between`, otherwise `null` | +| `rule.windowMinutes` | The evaluation window | +| `rule.severities` | The severities of the rule's threshold tiers, such as `["warning", "critical"]`. May be absent | +| `peakSeverity` | The highest severity the incident reached, or `null` | +| `observed.value` | The value the check measured | +| `observed.sampleCount` | How many data points the value was computed from | +| `template` | The rule's rendered notification template, or `null` | +| `chart` | `url` of a chart image and/or a `sparkline`, or `null` | +| `linkUrl` | The incident in the Maple dashboard | +| `chatUrl` | Opens the alert rule in Maple, or the alerts list for a test | +| `sentAt` | ISO-8601 time the event was first generated. Retries keep the original value. | +| `event` | The same event as a [CloudEvents 1.0](https://cloudevents.io) envelope, for routers that speak CloudEvents | The body is identical for every destination's webhook, regardless of the rule's notification template. Treat unknown fields as additive: new fields can appear without notice. diff --git a/apps/web/src/lab/infra-lab/update.ts b/apps/web/src/lab/infra-lab/update.ts index 299a462ea3..893170e0c3 100644 --- a/apps/web/src/lab/infra-lab/update.ts +++ b/apps/web/src/lab/infra-lab/update.ts @@ -22,7 +22,6 @@ import { VERCEL_API_AI_GATEWAY, VERCEL_API_DEPLOYMENTS, VERCEL_API_DETAIL, - VERCEL_AUDIT_LOG, VERCEL_BREAKDOWN, VERCEL_BUCKETS, VERCEL_DEPENDENCIES, @@ -106,7 +105,6 @@ const vercelList = (flags: Flags): VercelList.Model => ({ deployments: answered(VERCEL_TEAM_DEPLOYMENTS), speedInsights: answered(VERCEL_SPEED_INSIGHTS), webAnalytics: answered(VERCEL_WEB_ANALYTICS), - auditLog: answered(VERCEL_AUDIT_LOG), }) const vercelProject = (flags: Flags): VercelProject.Model => ({ diff --git a/apps/web/src/lab/infra-lab/view.ts b/apps/web/src/lab/infra-lab/view.ts index a8e63e4b79..a3152c89c1 100644 --- a/apps/web/src/lab/infra-lab/view.ts +++ b/apps/web/src/lab/infra-lab/view.ts @@ -25,7 +25,6 @@ import * as VercelProject from "../../page/infra-integrations/vercel/project/mod import { projectPage } from "../../page/infra-integrations/vercel/project/view" import { projectRail } from "../../page/infra-integrations/vercel/project/view-rail" import { - auditLogSection, buildsSection, dependenciesSection, functionsSection, @@ -36,12 +35,13 @@ import { import { pickTimeSearch } from "../../page/kit/view-tabs" import { sectionHeading } from "../../page/kit/section-heading" import { sortRowsBy } from "../../page/kit/table-sort" +import { cn } from "@maple/ui/lib/utils" import { eyebrow } from "@maple/views/ui/eyebrow" +import { pageRightSidebarWidthStyles } from "@maple/views/ui/page-layout.styles" import { AsyncData } from "foldkit" import { toLoadFailure } from "@maple/views/data/load-failure" import { VERCEL_ALL_DRAINS_STATUS, - VERCEL_AUDIT_LOG, VERCEL_DEPENDENCIES, VERCEL_EMPTY_SPEED_INSIGHTS, VERCEL_EMPTY_WEB_ANALYTICS, @@ -184,7 +184,13 @@ type VercelInputs = Readonly<{ timeZone: string }> /** `VercelListPage` over the fixtures, in the view its search names. */ /** A page's main column beside its right rail, as `dashboardPage` lays them out at lg. */ const withRail = (h: HtmlBuilder, main: Html, rail: Html): Html => - h.div([h.Class("flex items-start gap-6")], [h.div([h.Class("min-w-0 flex-1")], [main]), rail]) + h.div( + [h.Class("flex items-start gap-6")], + [ + h.div([h.Class("min-w-0 flex-1")], [main]), + h.div([h.Class(cn("shrink-0", pageRightSidebarWidthStyles))], [rail]), + ], + ) const vercelListView = Submodel.defineView( (list, inputs, h) => { @@ -271,14 +277,6 @@ const vercelSections = (h: H, model: Model): Html => { compact: false, }), ), - ...variants("Audit log", (state) => - auditLogSection(h, { - read: sectionRead(state, VERCEL_AUDIT_LOG, []), - enabled: state !== "off", - clock, - projectName: nameOf, - }), - ), ]) } diff --git a/apps/web/src/lab/security-audit-log-fixture.ts b/apps/web/src/lab/security-audit-log-fixture.ts index 71345ba1cd..c8ddb6b941 100644 --- a/apps/web/src/lab/security-audit-log-fixture.ts +++ b/apps/web/src/lab/security-audit-log-fixture.ts @@ -7,6 +7,8 @@ import { Array as Arr, String as Str } from "effect" const MINUTE = 60_000 type Seed = Readonly<{ + provider?: string + account?: string minutesAgo: number action: string category: string @@ -40,6 +42,34 @@ const deployKey: AuditTrailEntry["actor"] = { const system: AuditTrailEntry["actor"] = { type: "system", id: null, name: null, email: null } const SEEDS: ReadonlyArray = [ + { + provider: "vercel", + account: "team_acme", + minutesAgo: 2, + action: "project.env-var.created", + category: "credential", + actor: { type: "user", id: "usr_9f2c", name: "Grace Hopper", email: "grace@acme.test" }, + originIp: "203.0.113.90", + raw: { id: "evt_1", action: "project.env-var.created", payload: { projectId: "prj_web" } }, + }, + { + provider: "gcp", + account: "acme-prod", + minutesAgo: 11, + action: "google.iam.admin.v1.CreateServiceAccountKey", + category: "credential", + outcome: "denied", + outcomeReason: "Permission iam.serviceAccountKeys.create denied", + actor: { + type: "service", + id: "ci@acme-prod.iam.gserviceaccount.com", + name: null, + email: "ci@acme-prod.iam.gserviceaccount.com", + }, + target: { type: "service_account", name: "projects/-/serviceAccounts/deploy@acme-prod" }, + originIp: "198.51.100.24", + raw: { insertId: "abc123", protoPayload: { serviceName: "iam.googleapis.com" } }, + }, { minutesAgo: 4, action: "alert_rule.updated", @@ -130,8 +160,8 @@ const eventOf = (seed: Seed, index: number, nowMs: number): AuditTrailEntry => { const occurredAt = nowMs - seed.minutesAgo * MINUTE return { id: Str.padStart(32, "0")(index.toString(16)), - provider: "maple", - account: null, + provider: seed.provider ?? "maple", + account: seed.account ?? null, providerEventId: `alog_4CzLmR1pTxWvYbNhQd82K${index}`, occurredAt, ingestedAt: occurredAt + 900, diff --git a/apps/web/src/lab/vercel-infra-fixtures.ts b/apps/web/src/lab/vercel-infra-fixtures.ts index fd2c11e40b..6708727e62 100644 --- a/apps/web/src/lab/vercel-infra-fixtures.ts +++ b/apps/web/src/lab/vercel-infra-fixtures.ts @@ -1,6 +1,6 @@ // Fixture rows for the /infra/vercel lab frames: four projects (one erroring, one with no traces, // one forwarded but silent), AI Gateway usage across three models, team builds, one project's -// detail, functions and dependencies, and Speed Insights, Web Analytics and audit log readings. +// detail, functions and dependencies, and Speed Insights and Web Analytics readings. import { VercelAiGatewayResponse, @@ -14,7 +14,6 @@ import { VercelProjectStatus, VercelTeam, type VercelAiGatewayBucket, - type VercelAuditEvent, type VercelInfraDependency, type VercelInfraFunction, type VercelInfraFunctionBucket, @@ -418,7 +417,7 @@ export const VERCEL_API_DEPLOYMENTS: ReadonlyArray = [ deployment("api-v1", "READY", "main", "Products: cache the price table for 60s", 3 * 24 * HOUR), ] -/** Every drain on: the overview's Speed Insights, Web Analytics and audit log sections read. */ +/** Every drain on: the Speed Insights and Web Analytics sections read, the rail points at the audit log. */ export const VERCEL_ALL_DRAINS_STATUS = new VercelIntegrationStatus({ ...VERCEL_STATUS, speedInsightsEnabled: true, @@ -556,27 +555,3 @@ export const VERCEL_EMPTY_WEB_ANALYTICS = new VercelWebAnalyticsResponse({ countries: [], events: [], }) - -export const VERCEL_AUDIT_LOG: ReadonlyArray = [ - { - timestamp: new Date(NOW - 12 * MINUTE).toISOString(), - action: "project.env-variable.updated", - actor: "Rosa Moreau", - actorType: "user", - projectId: "prj_api", - }, - { - timestamp: new Date(NOW - 3 * HOUR).toISOString(), - action: "drain-created", - actor: "Maple", - actorType: "app", - projectId: "", - }, - { - timestamp: new Date(NOW - 26 * HOUR).toISOString(), - action: "team.member.added", - actor: "Sam Okafor", - actorType: "user", - projectId: "", - }, -] diff --git a/apps/web/src/page/alerts/incident/incident.scene.test.ts b/apps/web/src/page/alerts/incident/incident.scene.test.ts index 56366edec5..e5516a078e 100644 --- a/apps/web/src/page/alerts/incident/incident.scene.test.ts +++ b/apps/web/src/page/alerts/incident/incident.scene.test.ts @@ -36,6 +36,7 @@ import { MountChart } from "@maple/views/charts/chart-mounts" import { ResolveThemeColors, ThemeMessage } from "@maple/views/charts/theme-colors" import { toLoadFailure } from "@maple/views/data/load-failure" import { PageOutMessage, type PageContext } from "../../contract" +import { AppRoute, hrefOf } from "../../../route" import { sloHref, slosTabHref } from "../shared/slo-commands" import { checkoutSlo } from "../shared/slo-test-fixtures" import { monitorHref, uptimeTabHref } from "../../kit/uptime/commands" @@ -289,6 +290,63 @@ describe("alert incident", () => { ) }) + test("an audit detection incident asks for no rule preview, offers no rule edit, and links the audit log for an admin", () => { + const auditIncident = incidentRow({ + rule_name: "Members added", + signal_type: "audit", + comparator: "gte", + threshold: 3, + last_observed_value: 4, + group_key: "__total__", + }) + const loaded = Arr.reduce( + [ + Message.ReadClock({ nowMs: NOW }), + Message.UpdatedIncidents({ orgId: ORG, rows: [auditIncident] }), + ], + init(route(), context).model, + (model, message) => step(model, message).model, + ) + const withRule = step( + loaded, + Message.UpdatedRules({ + orgId: ORG, + rows: [ruleRow(2, { signal_type: "audit", name: "Members added" })], + }), + ) + expect(names(withRule.commands)).not.toContain("FetchAlertIncidentPreview") + expect(names(withRule.commands)).not.toContain("FetchAlertIncidentChecks") + scene( + { update: pageUpdate, view }, + given(withRule.model), + themeResolved(), + expectView(role("heading", { name: "Audit log 4" })).toExist(), + expectView(text(/__total__/u)).not.toExist(), + expectView(role("link", { name: "Edit rule" })).not.toExist(), + // Until the session answers there is no link, and no word yet on who may open the log. + expectView(role("link", { name: "Open audit log" })).not.toExist(), + expectView(text(/only org admins can open it/u)).not.toExist(), + ) + const seenBy = (roles: ReadonlyArray) => + step(withRule.model, Message.CompletedFetchSession({ grants: Policy.grantsOf(roles) })).model + scene( + { update: pageUpdate, view }, + given(seenBy(["org:member"])), + themeResolved(), + expectView(text(/only org admins can open it/u)).toExist(), + expectView(role("link", { name: "Open audit log" })).not.toExist(), + ) + scene( + { update: pageUpdate, view }, + given(seenBy(["org:admin"])), + themeResolved(), + expectView(role("link", { name: "Open audit log" })).toHaveAttr( + "href", + hrefOf(AppRoute.SecurityAuditLog({ search: {} })), + ), + ) + }) + test("an uptime incident links its monitor once known and keeps its rule's history, without a rule edit", () => { const uptimeIncident = incidentRow({ rule_id: MONITOR_RULE, diff --git a/apps/web/src/page/alerts/incident/view.ts b/apps/web/src/page/alerts/incident/view.ts index 282dda6f15..1b22a4306d 100644 --- a/apps/web/src/page/alerts/incident/view.ts +++ b/apps/web/src/page/alerts/incident/view.ts @@ -60,7 +60,12 @@ import { anomalyEvidence, type AnomalyEvidenceInputs } from "./view-anomaly" import { ANOMALY_CHART_TOKENS } from "../../kit/anomaly-evidence" import { timestamp } from "@maple/views/ui/timestamp" import { typography } from "@maple/views/ui/typography" -import { canManageAlerts, canRespondToIncidents, canRunInvestigations } from "../../kit/session-grants" +import { + canManageAlerts, + canReadAuditLog, + canRespondToIncidents, + canRunInvestigations, +} from "../../kit/session-grants" type H = HtmlBuilder @@ -275,6 +280,11 @@ export type IncidentBodyInputs = Readonly<{ escalation?: EscalationData /** An SLO or uptime incident's SLO or monitor page; its tab stands in until it is known. */ sourceHref?: string | null + /** + * Whether the viewer may open the audit log; null until the session answers. An audit + * incident links it only for someone who may: anyone else is sent home. + */ + canReadAuditLog?: boolean | null chartId: string chart: Chart.Model toChartMessage: (message: Chart.Message) => M @@ -283,23 +293,33 @@ export type IncidentBodyInputs = Readonly<{ anomaly?: AnomalyEvidenceInputs | null }> -/** A managed incident's note on what opened it, and a link to its SLO or monitor (its tab until known). */ +/** Where an audit detection's events are listed; only org admins can open it. */ +const auditLogHref = () => hrefOf(AppRoute.SecurityAuditLog({ search: {} })) + +/** + * A managed incident's note on what opened it, and a link to its SLO or monitor (its tab until + * known). With neither a link nor a fallback it is the note alone. + */ const sourceNote = ( h: HtmlBuilder, options: Readonly<{ text: string href: string | null label: string - fallback: Readonly<{ href: string; label: string }> + fallback?: Readonly<{ href: string; label: string }> }>, -): Html => - vstack(h, { gap: "3", align: "start" }, [ +): Html => { + const href = options.href ?? options.fallback?.href + return vstack(h, { gap: "3", align: "start" }, [ typography(h, { size: "sm", tone: "muted", as: "p" }, [options.text]), - linkButton(h, { size: "sm", variant: "outline", href: options.href ?? options.fallback.href }, [ - options.href === null ? options.fallback.label : options.label, - icon(h, "ArrowRightIcon"), - ]), + href === undefined + ? h.empty + : linkButton(h, { size: "sm", variant: "outline", href }, [ + options.href === null ? (options.fallback?.label ?? options.label) : options.label, + icon(h, "ArrowRightIcon"), + ]), ]) +} /** Signal chart, timeline and rail. */ export const incidentBody = (h: HtmlBuilder, inputs: IncidentBodyInputs): Html => { @@ -343,31 +363,44 @@ export const incidentBody = (h: HtmlBuilder, inputs: IncidentBodyInputs label: "Open SLO", fallback: { href: slosTabHref(), label: "Open SLOs" }, }) - : rule === null - ? typography(h, { size: "sm", tone: "muted", as: "p" }, [ - "The rule behind this incident was deleted, so its signal cannot be replayed.", - ]) - : ruleChart(h, { - chartId: inputs.chartId, - chart: inputs.chart, - toChartMessage: inputs.toChartMessage, - colors: inputs.colors, - timeZone: inputs.timeZone, - preview: inputs.preview, - checks: inputs.checks, - incidents: [incident], - threshold: incident.threshold, - thresholdUpper: incident.thresholdUpper, - comparator: incident.comparator, - signalType: incident.signalType, - window: { - min: inputs.range.start, - max: inputs.range.end, - }, - source: "preview", - isLoading: inputs.isPreviewLoading, - error: inputs.previewError, - }), + : incident.signalType === "audit" + ? sourceNote(h, { + text: `This incident follows an audit log detection: it opened when the matching events that arrived within the detection's window reached its count, and resolves when they fall back below it. ${inputs.canReadAuditLog === false ? "The audit log lists the events; only org admins can open it." : "The audit log lists the events."}`, + href: + inputs.canReadAuditLog === true + ? auditLogHref() + : null, + label: "Open audit log", + }) + : rule === null + ? typography( + h, + { size: "sm", tone: "muted", as: "p" }, + [ + "The rule behind this incident was deleted, so its signal cannot be replayed.", + ], + ) + : ruleChart(h, { + chartId: inputs.chartId, + chart: inputs.chart, + toChartMessage: inputs.toChartMessage, + colors: inputs.colors, + timeZone: inputs.timeZone, + preview: inputs.preview, + checks: inputs.checks, + incidents: [incident], + threshold: incident.threshold, + thresholdUpper: incident.thresholdUpper, + comparator: incident.comparator, + signalType: incident.signalType, + window: { + min: inputs.range.start, + max: inputs.range.end, + }, + source: "preview", + isLoading: inputs.isPreviewLoading, + error: inputs.previewError, + }), ], ), incidentTimeline(h, { @@ -420,6 +453,8 @@ const body = (h: H, model: Model, view: IncidentView, timeZone: string, now: num incident, rule, sourceHref: Option.getOrNull(incident.signalType === "uptime" ? monitorLink : sloLink), + // No grants yet is no answer yet: an admin must not read that the log is not theirs. + canReadAuditLog: model.grants.length === 0 ? null : canReadAuditLog(model), escalation: model.escalation.data, ruleIncidents: view.ruleIncidents, states: ruleStatesOf(model, incident.ruleId), diff --git a/apps/web/src/page/infra-integrations/gcp/integration/gcp-configure-copy.ts b/apps/web/src/page/infra-integrations/gcp/integration/gcp-configure-copy.ts index b5a556f14a..ff068f26ef 100644 --- a/apps/web/src/page/infra-integrations/gcp/integration/gcp-configure-copy.ts +++ b/apps/web/src/page/infra-integrations/gcp/integration/gcp-configure-copy.ts @@ -60,7 +60,8 @@ export const CAPABILITIES = { logs: { title: "Log forwarding", flag: "logs_enabled", - description: "Request, audit and managed-service logs, through a Log Router sink and Pub/Sub.", + description: + "Request and managed-service logs, through a Log Router sink and Pub/Sub. Admin activity goes to the Security audit log.", run: { on: "Log sink and Pub/Sub topic", off: "Removes the log sink" }, effects: { "starts-after-script": "Starts once it's set up in Google Cloud", diff --git a/apps/web/src/page/infra-integrations/gcp/integration/view-flow-gcp.ts b/apps/web/src/page/infra-integrations/gcp/integration/view-flow-gcp.ts index 853243c670..c6020dffd4 100644 --- a/apps/web/src/page/infra-integrations/gcp/integration/view-flow-gcp.ts +++ b/apps/web/src/page/infra-integrations/gcp/integration/view-flow-gcp.ts @@ -188,7 +188,7 @@ const chooseRail = (h: H): Html => { icon: "UnorderedListIcon", title: "Google Cloud's logs", - detail: "Request, audit and managed-service logs", + detail: "Request and managed-service logs, and admin activity for the audit log", }, { icon: "ServerIcon", diff --git a/apps/web/src/page/infra-integrations/gcp/integration/view-gcp-configure-filter.ts b/apps/web/src/page/infra-integrations/gcp/integration/view-gcp-configure-filter.ts index 7229cca8f4..7a65b3610f 100644 --- a/apps/web/src/page/infra-integrations/gcp/integration/view-gcp-configure-filter.ts +++ b/apps/web/src/page/infra-integrations/gcp/integration/view-gcp-configure-filter.ts @@ -134,7 +134,7 @@ export const filterField = ( h, icon(h, "CheckIcon", { size: 14, className: "text-severity-info" }), "Platform logs", - "Requests, audit, deploys, crashes, scaling", + "Requests, deploys, crashes, scaling, and admin activity for the audit log", ), summaryRow( h, diff --git a/apps/web/src/page/infra-integrations/vercel/data.ts b/apps/web/src/page/infra-integrations/vercel/data.ts index 9607d94121..2f31cd3a3c 100644 --- a/apps/web/src/page/infra-integrations/vercel/data.ts +++ b/apps/web/src/page/infra-integrations/vercel/data.ts @@ -6,7 +6,6 @@ import type { } from "@maple/domain/http" import { getVercelAiGateway, - getVercelAuditLog, getVercelBreakdown, getVercelLatencyTimeseries, getVercelDependencies, @@ -199,13 +198,6 @@ export const loadVercelSpeedInsights = (input: WindowInput) => export const loadVercelWebAnalytics = (input: WindowInput) => getVercelWebAnalytics({ data: { ...input } }).pipe(Effect.mapError(toLoadFailure), Effect.result) -/** `vercelAuditLogResultAtom({ data: window })`. */ -export const loadVercelAuditLog = (window: TimeWindow) => - getVercelAuditLog({ data: { startTime: window.startTime, endTime: window.endTime } }).pipe( - Effect.mapBoth({ onSuccess: (response) => response.events, onFailure: toLoadFailure }), - Effect.result, - ) - export const DEPLOYMENTS_LIMIT = 20 /** `integrations.vercelDeployments`: the newest deployments of one project, or of the whole team. */ diff --git a/apps/web/src/page/infra-integrations/vercel/integration/vercel.scene.test.ts b/apps/web/src/page/infra-integrations/vercel/integration/vercel.scene.test.ts index 9e2fe1a837..4c41df33d1 100644 --- a/apps/web/src/page/infra-integrations/vercel/integration/vercel.scene.test.ts +++ b/apps/web/src/page/infra-integrations/vercel/integration/vercel.scene.test.ts @@ -337,6 +337,9 @@ describe("vercel card: connected", () => { expectView(text("Connected")).toExist(), expectView(within(role("row", { name: /Logs/ }), text("live"))).toExist(), expectView(within(role("row", { name: /Speed Insights/ }), text("off"))).toExist(), + expectView( + within(role("row", { name: /Audit log/ }), text(/Security audit log, org admins only/)), + ).toExist(), expectView(role("link", { name: "storefront" })).toHaveAttr("href", "/infra/vercel/prj_web"), expectView(role("link", { name: /Open Vercel dashboard/ })).toHaveAttr("href", "/infra/vercel"), ) @@ -527,16 +530,14 @@ describe("vercel card: configure", () => { ).toEqual(expect.objectContaining({ request: { aiGatewayEnabled: false } })) }) - test("turning on the audit log names its plan and saves only that switch at once", () => { + test("turning on the audit log names where its events show and its plan, and saves only that switch at once", () => { const opened = configured(connected()) scene( { update: step, view }, given(opened), mounted(), expectView( - text( - "Who changed what on the team, for the whole team whatever projects you pick. Enterprise plan only.", - ), + text(/Shown in the Security audit log to org admins\. Enterprise plan only\./), ).toExist(), ) const sent = step( diff --git a/apps/web/src/page/infra-integrations/vercel/integration/view-vercel-configure.ts b/apps/web/src/page/infra-integrations/vercel/integration/view-vercel-configure.ts index f6ee9f2597..3b353ea13a 100644 --- a/apps/web/src/page/infra-integrations/vercel/integration/view-vercel-configure.ts +++ b/apps/web/src/page/infra-integrations/vercel/integration/view-vercel-configure.ts @@ -348,7 +348,7 @@ const SIGNALS = [ title: "Audit log", drain: "Maple audit log", description: - "Who changed what on the team, for the whole team whatever projects you pick. Enterprise plan only.", + "Who changed what on the team, whatever projects you pick. Shown in the Security audit log to org admins. Enterprise plan only.", }, ] as const diff --git a/apps/web/src/page/infra-integrations/vercel/integration/view-vercel.ts b/apps/web/src/page/infra-integrations/vercel/integration/view-vercel.ts index 3ad8961deb..65a45bdaab 100644 --- a/apps/web/src/page/infra-integrations/vercel/integration/view-vercel.ts +++ b/apps/web/src/page/infra-integrations/vercel/integration/view-vercel.ts @@ -270,7 +270,7 @@ const SIGNALS = { auditLog: { title: "Audit log", carries: "Team actions: env vars, domains, members", - powers: "Team activity", + powers: "Security audit log, org admins only", }, } as const satisfies Record<(typeof VERCEL_DRAINS)[number]["key"], unknown> diff --git a/apps/web/src/page/infra-integrations/vercel/list/command.ts b/apps/web/src/page/infra-integrations/vercel/list/command.ts index d1df6f932a..050bc76f0f 100644 --- a/apps/web/src/page/infra-integrations/vercel/list/command.ts +++ b/apps/web/src/page/infra-integrations/vercel/list/command.ts @@ -4,7 +4,6 @@ import { linkedCursorStream } from "@maple/views/charts/linked-cursor" import { resolveWindow, TimeWindow } from "../../../../data/time-range" import { loadVercelAiGateway, - loadVercelAuditLog, loadVercelDeployments, loadVercelLatencySeries, loadVercelProjects, @@ -102,13 +101,6 @@ export const FetchWebAnalytics = Command.define("FetchVercelWebAnalytics", { ), }) -export const FetchAuditLog = Command.define("FetchVercelAuditLog", { - args: { key: Schema.String, window: TimeWindow }, - messages: [Message.CompletedFetchAuditLog], - execute: ({ key, window }) => - Effect.map(loadVercelAuditLog(window), (result) => Message.CompletedFetchAuditLog({ key, result })), -}) - export const CompleteReloadSpin = Command.define("CompleteVercelReloadSpin", { args: { version: Schema.Number }, messages: [Message.CompletedReloadSpin], diff --git a/apps/web/src/page/infra-integrations/vercel/list/model.ts b/apps/web/src/page/infra-integrations/vercel/list/model.ts index 959b226be8..4255065a37 100644 --- a/apps/web/src/page/infra-integrations/vercel/list/model.ts +++ b/apps/web/src/page/infra-integrations/vercel/list/model.ts @@ -1,6 +1,5 @@ import { VercelAiGatewayResponse, - VercelAuditEvent, VercelDeploymentSummary, VercelInfraLatencyBucket, VercelInfraProject, @@ -37,7 +36,6 @@ export const Model = Schema.Struct({ deployments: KeyedSchema(Schema.Array(VercelDeploymentSummary), LoadFailure), speedInsights: KeyedSchema(VercelSpeedInsightsResponse, LoadFailure), webAnalytics: KeyedSchema(VercelWebAnalyticsResponse, LoadFailure), - auditLog: KeyedSchema(Schema.Array(VercelAuditEvent), LoadFailure), charts: Schema.Record(Schema.String, Chart.Model), chartRuntime: ChartRuntime, themeColors: Schema.NullOr(ThemeColors), @@ -65,7 +63,6 @@ export const Message = defineMessageUnion({ CompletedFetchDeployments: result(Schema.Array(VercelDeploymentSummary)), CompletedFetchSpeedInsights: result(VercelSpeedInsightsResponse), CompletedFetchWebAnalytics: result(VercelWebAnalyticsResponse), - CompletedFetchAuditLog: result(Schema.Array(VercelAuditEvent)), CompletedLoadChartRuntime: {}, FailedLoadChartRuntime: { reason: Schema.String }, ResolvedThemeColors: { colors: ThemeColors }, @@ -80,7 +77,6 @@ export const Message = defineMessageUnion({ ClickedRetryDeployments: {}, ClickedRetrySpeedInsights: {}, ClickedRetryWebAnalytics: {}, - ClickedRetryAuditLog: {}, ChangedSearch: { value: Schema.String }, ClickedClearSearch: {}, /** A fleet band cell: its health filter on the projects table, or none to leave it. */ diff --git a/apps/web/src/page/infra-integrations/vercel/list/update.ts b/apps/web/src/page/infra-integrations/vercel/list/update.ts index 7a07f9792f..851d163e2e 100644 --- a/apps/web/src/page/infra-integrations/vercel/list/update.ts +++ b/apps/web/src/page/infra-integrations/vercel/list/update.ts @@ -16,7 +16,6 @@ import { DEFAULT_PRESET, DEPLOYMENTS_LIMIT, seriesInput } from "../data" import { CompleteReloadSpin, FetchAiGateway, - FetchAuditLog, FetchBuckets, FetchDeployments, FetchLatency, @@ -62,7 +61,6 @@ type Slot = | "deployments" | "speedInsights" | "webAnalytics" - | "auditLog" /** The reads the overview shows, once the status says connected: each only while its drain is on. */ const sync = ( @@ -96,7 +94,6 @@ const sync = ( const deploymentsKey = requestKey(org, "integrations.vercelDeployments", { limit: DEPLOYMENTS_LIMIT }) const speedKey = requestKey(org, "getVercelSpeedInsights", { data: window }) const webKey = requestKey(org, "getVercelWebAnalytics", { data: window }) - const auditKey = requestKey(org, "getVercelAuditLog", { data: window }) return { model: { ...model, @@ -125,9 +122,6 @@ const sync = ( webAnalytics: take("webAnalytics", status.webAnalyticsEnabled, model.webAnalytics, webKey, () => FetchWebAnalytics({ key: webKey, input: window }), ), - auditLog: take("auditLog", status.auditLogEnabled, model.auditLog, auditKey, () => - FetchAuditLog({ key: auditKey, window }), - ), }, commands, } @@ -154,7 +148,6 @@ export const init = (route: { readonly search: Search }, context: PageContext): deployments: Keyed.idle(), speedInsights: Keyed.idle(), webAnalytics: Keyed.idle(), - auditLog: Keyed.idle(), charts: {}, chartRuntime: { _tag: "Loading" }, themeColors: null, @@ -221,9 +214,6 @@ export const update = (model: Model, message: Message, context: PageContext): Re CompletedFetchWebAnalytics: ({ key, result }) => ({ model: { ...model, webAnalytics: Keyed.accept(model.webAnalytics, key, result) }, }), - CompletedFetchAuditLog: ({ key, result }) => ({ - model: { ...model, auditLog: Keyed.accept(model.auditLog, key, result) }, - }), CompletedLoadChartRuntime: () => ({ model: { ...model, chartRuntime: { _tag: "Ready" } } }), FailedLoadChartRuntime: ({ reason }) => ({ model: { ...model, chartRuntime: { _tag: "Failed", reason } }, @@ -239,7 +229,6 @@ export const update = (model: Model, message: Message, context: PageContext): Re ClickedRetryDeployments: () => sync(model, context, "Refresh", ["deployments"]), ClickedRetrySpeedInsights: () => sync(model, context, "Refresh", ["speedInsights"]), ClickedRetryWebAnalytics: () => sync(model, context, "Refresh", ["webAnalytics"]), - ClickedRetryAuditLog: () => sync(model, context, "Refresh", ["auditLog"]), ChangedSearch: ({ value }) => navigate(model, { ...model.search, q: value === "" ? undefined : value }), ClickedClearSearch: () => navigate(model, { ...model.search, q: undefined }), diff --git a/apps/web/src/page/infra-integrations/vercel/list/vercel.scene.test.ts b/apps/web/src/page/infra-integrations/vercel/list/vercel.scene.test.ts index 46afb70f54..71641b89d9 100644 --- a/apps/web/src/page/infra-integrations/vercel/list/vercel.scene.test.ts +++ b/apps/web/src/page/infra-integrations/vercel/list/vercel.scene.test.ts @@ -22,7 +22,6 @@ import { toLoadFailure } from "@maple/views/data/load-failure" import { VERCEL_AI_GATEWAY, VERCEL_ALL_DRAINS_STATUS, - VERCEL_AUDIT_LOG, VERCEL_BUCKETS, VERCEL_LATENCY_BUCKETS, VERCEL_PROJECTS, @@ -108,11 +107,6 @@ const loaded = ( key: model.webAnalytics.key ?? "", result: Result.succeed(VERCEL_WEB_ANALYTICS), }), - (model) => - Message.CompletedFetchAuditLog({ - key: model.auditLog.key ?? "", - result: Result.succeed(VERCEL_AUDIT_LOG), - }), ]) /** The commands the status answer starts, the window already resolved. */ @@ -169,17 +163,12 @@ describe("vercel overview", () => { const defaults = readsFor(VERCEL_STATUS) expect(defaults).not.toContain("FetchVercelSpeedInsights") expect(defaults).not.toContain("FetchVercelWebAnalytics") - expect(defaults).not.toContain("FetchVercelAuditLog") expect(readsFor(VERCEL_TRACES_ONLY_STATUS)).not.toContain("FetchVercelRequestSeries") expect( readsFor(new VercelIntegrationStatus({ ...VERCEL_STATUS, tracesEnabled: false })), ).not.toContain("FetchVercelLatencySeries") expect(readsFor(VERCEL_ALL_DRAINS_STATUS)).toEqual( - expect.arrayContaining([ - "FetchVercelSpeedInsights", - "FetchVercelWebAnalytics", - "FetchVercelAuditLog", - ]), + expect.arrayContaining(["FetchVercelSpeedInsights", "FetchVercelWebAnalytics"]), ) }) @@ -298,22 +287,41 @@ describe("vercel overview", () => { expectView(text(VERCEL_AI_GATEWAY.models[0]?.model ?? "")).toExist(), expectView(text(/Speed Insights is off/)).toExist(), expectView(text(/Web Analytics is off/)).toExist(), - expectView(text("Team activity")).toBeAbsent(), ) }) - test("with every drain on, the overview shows Web Vitals, page views and team activity", () => { + test("with every drain on, the overview shows Web Vitals and page views", () => { scene( { update: pageUpdate, view }, given(loaded(VERCEL_ALL_DRAINS_STATUS, VERCEL_PROJECTS)), themeResolved(), expectView(text("needs work")).toExist(), expectView(text("184.2K")).toExist(), - expectView(text("project.env-variable.updated")).toExist(), expectView(text(/Speed Insights is off/)).toBeAbsent(), ) }) + test("the audit log drain puts a link to the Security audit log in the rail, and nothing when it is off", () => { + scene( + { update: pageUpdate, view }, + given(loaded(VERCEL_ALL_DRAINS_STATUS, VERCEL_PROJECTS)), + themeResolved(), + expectView(text("Team activity")).toExist(), + expectView(text("Audit events from this team are in the Security audit log.")).toExist(), + expectView(role("link", { name: "Open the audit log" })).toHaveAttr( + "href", + "/security/audit-log?provider=vercel", + ), + ) + scene( + { update: pageUpdate, view }, + given(loaded(VERCEL_STATUS, VERCEL_PROJECTS)), + themeResolved(), + expectView(text("Team activity")).toBeAbsent(), + expectView(role("link", { name: "Open the audit log" })).toBeAbsent(), + ) + }) + test("a retired projects link lands on the one overview", () => { scene( { update: pageUpdate, view }, diff --git a/apps/web/src/page/infra-integrations/vercel/list/view-rail.ts b/apps/web/src/page/infra-integrations/vercel/list/view-rail.ts index 093b67ce19..f7103935ca 100644 --- a/apps/web/src/page/infra-integrations/vercel/list/view-rail.ts +++ b/apps/web/src/page/infra-integrations/vercel/list/view-rail.ts @@ -16,14 +16,14 @@ import { type Clock, dataOf } from "../view-parts" import type { Overview } from "./derive" import type { Message, Model } from "./model" -// `VercelOverviewRail`: the team, its drains, the deploys in the window and the team's activity. +// `VercelOverviewRail`: the team, its drains, the deploys in the window and where its audit events are. type H = HtmlBuilder const RAIL_DEPLOYS = 5 -const RAIL_EVENTS = 3 const settingsHref = () => hrefOf(AppRoute.Integrations({ search: { integration: "vercel" } })) +const auditLogHref = () => hrefOf(AppRoute.SecurityAuditLog({ search: { provider: "vercel" } })) const value = (h: H, text: string): Html => typography(h, { size: "xs", tone: "foreground", mono: true, truncate: true }, [text]) @@ -170,42 +170,18 @@ const deploysGroup = (h: H, model: Model, o: Overview, clock: Clock): Html | nul ) } -const activityGroup = (h: H, model: Model, o: Overview, clock: Clock): Html | null => { - const events = dataOf(model.auditLog.data) - if (!o.status.auditLogEnabled || events === null) return null - const newest = Arr.take( - Arr.sort( - events, - Order.mapInput(Order.flip(Order.String), (e: (typeof events)[number]) => e.timestamp), - ), - RAIL_EVENTS, - ) - return detailRailGroup( - h, - "Team activity", - newest.length === 0 - ? [typography(h, { size: "2xs", tone: "muted", as: "p" }, ["No team activity in this window"])] - : Arr.map(newest, (event) => - vstack(h, { gap: "0.5", className: "py-1" }, [ - typography(h, { size: "xs", tone: "foreground", mono: true, truncate: true }, [ - event.action, - ]), - hstack(h, { gap: "1" }, [ - typography(h, { size: "3xs", tone: "muted", truncate: true }, [ - `${event.actor} · ${event.projectId === "" ? "team" : o.projectName(event.projectId)} ·`, - ]), - relativeTime(h, { - value: event.timestamp, - ...clock, - variant: "short", - mono: true, - className: "shrink-0 text-3xs text-muted-foreground", - }), - ]), - ]), - ), - ) -} +// Audit events are admin-only, so they live in the Security audit log: the rail points there. +const activityGroup = (h: H, o: Overview): Html | null => + o.status.auditLogEnabled + ? detailRailGroup(h, "Team activity", [ + typography(h, { size: "2xs", tone: "muted", as: "p" }, [ + "Audit events from this team are in the Security audit log.", + ]), + typography(h, { size: "2xs", className: "self-start" }, [ + textLink(h, { href: auditLogHref() }, ["Open the audit log"]), + ]), + ]) + : null export const overviewRail = (h: H, model: Model, o: Overview, clock: Clock): Html => vstack( @@ -216,7 +192,7 @@ export const overviewRail = (h: H, model: Model, o: Overview, clock: Clock): Htm teamGroup(h, o, clock), drainsGroup(h, o), deploysGroup(h, model, o, clock), - activityGroup(h, model, o, clock), + activityGroup(h, o), ], (group): group is Html => group !== null, ), diff --git a/apps/web/src/page/infra-integrations/vercel/sections.ts b/apps/web/src/page/infra-integrations/vercel/sections.ts index 91d140dccb..94cab70dc5 100644 --- a/apps/web/src/page/infra-integrations/vercel/sections.ts +++ b/apps/web/src/page/infra-integrations/vercel/sections.ts @@ -1,5 +1,4 @@ import type { - VercelAuditEvent, VercelDeploymentSummary, VercelInfraDependency, VercelInfraFunction, @@ -912,93 +911,3 @@ export const webAnalyticsSection = ( ] }, }) - -// Audit log - -const AUDIT_LOG_OFF: DrainOff = { - lead: "The audit log is off.", - text: "Turn on the Audit Log drain to see who changed the team: members, domains, env vars and drains.", -} - -/** The team's newest audit log events. */ -export const auditLogSection = ( - h: HtmlBuilder, - options: Readonly<{ - read: SectionRead, M> - enabled: boolean - clock: Clock - projectName: (projectId: string) => string - }>, -): Html => - readSection(h, { - title: "Audit log", - hint: "newest first", - read: options.read, - off: options.enabled ? null : AUDIT_LOG_OFF, - failure: "Couldn't load the audit log", - loading: () => tableLoading(h, "Audit log"), - body: (events) => [ - dataTable( - h, - { ariaLabel: "Audit log" }, - [ - columnHead(h, { label: "Action", width: LEAD_COLUMN }), - columnHead(h, { label: "Actor", width: "w-[160px]" }), - columnHead(h, { label: "Project", width: "w-[140px]", hidden: "hidden md:flex" }), - columnHead(h, { label: "When", width: "w-[72px]", align: "right" }), - ], - events.length === 0 - ? [dataTableEmpty(h, ["No audit events in this window"])] - : Arr.map(events, (event, index) => - h.div( - [h.Key(`${event.timestamp}:${index}`), h.Class(ROW_CLASS)], - [ - typography( - h, - { - size: "xs", - tone: "foreground", - mono: true, - truncate: true, - className: LEAD_COLUMN, - }, - [event.action], - ), - typography( - h, - { size: "xs", tone: "soft", truncate: true, className: "w-[160px]" }, - [event.actor === "" ? event.actorType || EMPTY_VALUE : event.actor], - ), - typography( - h, - { - size: "xs", - tone: "muted", - mono: true, - truncate: true, - className: "hidden w-[140px] md:block", - }, - [ - event.projectId === "" - ? "team" - : options.projectName(event.projectId), - ], - ), - h.span( - [h.Class("w-[72px] text-right")], - [ - relativeTime(h, { - value: event.timestamp, - ...options.clock, - variant: "short", - mono: true, - className: typographyClass({ size: "2xs", tone: "muted" }), - }), - ], - ), - ], - ), - ), - ), - ], - }) diff --git a/apps/web/src/page/kit/session-grants.ts b/apps/web/src/page/kit/session-grants.ts index c11d2087a5..562f35a7c5 100644 --- a/apps/web/src/page/kit/session-grants.ts +++ b/apps/web/src/page/kit/session-grants.ts @@ -40,3 +40,6 @@ export const canRespondToIncidents = (model: Grants): boolean => Policy.has(mode /** The viewer may start, restart and resolve investigations (`investigations:run`). */ export const canRunInvestigations = (model: Grants): boolean => Policy.has(model.grants, "investigations:run") + +/** The viewer may open the audit log (`audit_log:read`); false until the session answers. */ +export const canReadAuditLog = (model: Grants): boolean => Policy.has(model.grants, "audit_log:read") diff --git a/apps/web/src/page/security/audit-log/audit-log.scene.test.ts b/apps/web/src/page/security/audit-log/audit-log.scene.test.ts index 872f89cabd..502fa30e70 100644 --- a/apps/web/src/page/security/audit-log/audit-log.scene.test.ts +++ b/apps/web/src/page/security/audit-log/audit-log.scene.test.ts @@ -1,7 +1,7 @@ import { OrgId } from "@maple/domain" import { AuditTrailForbiddenError, AuditTrailPersistenceError } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" -import { Result, Schema, Array as Arr, String as Str } from "effect" +import { Option, Result, Schema, Array as Arr, String as Str } from "effect" import { expect as expectView, given, role, scene, text, within, withViewInputs } from "foldkit/scene" import { describe, expect, test } from "vitest" import { toLoadFailure } from "@maple/views/data/load-failure" @@ -186,25 +186,55 @@ describe("security audit log", () => { ) }) - test("opening an event shows what was recorded about it, and closing hides it", () => { + /** The first loaded event from `provider`, opened. */ + const openedFrom = (provider: string) => { const model = loaded() - const first = eventsOf(model)?.[0] - const opened = pageUpdate(model, Message.ToggledEvent({ id: first?.id ?? "" })).model + const event = Arr.findFirst(eventsOf(model) ?? [], (candidate) => candidate.provider === provider) + const id = Option.match(event, { onNone: () => "", onSome: (found) => found.id }) + return { + event: Option.getOrUndefined(event), + id, + opened: pageUpdate(model, Message.ToggledEvent({ id })).model, + } + } + + test("opening an event shows what was recorded about it, and closing hides it", () => { + const { event, id, opened } = openedFrom("maple") scene( { update: pageUpdate, view }, given(opened), expectView(text("Payload")).toExist(), - expectView(text(first?.providerEventId ?? "")).toExist(), - expectView(text("203.0.113.7")).toExist(), + expectView(text(event?.providerEventId ?? "")).toExist(), + expectView(text(event?.originIp ?? "")).toExist(), ) scene( { update: pageUpdate, view }, - given(pageUpdate(opened, Message.ToggledEvent({ id: first?.id ?? "" })).model), + given(pageUpdate(opened, Message.ToggledEvent({ id })).model), expectView(text("Payload")).toBeAbsent(), ) }) + test("each event names the platform it came from, and the account on it", () => { + scene( + { update: pageUpdate, view }, + given(loaded()), + expectView(within(eventsTable, text("Vercel"))).toExist(), + expectView(within(eventsTable, text("Google Cloud"))).toExist(), + ) + scene( + { update: pageUpdate, view }, + given(openedFrom("gcp").opened), + expectView(text("acme-prod")).toExist(), + ) + }) + + test("the platform filter goes to the URL like the others", () => { + const vercel = pageUpdate(loaded(), Message.ChangedFilter({ filter: "provider", value: "vercel" })) + + expect(vercel.outMessage).toMatchObject({ url: hrefOf(routeOf({ provider: "vercel" })) }) + }) + test("a filter change goes to the URL, and choosing All drops it", () => { const denied = pageUpdate(loaded(), Message.ChangedFilter({ filter: "outcome", value: "denied" })) expect(denied.outMessage).toMatchObject({ url: hrefOf(routeOf({ outcome: "denied" })) }) @@ -313,20 +343,30 @@ describe("security audit log", () => { }) test("a Maple event's id opens the same entry in the Settings audit log", () => { - const model = loaded() - const first = eventsOf(model)?.[0] - const opened = pageUpdate(model, Message.ToggledEvent({ id: first?.id ?? "" })).model + const { event, opened } = openedFrom("maple") scene( { update: pageUpdate, view }, given(opened), - expectView(role("link", { name: first?.providerEventId ?? "" })).toHaveAttr( + expectView(role("link", { name: event?.providerEventId ?? "" })).toHaveAttr( "href", - auditEntryHref(first?.occurredAt ?? 0, first?.providerEventId ?? ""), + auditEntryHref(event?.occurredAt ?? 0, event?.providerEventId ?? ""), ), ) }) + // Only Maple's own events are entries of the Settings audit log. + test("another platform's event id is text, not a link into Maple's own audit log", () => { + const { event, opened } = openedFrom("vercel") + + scene( + { update: pageUpdate, view }, + given(opened), + expectView(text(event?.providerEventId ?? "")).toExist(), + expectView(role("link", { name: event?.providerEventId ?? "" })).toBeAbsent(), + ) + }) + test("an empty log says what will show up; an empty filter result offers to clear the filters", () => { scene( { update: pageUpdate, view }, diff --git a/apps/web/src/page/security/audit-log/command.ts b/apps/web/src/page/security/audit-log/command.ts index 1109175ffc..040819ae77 100644 --- a/apps/web/src/page/security/audit-log/command.ts +++ b/apps/web/src/page/security/audit-log/command.ts @@ -23,6 +23,7 @@ const listEvents = (search: SecurityAuditLogSearch, cursor: string | undefined) Effect.flatMap(MapleInternalApiClient, (api) => api.auditTrail.list({ query: { + ...(search.provider !== undefined ? { provider: search.provider } : undefined), ...(search.category !== undefined ? { category: search.category } : undefined), ...(search.outcome !== undefined ? { outcome: search.outcome } : undefined), ...(search.actor !== undefined ? { actor: search.actor } : undefined), diff --git a/apps/web/src/page/security/audit-log/derive.ts b/apps/web/src/page/security/audit-log/derive.ts index a98638cc9c..5235d8fbed 100644 --- a/apps/web/src/page/security/audit-log/derive.ts +++ b/apps/web/src/page/security/audit-log/derive.ts @@ -1,12 +1,18 @@ -import type { AuditTrailCategory, AuditTrailOutcome } from "@maple/domain/audit-trail" +import type { AuditTrailCategory, AuditTrailOutcome, AuditTrailProvider } from "@maple/domain/audit-trail" import type { AuditTrailEntry } from "@maple/domain/http" import type { LoadFailure } from "@maple/views/data/load-failure" import { Array as Arr, Option, Record as Rec } from "effect" import { AsyncData } from "foldkit" import type { Model, Search } from "./model" -// What the audit log shows of an event: the names of categories and outcomes, who acted and on -// what. A value this build does not know shows as itself. +// What the audit log shows of an event: the names of platforms, categories and outcomes, who acted +// and on what. A value this build does not know shows as itself. + +export const PROVIDER_LABEL = { + maple: "Maple", + vercel: "Vercel", + gcp: "Google Cloud", +} satisfies Record export const CATEGORY_LABEL = { authentication: "Authentication", @@ -27,8 +33,12 @@ export const OUTCOME_LABEL = { unknown: "Unknown", } satisfies Record -/** The outcomes a platform writes today: Maple records what went through and what it refused. */ -export const OUTCOME_FILTERS = ["success", "denied"] as const satisfies ReadonlyArray +/** The outcomes worth filtering by. `unknown` is a platform that did not say, not something to look for. */ +export const OUTCOME_FILTERS = [ + "success", + "failure", + "denied", +] as const satisfies ReadonlyArray const ACTOR_TYPE_LABEL = { user: "User", @@ -41,6 +51,7 @@ const ACTOR_TYPE_LABEL = { export const labelIn = (labels: Readonly>, value: string): string => Option.getOrElse(Rec.get(labels, value), () => value) +export const providerLabel = (provider: string): string => labelIn(PROVIDER_LABEL, provider) export const categoryLabel = (category: string): string => labelIn(CATEGORY_LABEL, category) export const outcomeLabel = (outcome: string): string => labelIn(OUTCOME_LABEL, outcome) export const actorTypeLabel = (type: string): string => labelIn(ACTOR_TYPE_LABEL, type) @@ -60,7 +71,10 @@ export const targetText = (event: AuditTrailEntry): string | null => event.targe export const isMarked = (event: AuditTrailEntry): boolean => event.outcome !== "success" export const isFiltered = (search: Search): boolean => - search.category !== undefined || search.outcome !== undefined || search.actor !== undefined + search.provider !== undefined || + search.category !== undefined || + search.outcome !== undefined || + search.actor !== undefined /** The API refused the viewer: the same thing the session says for a member without the permission. */ export const isForbidden = (failure: LoadFailure): boolean => failure.code === "audit_trail_forbidden" diff --git a/apps/web/src/page/security/audit-log/model.ts b/apps/web/src/page/security/audit-log/model.ts index 5b7ad3fba9..70e7fc450b 100644 --- a/apps/web/src/page/security/audit-log/model.ts +++ b/apps/web/src/page/security/audit-log/model.ts @@ -25,7 +25,7 @@ export const LaterPages = Schema.Struct({ export type LaterPages = typeof LaterPages.Type /** The filters a select sets; the actor is typed and applied on Enter. */ -export const SelectFilter = Schema.Literals(["category", "outcome"]) +export const SelectFilter = Schema.Literals(["provider", "category", "outcome"]) export type SelectFilter = typeof SelectFilter.Type export const Model = Schema.Struct({ diff --git a/apps/web/src/page/security/audit-log/view.ts b/apps/web/src/page/security/audit-log/view.ts index a333c936d2..7a82abf7fd 100644 --- a/apps/web/src/page/security/audit-log/view.ts +++ b/apps/web/src/page/security/audit-log/view.ts @@ -1,4 +1,4 @@ -import { AuditTrailCategory } from "@maple/domain/audit-trail" +import { AuditTrailCategory, AuditTrailProvider } from "@maple/domain/audit-trail" import type { AuditTrailEntry } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" import { refreshingClass } from "@maple/ui/lib/refreshing" @@ -48,7 +48,7 @@ import { expandChevron } from "../../kit/expand-chevron" import { jsonBlock, prettyJson } from "../../kit/json-block" import { refusedBadge } from "../../kit/refused-badge" import { relativeTime } from "../../kit/relative-time" -import { securityLayout, WIDE_COLUMN } from "../kit/layout" +import { ROOMY_COLUMN, securityLayout, WIDE_COLUMN } from "../kit/layout" import { placeholderRows } from "../kit/parts" import { actorDetail, @@ -65,6 +65,8 @@ import { OUTCOME_FILTERS, OUTCOME_LABEL, outcomeLabel, + PROVIDER_LABEL, + providerLabel, targetText, } from "./derive" import { accessOf, Message, type Model, type SelectFilter } from "./model" @@ -72,11 +74,11 @@ import { accessOf, Message, type Model, type SelectFilter } from "./model" type H = HtmlBuilder type Clock = Readonly<{ nowMs: number; timeZone: string }> -const COLUMN_COUNT = 5 +const COLUMN_COUNT = 6 // On a phone the two text columns share the width left of the time, so neither scrolls away. const NARROW_ACTOR = "max-w-32 sm:max-w-56" -const NARROW_ACTION = "max-w-36 sm:max-w-72" +const NARROW_ACTION = "max-w-36 sm:max-w-64" /** One filter as a native select: "All …" clears it. The URL holds the value it shows. */ const filterSelect = ( @@ -107,6 +109,15 @@ const filterSelect = ( const toolbar = (h: H, model: Model): Html => hstack(h, { gap: "2", align: "center", wrap: true, className: "border-b px-4 py-2.5" }, [ + filterSelect( + h, + "provider", + "Platform", + model.search.provider, + "All platforms", + PROVIDER_LABEL, + AuditTrailProvider.literals, + ), filterSelect( h, "category", @@ -184,6 +195,7 @@ const eventDetail = (h: H, event: AuditTrailEntry, clock: Clock): Html => { ...detailField(h, "IP address", event.originIp, true), ...detailField(h, "Country", event.originCountry), ...detailField(h, "User agent", event.userAgent, true), + keyValue(h, { label: "Platform", wrap: true }, [providerLabel(event.provider)]), ...detailField(h, "Account", event.account, true), keyValue(h, { label: "Occurred", wrap: true }, [at(event.occurredAt)]), keyValue(h, { label: "Recorded", wrap: true }, [at(event.ingestedAt)]), @@ -232,6 +244,7 @@ const eventRows = (h: H, model: Model, event: AuditTrailEntry, clock: Clock): Re ], "p-0", ), + tableCell(h, [providerLabel(event.provider)], WIDE_COLUMN), tableCell( h, [ @@ -255,6 +268,19 @@ const eventRows = (h: H, model: Model, event: AuditTrailEntry, clock: Clock): Re className: typographyClass({ size: "xs" }), }), hstack(h, { gap: "1.5", align: "center", wrap: true }, [ + // A phone has no Platform column: another platform's name leads the line. + ...(event.provider === "maple" + ? [] + : [ + h.span( + [h.Class("sm:hidden")], + [ + typography(h, { size: "2xs", tone: "muted" }, [ + providerLabel(event.provider), + ]), + ], + ), + ]), typography(h, { size: "2xs", tone: "muted" }, [ categoryLabel(event.category), ]), @@ -288,7 +314,7 @@ const eventRows = (h: H, model: Model, event: AuditTrailEntry, clock: Clock): Re className: typographyClass({ size: "xs", tone: "muted" }), }), ], - `max-w-64 ${WIDE_COLUMN}`, + `max-w-64 ${ROOMY_COLUMN}`, ), tableCell(h, outcomeMark(h, event), WIDE_COLUMN), ], @@ -340,9 +366,11 @@ const eventsTable = (h: H, model: Model, events: ReadonlyArray, tableHeader(h, [ tableRow(h, [ tableHead(h, ["Time"], "w-24"), + tableHead(h, ["Platform"], WIDE_COLUMN), tableHead(h, ["Actor"]), tableHead(h, ["Action"]), - tableHead(h, ["Target"], WIDE_COLUMN), + // Beside the rail a laptop has no room for it; the detail and the wide screen do. + tableHead(h, ["Target"], ROOMY_COLUMN), tableHead(h, ["Outcome"], `w-28 ${WIDE_COLUMN}`), ]), ]), @@ -372,7 +400,7 @@ const noEvents = (h: H): Html => emptyMedia(h, { variant: "icon" }, [icon(h, "HistoryIcon")]), emptyTitle(h, ["No audit events yet"]), emptyDescription(h, [ - "Changes made in Maple, and requests it refused, appear here as they happen.", + "Changes made in Maple, and requests it refused, appear here as they happen. So do audit events from Vercel and Google Cloud once their audit feeds are on.", ]), ]), ], @@ -397,7 +425,7 @@ const listBody = (h: H, model: Model, clock: Clock): Html => if (events.length === 0 && isFiltered(model.search)) return filteredEmpty(h, { noun: "events", - description: "Try a wider category or outcome, or another actor.", + description: "Try another platform, category or outcome, or another actor.", onClear: Message.ClickedClearFilters(), className: "py-8", }) diff --git a/apps/web/src/page/security/kit/layout.ts b/apps/web/src/page/security/kit/layout.ts index aba940519a..84a801686c 100644 --- a/apps/web/src/page/security/kit/layout.ts +++ b/apps/web/src/page/security/kit/layout.ts @@ -146,7 +146,8 @@ const DESCRIPTION = { "The language runtime each service reports, checked for known vulnerabilities and end-of-life dates.", dependencies: "Known vulnerable packages in your connected repositories' lockfiles, rescanned on every push.", - auditLog: "Who changed what in Maple, and what it refused. Visible to org admins.", + auditLog: + "Who changed what, and what was refused, in Maple and your connected platforms. Visible to org admins.", } satisfies Record /** The page's blocks at their own height: the scroll area scrolls them, never squeezes them. */ diff --git a/apps/web/src/route/search.ts b/apps/web/src/route/search.ts index f41d12cf1a..4600fec5d0 100644 --- a/apps/web/src/route/search.ts +++ b/apps/web/src/route/search.ts @@ -1087,6 +1087,8 @@ export const SecurityDependenciesSearch = Schema.Struct({ /** The Security hub's audit log filters; each absent one shows everything. */ export const SecurityAuditLogSearch = Schema.Struct({ + /** The platform the events came from (`maple`, `vercel`, `gcp`). */ + provider: StringParam, category: StringParam, outcome: StringParam, /** An actor's id or email on the platform the event came from. */ diff --git a/packages/backend/src/queries/registry.ts b/packages/backend/src/queries/registry.ts index 499bc856e6..4d4c16fc16 100644 --- a/packages/backend/src/queries/registry.ts +++ b/packages/backend/src/queries/registry.ts @@ -558,14 +558,6 @@ const vercelWebAnalyticsReferrers = vercelWebAnalyticsTop("vercelWebAnalyticsRef const vercelWebAnalyticsCountries = vercelWebAnalyticsTop("vercelWebAnalyticsCountries", "country") const vercelWebAnalyticsEvents = vercelWebAnalyticsTop("vercelWebAnalyticsEvents", "event") -const vercelAuditLog = defineQuery({ - id: "vercelAuditLog", - profile: "aggregation", - cache: 15, - compile: (payload: VercelInfraProjectsRequest, orgId: OrgId) => - CH.compile(Integrations.vercelAuditLogSQL(), vercelWindow(payload, orgId)), -}) - // /infra/supabase: gauges and counter rates are separate reads (metrics_gauge, metrics_sum), // merged per project or bucket by the handler. const supabaseInfraProjectGauges = defineQuery({ @@ -1317,7 +1309,6 @@ export const Queries = { vercelWebAnalyticsReferrers, vercelWebAnalyticsCountries, vercelWebAnalyticsEvents, - vercelAuditLog, supabaseInfraProjectGauges, supabaseInfraProjectCpu, supabaseInfraProjectGaugeSeries, diff --git a/packages/backend/src/services/alerts/AlertDeliveryDispatch.test.ts b/packages/backend/src/services/alerts/AlertDeliveryDispatch.test.ts index db97e822ae..611727536e 100644 --- a/packages/backend/src/services/alerts/AlertDeliveryDispatch.test.ts +++ b/packages/backend/src/services/alerts/AlertDeliveryDispatch.test.ts @@ -289,6 +289,45 @@ describe("anomaly notifications", () => { }) }) +describe("audit detection notifications", () => { + const auditContext: TemplateRenderContext = { + ...baseContext, + ruleName: "Members added", + signalType: "audit", + signalDisplay: resolveSignalDisplay({ signalType: "audit" }), + comparator: "gte", + threshold: 3, + value: 4, + sampleCount: 4, + windowMinutes: 10, + detail: "4 matching audit events in the last 10 minutes", + } + + it("says how many events matched and over which window", () => { + assert.deepStrictEqual( + [buildSummaryLine(auditContext, (value) => value), formatObservedSummary(auditContext)], + [ + "4 matching audit events in the last 10 minutes", + "4 matching audit events in the last 10 minutes", + ], + ) + }) + + it("names the signal Audit log and reads the count as a whole number", () => { + const context = buildTemplateContext(auditContext, LINK, CHAT) + assert.strictEqual(context["signal.label"], "Audit log") + assert.strictEqual(context.observed, "4") + assert.strictEqual(context.threshold, "3") + }) + + // A test send observed nothing, so it describes the condition instead of a count. + it("a test notification describes the condition", () => { + const line = buildSummaryLine({ ...auditContext, eventType: "test" }, (value) => value) + assert.include(line, "Live alerts fire when Audit log is") + assert.notInclude(line, "matching audit events") + }) +}) + describe("buildDiscordEmbedsFromTemplate", () => { it("maps title/body to the embed and color-codes by severity", () => { const [embed] = buildDiscordEmbedsFromTemplate("T", "B", baseContext, LINK) as Array<{ diff --git a/packages/backend/src/services/alerts/AlertDeliveryDispatch.ts b/packages/backend/src/services/alerts/AlertDeliveryDispatch.ts index f36615fbaf..1761d81c32 100644 --- a/packages/backend/src/services/alerts/AlertDeliveryDispatch.ts +++ b/packages/backend/src/services/alerts/AlertDeliveryDispatch.ts @@ -84,6 +84,9 @@ export const buildSummaryLine = (context: SummaryLineContext, em: (value: string // An SLO's check names both windows and the tier, which one value and window cannot. if (context.signalType === "slo_burn_rate" && context.detail != null && context.eventType !== "test") return em(context.detail) + // A detection's check already says how many events arrived and over which window. + if (context.signalType === "audit" && context.detail != null && context.eventType !== "test") + return em(context.detail) const signal = formatSignalLabel(context) const observed = formatSignalMetric(context.value, signalDisplayOf(context)) const window = formatWindow(context.windowMinutes) diff --git a/packages/backend/src/services/alerts/AlertReadModelsService.test.ts b/packages/backend/src/services/alerts/AlertReadModelsService.test.ts index d0e52c07a1..7a5d9e79bb 100644 --- a/packages/backend/src/services/alerts/AlertReadModelsService.test.ts +++ b/packages/backend/src/services/alerts/AlertReadModelsService.test.ts @@ -253,4 +253,30 @@ describe("AlertReadModelsService", () => { }) }).pipe(Effect.provide(makeLayer(testDb, contexts))) }) + + // The public rule API does not list a detection's rule. Its checks are a minute-by-minute + // count of audit activity, so the same API must not hand those out by id either. + it.effect("hides an audit detection's checks from the public API and serves them inside Maple", () => { + const testDb = createTestDb(createdDbs) + const contexts: Array = [] + const range = { since: "2026-08-01T00:00:00.000Z", until: "2026-08-02T00:00:00.000Z" } + return Effect.gen(function* () { + yield* Effect.promise(() => seedReadModels(testDb)) + yield* Effect.promise(() => + executeSql(testDb, "UPDATE alert_rules SET signal_type = 'audit' WHERE id = $1", [RULE]), + ) + const readModels = yield* AlertReadModelsService + + const list = yield* Effect.flip(readModels.listRuleChecks(ORG, RULE, { publicApi: true })) + const summary = yield* Effect.flip( + readModels.summarizeRuleChecks(ORG, RULE, { ...range, publicApi: true }), + ) + assert.strictEqual(list._tag, "@maple/http/errors/AlertRuleNotFoundError") + assert.strictEqual(summary._tag, "@maple/http/errors/AlertRuleNotFoundError") + assert.deepStrictEqual(contexts, []) + + assert.deepStrictEqual((yield* readModels.listRuleChecks(ORG, RULE, {})).checks, []) + assert.deepStrictEqual((yield* readModels.summarizeRuleChecks(ORG, RULE, range)).points, []) + }).pipe(Effect.provide(makeLayer(testDb, contexts))) + }) }) diff --git a/packages/backend/src/services/alerts/AlertReadModelsService.ts b/packages/backend/src/services/alerts/AlertReadModelsService.ts index a53dbc1bef..df6e89f37c 100644 --- a/packages/backend/src/services/alerts/AlertReadModelsService.ts +++ b/packages/backend/src/services/alerts/AlertReadModelsService.ts @@ -117,6 +117,8 @@ export interface AlertReadModelsServiceApi { readonly limit?: number readonly beforeTimestamp?: string readonly beforeGroupKey?: string + /** The public API: an audit detection's rule is not there, and neither are its checks. */ + readonly publicApi?: boolean }, ) => Effect.Effect< AlertChecksListResponse, @@ -128,6 +130,7 @@ export interface AlertReadModelsServiceApi { options: { readonly since: string readonly until: string + readonly publicApi?: boolean }, ) => Effect.Effect< AlertChecksSummary, @@ -139,6 +142,15 @@ export interface AlertReadModelsServiceApi { ) => Effect.Effect } +/** + * An audit detection's rule is absent from the public rule API, so its checks are too: they are + * a minute-by-minute count of audit activity. Uptime and SLO checks have always been served. + */ +const hiddenFromPublicApi = ( + rule: { readonly signalType: string }, + options: { readonly publicApi?: boolean }, +): boolean => options.publicApi === true && rule.signalType === "audit" + const decodeAlertIncidentIdSync = Schema.decodeUnknownSync(AlertIncidentDocument.fields.id) const decodeAlertRuleIdSync = Schema.decodeUnknownSync(AlertRuleDocument.fields.id) const decodeAlertDeliveryEventIdSync = Schema.decodeUnknownSync(AlertDeliveryEventDocument.fields.id) @@ -366,6 +378,7 @@ export class AlertReadModelsService extends Context.Service< readonly limit?: number readonly beforeTimestamp?: string readonly beforeGroupKey?: string + readonly publicApi?: boolean }, ) { yield* Effect.annotateCurrentSpan({ orgId, "maple.alert.rule_id": ruleId }) @@ -373,13 +386,13 @@ export class AlertReadModelsService extends Context.Service< const ruleRow = yield* dbExecute((db) => db.run( PG.from(AlertRules) - .select("id", "createdAt") + .select("id", "createdAt", "signalType") .where(($) => [$.orgId.eq(orgId), $.id.eq(ruleId)]) .limit(1), ), ) const [rule] = ruleRow - if (rule === undefined) { + if (rule === undefined || hiddenFromPublicApi(rule, options)) { return yield* new AlertRuleNotFoundError({ message: "Alert rule not found", ruleId, @@ -500,18 +513,19 @@ export class AlertReadModelsService extends Context.Service< const summarizeRuleChecks = Effect.fn("AlertReadModelsService.summarizeRuleChecks")(function* ( orgId: OrgId, ruleId: AlertRuleId, - options: { readonly since: string; readonly until: string }, + options: { readonly since: string; readonly until: string; readonly publicApi?: boolean }, ) { yield* Effect.annotateCurrentSpan({ orgId, "maple.alert.rule_id": ruleId }) const ruleRow = yield* dbExecute((db) => db.run( PG.from(AlertRules) - .select("id") + .select("id", "signalType") .where(($) => [$.orgId.eq(orgId), $.id.eq(ruleId)]) .limit(1), ), ) - if (ruleRow.length === 0) { + const [summarized] = ruleRow + if (summarized === undefined || hiddenFromPublicApi(summarized, options)) { return yield* new AlertRuleNotFoundError({ message: "Alert rule not found", ruleId, diff --git a/packages/backend/src/services/alerts/AlertRuleModel.ts b/packages/backend/src/services/alerts/AlertRuleModel.ts index 683b43448c..c0054a7f48 100644 --- a/packages/backend/src/services/alerts/AlertRuleModel.ts +++ b/packages/backend/src/services/alerts/AlertRuleModel.ts @@ -41,6 +41,7 @@ import { isManagedSignalType, type ManagedSignalType, } from "@maple/domain/http" +import { AuditDetectionFilters } from "@maple/domain/audit-trail" import * as PG from "@maple-dev/effect-orm/postgres" import { AlertRules, type AlertRuleRow } from "@maple/db/tables" import { DateTime, Array as Arr, Effect, Match, Result, Schema, String as Str } from "effect" @@ -57,6 +58,7 @@ type StoredJsonColumn = | "tagsJson" | "destinationIdsJson" | "thresholdsJson" + | "auditFiltersJson" /** * An `alert_rules` row with its schema-typed jsonb columns read as `unknown`. The model @@ -76,6 +78,8 @@ export const selectStoredAlertRules = () => tagsJson: PG.undecoded($.tagsJson), destinationIdsJson: PG.undecoded($.destinationIdsJson), thresholdsJson: PG.undecoded($.thresholdsJson), + // Undecoded so one detection with filters this build cannot read fails alone. + auditFiltersJson: PG.undecoded($.auditFiltersJson), })) const DestinationIdArraySchema = Schema.Array(AlertDestinationDocument.fields.id) const AlertGroupByFromJson = Schema.fromJsonString(AlertGroupBySchema) @@ -105,6 +109,10 @@ export interface NormalizedRule { readonly environments: ReadonlyArray /** Anomaly rules only: detector signals in scope; empty is every signal. */ readonly anomalySignals: ReadonlyArray + /** Audit detections only: which events the rule counts; empty is every event. */ + readonly auditFilters: AuditDetectionFilters + /** Audit detections only: events that arrived before this do not count; null counts the whole window. */ + readonly auditCountFrom: number | null readonly tags: ReadonlyArray readonly groupBy: AlertGroupBy | null readonly signalType: AlertSignalType @@ -235,13 +243,26 @@ const planGroupingTokens = ( /** Why the generic rule APIs refuse a rule another resource owns. */ export const managedRuleMessage = (signalType: ManagedSignalType): string => - signalType === "uptime" - ? "uptime rules belong to an uptime monitor; create, edit or delete the monitor instead" - : "slo_burn_rate rules belong to an SLO; create, edit or delete the SLO instead" + Match.value(signalType).pipe( + Match.when( + "uptime", + () => "uptime rules belong to an uptime monitor; create, edit or delete the monitor instead", + ), + Match.when( + "slo_burn_rate", + () => "slo_burn_rate rules belong to an SLO; create, edit or delete the SLO instead", + ), + Match.when( + "audit", + () => + "audit rules belong to an audit log detection; create, edit or delete the detection in the Security hub instead", + ), + Match.exhaustive, + ) /** - * The stored plan of every queryless rule (`uptime`, `slo_burn_rate`, `anomaly`): the evaluator - * reads the owning resource or the anomaly detector's incidents instead. + * The stored plan of every queryless rule (`uptime`, `slo_burn_rate`, `audit`, `anomaly`): the + * evaluator reads the owning resource or the anomaly detector's incidents instead. */ export const MANAGED_RULE_PLAN = new CompiledAlertQueryPlan({ kind: "spec", @@ -646,6 +667,12 @@ export const decodeStoredAlertRuleMetadata = (row: StoredAlertRuleRow) => ), }) +/** An audit detection's filters. A detection stored without any counts every event. */ +const decodeStoredAuditFilters = (row: StoredAlertRuleRow) => + row.signalType !== "audit" || row.auditFiltersJson == null + ? Effect.succeed({}) + : decodeStoredRuleComponent(row.id, "audit_filters", AuditDetectionFilters, row.auditFiltersJson) + export const decodeStoredAlertRuleDestinationIds = ( ruleId: AlertRuleId, value: unknown, @@ -804,6 +831,7 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { const destinationIds = yield* decodeStoredAlertRuleDestinationIds(row.id, row.destinationIdsJson) const thresholds = yield* decodeStoredAlertRuleThresholds(row, destinationIds) const legacy = alertThresholdLegacyFields(thresholds) + const auditFilters = yield* decodeStoredAuditFilters(row) return { id: decoded.id, name: row.name, @@ -816,6 +844,8 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { excludeServiceNames: stored.excludeServiceNames, environments: stored.environments, anomalySignals: row.anomalySignalsJson ?? [], + auditFilters, + auditCountFrom: row.auditCountFrom, tags: stored.tags, groupBy: decoded.groupBy, signalType: decoded.signalType, @@ -1040,6 +1070,8 @@ export const makeAlertRuleNormalizer = (runtime: AlertRuntimeApi) => { excludeServiceNames, environments, anomalySignals, + auditFilters: {}, + auditCountFrom: null, tags, groupBy, signalType: request.signalType, diff --git a/packages/backend/src/services/alerts/AlertsService.audit.test.ts b/packages/backend/src/services/alerts/AlertsService.audit.test.ts new file mode 100644 index 0000000000..94afbf9bfb --- /dev/null +++ b/packages/backend/src/services/alerts/AlertsService.audit.test.ts @@ -0,0 +1,314 @@ +import { afterEach, assert, describe, it } from "@effect/vitest" +import { WarehouseUpstreamError } from "@maple/domain/http" +import type { AuditDetectionFilters } from "@maple/domain/audit-trail" +import { compiledQueryOf } from "@maple/query-engine/execution" +import { Array as Arr, Duration, Effect, Layer, Schema } from "effect" +import { TestClock } from "effect/testing" +import { + cleanupTestDbs, + createTestDb, + executeSql, + queryFirstRow, + type TestDb, +} from "@maple/backend/testing/test-pglite" +import { makeWarehouseServiceStub } from "@maple/backend/testing/warehouse-test-support" +import { AuditDetectionsService } from "@maple/backend/services/audit-trail/AuditDetectionsService" +import { + DEFAULT_CLOCK_EPOCH_MS, + alertServices, + asOrgId, + asUserId, + createWebhookDestination, + makeLayer, +} from "./alerts-service-test-support" + +const trackedDbs: TestDb[] = [] +afterEach(() => cleanupTestDbs(trackedDbs)) + +/** Fetch bodies arrive as a string or, from the Effect HTTP client, as bytes. */ +const requestBodyText = (body: RequestInit["body"]): string => + body instanceof Uint8Array ? new TextDecoder().decode(body) : typeof body === "string" ? body : "" + +/** What the audit trail answers a detection's count with; mutated as a test moves on. */ +interface Trail { + matchingEvents: number + down?: boolean +} + +/** + * A warehouse answering the detection count and keeping the SQL of each count it was asked + * for, and a webhook endpoint keeping the bodies it was sent. + */ +const harness = (trail: Trail) => { + const testDb = createTestDb(trackedDbs) + const counted: Array = [] + const bodies: Array = [] + const fetchImpl: typeof fetch = async (_input, init) => { + bodies.push(requestBodyText(init?.body)) + return new Response("ok", { status: 200 }) + } + const warehouse = makeWarehouseServiceStub({ + compiledQuery: (_tenant, compiled, options) => + Effect.gen(function* () { + const query = compiledQueryOf(compiled) + if (options?.context !== "auditTrailCount") { + return yield* query.decodeRows([]).pipe(Effect.orDie) + } + if (trail.down === true) { + return yield* new WarehouseUpstreamError({ + message: "warehouse down", + pipeName: "auditTrailCount", + }) + } + counted.push(query.sql) + return yield* query.decodeRows([{ events: trail.matchingEvents }]).pipe(Effect.orDie) + }), + }) + const detections = Layer.effect(AuditDetectionsService, AuditDetectionsService.make).pipe( + Layer.provide(testDb.layer), + ) + return { + testDb, + counted, + bodies, + layer: Layer.mergeAll(makeLayer(testDb, warehouse, { fetch: fetchImpl }), detections), + } +} + +const orgId = asOrgId("org_audit_alerts") +const userId = asUserId("user_audit_alerts") + +const createDetection = Effect.fn(function* (filters: AuditDetectionFilters = { category: "membership" }) { + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const destination = yield* createWebhookDestination(alerts, orgId, userId) + return yield* detections.create(orgId, { + name: "Members added", + filters, + windowMinutes: 10, + threshold: 3, + severity: "critical", + destinationIds: [destination.id], + createdBy: userId, + }) +}) + +const ruleState = (testDb: TestDb, ruleId: string) => + Effect.promise(() => + queryFirstRow<{ last_status: string | null; last_value: number | null; last_error: string | null }>( + testDb, + "select last_status, last_value, last_error from alert_rule_states where rule_id = $1", + [ruleId], + ), + ) + +describe("AlertsService audit log detections", () => { + it.effect("stays healthy below the count and opens nothing", () => { + const { layer, testDb } = harness({ matchingEvents: 2 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detection = yield* createDetection() + + yield* alerts.runSchedulerTick() + + assert.lengthOf((yield* alerts.listIncidents(orgId)).incidents, 0) + assert.deepInclude(yield* ruleState(testDb, detection.id), { + last_status: "healthy", + last_value: 2, + }) + }).pipe(Effect.provide(layer)) + }) + + it.effect("opens an incident at the count, notifies the destination, and resolves below it", () => { + const trail: Trail = { matchingEvents: 3 } + const { layer, testDb } = harness(trail) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detection = yield* createDetection() + + yield* alerts.runSchedulerTick() + const opened = (yield* alerts.listIncidents(orgId)).incidents + assert.lengthOf(opened, 1) + assert.deepInclude(opened[0], { + ruleId: detection.id, + ruleName: "Members added", + signalType: "audit", + severity: "critical", + status: "open", + lastObservedValue: 3, + }) + assert.deepStrictEqual( + Arr.map((yield* alerts.listDeliveryEvents(orgId)).events, (event) => event.eventType), + ["trigger"], + ) + // No fault in a service: the incident files nothing in the errors triage queue. + const issues = yield* Effect.promise(() => + queryFirstRow<{ issues: number }>( + testDb, + "select count(*)::int as issues from error_issues where org_id = $1", + [orgId], + ), + ) + assert.deepInclude(issues, { issues: 0 }) + + // The events slide out of the window: the count is a real zero, so it resolves. + trail.matchingEvents = 0 + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "resolved") + }).pipe(Effect.provide(layer)) + }) + + // Notifications say how many events matched and over which window, never which events. + it.effect("notifies with the count as its detail", () => { + const { layer, bodies } = harness({ matchingEvents: 4 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + yield* createDetection() + yield* alerts.runSchedulerTick() + + const trigger = Schema.decodeSync( + Schema.fromJsonString( + Schema.Struct({ + eventType: Schema.String, + observed: Schema.Struct({ detail: Schema.String, value: Schema.Number }), + }), + ), + )(bodies[0] ?? "") + assert.deepStrictEqual(trigger, { + eventType: "trigger", + observed: { detail: "4 matching audit events in the last 10 minutes", value: 4 }, + }) + }).pipe(Effect.provide(layer)) + }) + + it.effect("counts the detection's filters over its window, ending at the snapped minute", () => { + const { layer, counted } = harness({ matchingEvents: 0 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + yield* createDetection({ provider: "vercel", action: "team.member.added" }) + + // Saved at 22:13:20 UTC. Eleven minutes on, the window ends at the snapped minute + // 22:24 and reaches back its full 10 minutes, all of them after the save. + yield* TestClock.adjust(Duration.minutes(11)) + yield* alerts.runSchedulerTick() + + assert.lengthOf(counted, 1) + assert.include(counted[0], `OrgId = '${orgId}'`) + assert.include(counted[0], "IngestedAt > '2023-11-14 22:14:00'") + assert.include(counted[0], "IngestedAt <= '2023-11-14 22:24:00'") + assert.include(counted[0], "Provider = 'vercel'") + assert.include(counted[0], "Action = 'team.member.added'") + }).pipe(Effect.provide(layer)) + }) + + // A new detection, or one whose window was just widened, would otherwise fire at once on + // events that were already there, some of them already alerted on. + it.effect("counts only what arrived since the detection started counting it", () => { + const { layer, counted } = harness({ matchingEvents: 0 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const detection = yield* createDetection() + + // Three minutes after it was created at 22:13:20 the 10 minute window starts there. + yield* TestClock.adjust(Duration.minutes(3)) + yield* alerts.runSchedulerTick() + assert.include(counted[0], "IngestedAt > '2023-11-14 22:13:20'") + assert.include(counted[0], "IngestedAt <= '2023-11-14 22:16:00'") + + // Widened at 22:16:20: the wider window does not reach back past the edit. + yield* detections.update(orgId, detection.id, { windowMinutes: 60, updatedBy: userId }) + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + assert.include(counted[1], "IngestedAt > '2023-11-14 22:16:20'") + }).pipe(Effect.provide(layer)) + }) + + // A rename counts nothing new. Moving the floor for it would empty the window, resolve the + // open incident and tell everyone it was over. + it.effect("an edit that changes nothing about what is counted keeps an open incident open", () => { + const { layer, counted } = harness({ matchingEvents: 3 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const detection = yield* createDetection() + yield* TestClock.adjust(Duration.minutes(3)) + yield* alerts.runSchedulerTick() + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "open") + + yield* detections.update(orgId, detection.id, { + name: "Members added or removed", + severity: "warning", + threshold: 2, + windowMinutes: 5, + updatedBy: userId, + }) + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + + assert.include(counted[1], "IngestedAt > '2023-11-14 22:13:20'") + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "open") + }).pipe(Effect.provide(layer)) + }) + + it.effect("a stored audit rule with no filters column counts every event", () => { + const { layer, counted, testDb } = harness({ matchingEvents: 3 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detection = yield* createDetection() + yield* Effect.promise(() => + executeSql(testDb, "update alert_rules set audit_filters_json = null where id = $1", [ + detection.id, + ]), + ) + + yield* alerts.runSchedulerTick() + + assert.notInclude(counted[0], "Category =") + assert.lengthOf((yield* alerts.listIncidents(orgId)).incidents, 1) + }).pipe(Effect.provide(layer)) + }) + + // A count that could not be read is not a count of zero: the incident must not resolve on it. + it.effect("keeps an open incident open and records the failure while the audit trail is down", () => { + const trail: Trail = { matchingEvents: 3 } + const { layer, testDb } = harness(trail) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detection = yield* createDetection() + yield* alerts.runSchedulerTick() + + trail.down = true + yield* TestClock.adjust(Duration.minutes(1)) + yield* alerts.runSchedulerTick() + + assert.strictEqual((yield* alerts.listIncidents(orgId)).incidents[0]?.status, "open") + assert.isNotNull((yield* ruleState(testDb, detection.id))?.last_error) + }).pipe(Effect.provide(layer)) + }) + + it.effect("a disabled detection is not checked", () => { + const { layer, counted } = harness({ matchingEvents: 3 }) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const detection = yield* createDetection() + yield* detections.update(orgId, detection.id, { enabled: false, updatedBy: userId }) + + yield* alerts.runSchedulerTick() + + assert.lengthOf(counted, 0) + assert.lengthOf((yield* alerts.listIncidents(orgId)).incidents, 0) + }).pipe(Effect.provide(layer)) + }) +}) diff --git a/packages/backend/src/services/alerts/AlertsService.ts b/packages/backend/src/services/alerts/AlertsService.ts index bea2a97d4d..c4f2c3e0b2 100644 --- a/packages/backend/src/services/alerts/AlertsService.ts +++ b/packages/backend/src/services/alerts/AlertsService.ts @@ -193,6 +193,8 @@ import { AlertReadModelsService } from "./AlertReadModelsService" import { makeAlertRulePersistence } from "./AlertRulesService" import { UptimeAlertSource } from "@maple/backend/services/uptime/UptimeAlertSource" import { SloAlertSource } from "@maple/backend/services/slos/SloAlertSource" +import { AuditTrailService } from "@maple/backend/services/audit-trail/AuditTrailService" +import { auditCountReason } from "@maple/backend/services/audit-trail/audit-detection-rule" import { AnomalyAlertSource, type AnomalyAlertObservation } from "./anomaly/AnomalyAlertSource" import { evaluateSloBurn } from "@maple/backend/services/slos/slo-alert-rule" import { @@ -631,6 +633,7 @@ export class AlertsService extends Context.Service = yield* queryEngine.evaluate( @@ -4130,7 +4155,9 @@ export class AlertsService extends Context.Service): str /** Whether a rule's notifications carry the evaluator's reason as their `detail`. */ export const signalCarriesDetail = (signalType: string): boolean => - signalType === "uptime" || signalType === "slo_burn_rate" || signalType === "anomaly" + signalType === "uptime" || + signalType === "slo_burn_rate" || + signalType === "anomaly" || + signalType === "audit" /** An anomaly rule's value is its severity code; what it says is the detector's sentence. */ const anomalyObserved = (context: Pick): string => @@ -233,7 +236,8 @@ export const formatObservedSummary = (context: ObservedContext): string => ? uptimeObserved(context) : context.signalType === "anomaly" ? anomalyObserved(context) - : context.signalType === "slo_burn_rate" && context.detail != null + : (context.signalType === "slo_burn_rate" || context.signalType === "audit") && + context.detail != null ? context.detail : context.value === null ? `No data (threshold ${formatThresholdSummary(context)})` diff --git a/packages/backend/src/services/alerts/alert-signal-display.ts b/packages/backend/src/services/alerts/alert-signal-display.ts index b2009fb091..9d3109d530 100644 --- a/packages/backend/src/services/alerts/alert-signal-display.ts +++ b/packages/backend/src/services/alerts/alert-signal-display.ts @@ -51,6 +51,7 @@ const PRESET_SIGNALS: Record = { uptime: { label: "Uptime check", unit: "plain" }, slo_burn_rate: { label: "SLO burn rate", unit: "burn" }, anomaly: { label: "Anomaly", unit: "severity" }, + audit: { label: "Audit log", unit: "count" }, } satisfies Record /** diff --git a/packages/backend/src/services/alerts/alerts-service-test-support.ts b/packages/backend/src/services/alerts/alerts-service-test-support.ts index 879bbffa9f..16ab903d8e 100644 --- a/packages/backend/src/services/alerts/alerts-service-test-support.ts +++ b/packages/backend/src/services/alerts/alerts-service-test-support.ts @@ -37,6 +37,7 @@ import { AlertSilencesService } from "./AlertSilencesService" import { AlertMaintenanceWindowsService } from "./AlertMaintenanceWindowsService" import { AlertRulesService, type AlertRulesServiceApi } from "./AlertRulesService" import { SloAlertSource } from "@maple/backend/services/slos/SloAlertSource" +import { AuditTrailService } from "@maple/backend/services/audit-trail/AuditTrailService" import { UptimeAlertSource } from "@maple/backend/services/uptime/UptimeAlertSource" import { AnomalyAlertSource } from "@maple/backend/services/alerts/anomaly/AnomalyAlertSource" import { BucketCacheService } from "@maple/query-engine/caching" @@ -361,6 +362,9 @@ export const makeLayer = ( Layer.provide(Layer.mergeAll(databaseLive, warehouseLive)), ), ), + Layer.provide( + Layer.effect(AuditTrailService, AuditTrailService.make).pipe(Layer.provide(warehouseLive)), + ), Layer.provide(chatSessions === undefined ? Layer.empty : Layer.succeed(ChatSessions, chatSessions)), ) const alertSilencesLive = AlertSilencesService.layer.pipe( diff --git a/packages/backend/src/services/audit-trail/AuditDetectionsService.test.ts b/packages/backend/src/services/audit-trail/AuditDetectionsService.test.ts new file mode 100644 index 0000000000..fac62dcef3 --- /dev/null +++ b/packages/backend/src/services/audit-trail/AuditDetectionsService.test.ts @@ -0,0 +1,413 @@ +import { afterEach, assert, describe, it } from "@effect/vitest" +import { AlertRuleUpsertRequest, MAX_AUDIT_DETECTIONS_PER_ORG } from "@maple/domain/http" +import { AlertDestinationId, AlertRuleId } from "@maple/primitives" +import { Array as Arr, Duration, Effect, Layer } from "effect" +import { TestClock } from "effect/testing" +import { + DEFAULT_CLOCK_EPOCH_MS, + adminRoles, + alertServices, + asOrgId, + asUserId, + createErrorRateRule, + createWebhookDestination, + makeLayer, +} from "@maple/backend/services/alerts/alerts-service-test-support" +import { cleanupTestDbs, createTestDb, queryFirstRow, type TestDb } from "@maple/backend/testing/test-pglite" +import { makeWarehouseServiceStub } from "@maple/backend/testing/warehouse-test-support" +import { compiledQueryOf } from "@maple/query-engine/execution" +import { AuditDetectionsService, type CreateAuditDetectionInput } from "./AuditDetectionsService" + +const trackedDbs: TestDb[] = [] +afterEach(() => cleanupTestDbs(trackedDbs)) + +const orgId = asOrgId("org_audit_detections") +const otherOrgId = asOrgId("org_audit_detections_other") +const userId = asUserId("user_audit_detections") +const editorId = asUserId("user_audit_detections_editor") + +/** `matchingEvents` is what the audit trail counts for every detection's window. */ +const harness = (matchingEvents = 0) => { + const testDb = createTestDb(trackedDbs) + const warehouse = makeWarehouseServiceStub({ + compiledQuery: (_tenant, compiled, options) => + compiledQueryOf(compiled) + .decodeRows(options?.context === "auditTrailCount" ? [{ events: matchingEvents }] : []) + .pipe(Effect.orDie), + }) + const detections = Layer.effect(AuditDetectionsService, AuditDetectionsService.make).pipe( + Layer.provide(testDb.layer), + ) + return { testDb, layer: Layer.mergeAll(makeLayer(testDb, warehouse), detections) } +} + +const detectionInput = (overrides: Partial = {}): CreateAuditDetectionInput => ({ + name: "Members added", + filters: { provider: "vercel", category: "membership" }, + windowMinutes: 10, + threshold: 3, + severity: "warning", + createdBy: userId, + ...overrides, +}) + +/** The tag an attempt was refused with. */ +const refusal = (attempt: Effect.Effect) => + Effect.map(Effect.flip(attempt), (error) => error._tag) + +const ruleRow = (testDb: TestDb, id: AlertRuleId) => + Effect.promise(() => + queryFirstRow<{ + signal_type: string + created_by: string + updated_by: string + updated_at: Date + audit_filters_json: unknown + }>( + testDb, + "select signal_type, created_by, updated_by, updated_at, audit_filters_json from alert_rules where id = $1", + [id], + ), + ) + +describe("AuditDetectionsService", () => { + it.effect("creates a detection as an audit rule owned by its author, and lists it", () => { + const { layer, testDb } = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const destination = yield* createWebhookDestination(alerts, orgId, userId) + + const created = yield* detections.create( + orgId, + detectionInput({ destinationIds: [destination.id, destination.id] }), + ) + + assert.deepStrictEqual( + { ...created, id: null }, + { + id: null, + name: "Members added", + enabled: true, + filters: { provider: "vercel", category: "membership" }, + windowMinutes: 10, + threshold: 3, + severity: "warning", + destinationIds: [destination.id], + createdAt: DEFAULT_CLOCK_EPOCH_MS, + updatedAt: DEFAULT_CLOCK_EPOCH_MS, + }, + ) + assert.deepStrictEqual(yield* detections.list(orgId), [created]) + assert.deepStrictEqual(yield* detections.get(orgId, created.id), created) + const row = yield* ruleRow(testDb, created.id) + assert.strictEqual(row?.signal_type, "audit") + assert.strictEqual(row?.created_by, userId) + }).pipe(Effect.provide(layer)) + }) + + it.effect("lists newest first, only the org's own, and never another kind of rule", () => { + const { layer } = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const destination = yield* createWebhookDestination(alerts, orgId, userId) + yield* createErrorRateRule(alerts, orgId, userId, destination.id) + const first = yield* detections.create(orgId, detectionInput({ name: "First" })) + yield* TestClock.adjust(Duration.minutes(1)) + const second = yield* detections.create(orgId, detectionInput({ name: "Second" })) + yield* detections.create(otherOrgId, detectionInput({ name: "Theirs" })) + + assert.deepStrictEqual( + Arr.map(yield* detections.list(orgId), (detection) => detection.id), + [second.id, first.id], + ) + }).pipe(Effect.provide(layer)) + }) + + it.effect("a detection with no filters counts every event", () => { + const { layer } = harness() + return Effect.gen(function* () { + const detections = yield* AuditDetectionsService + const created = yield* detections.create(orgId, { + name: "Any event", + windowMinutes: 5, + threshold: 100, + severity: "critical", + createdBy: userId, + }) + + assert.deepStrictEqual(created.filters, {}) + }).pipe(Effect.provide(layer)) + }) + + it.effect("updates only the fields sent, replacing the filters whole, under the editor's name", () => { + const { layer, testDb } = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const detections = yield* AuditDetectionsService + const created = yield* detections.create(orgId, detectionInput()) + yield* TestClock.adjust(Duration.minutes(5)) + + const updated = yield* detections.update(orgId, created.id, { + threshold: 5, + filters: { outcome: "denied" }, + enabled: false, + updatedBy: editorId, + }) + + assert.deepStrictEqual(updated, { + ...created, + threshold: 5, + filters: { outcome: "denied" }, + enabled: false, + updatedAt: DEFAULT_CLOCK_EPOCH_MS + 5 * 60_000, + }) + const row = yield* ruleRow(testDb, created.id) + assert.strictEqual(row?.created_by, userId) + assert.strictEqual(row?.updated_by, editorId) + assert.deepStrictEqual(row?.audit_filters_json, { outcome: "denied" }) + }).pipe(Effect.provide(layer)) + }) + + // alert_rules is synced to every member's browser: a save that changes nothing writes nothing. + it.effect("leaves the row untouched when an update changes nothing", () => { + const { layer, testDb } = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const detections = yield* AuditDetectionsService + const created = yield* detections.create(orgId, detectionInput()) + yield* TestClock.adjust(Duration.minutes(5)) + + const updated = yield* detections.update(orgId, created.id, { + name: created.name, + threshold: created.threshold, + updatedBy: editorId, + }) + + assert.deepStrictEqual(updated, created) + assert.strictEqual((yield* ruleRow(testDb, created.id))?.updated_by, userId) + }).pipe(Effect.provide(layer)) + }) + + // The floor is what stops a detection firing on events from before it counted them. It has + // to move when the detection counts more, and only then. + it.effect("starts counting at its creation, and again when it counts more than it did", () => { + const { layer, testDb } = harness() + const countFrom = (id: AlertRuleId) => + Effect.promise(() => + queryFirstRow<{ floor_ms: number }>( + testDb, + "select (extract(epoch from audit_count_from) * 1000)::float8 as floor_ms from alert_rules where id = $1", + [id], + ), + ).pipe(Effect.map((row) => row?.floor_ms)) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const detections = yield* AuditDetectionsService + const created = yield* detections.create(orgId, detectionInput()) + assert.strictEqual(yield* countFrom(created.id), DEFAULT_CLOCK_EPOCH_MS) + const edit = (change: Omit[2], "updatedBy">) => + Effect.gen(function* () { + yield* TestClock.adjust(Duration.minutes(1)) + yield* detections.update(orgId, created.id, { ...change, updatedBy: editorId }) + const floor = yield* countFrom(created.id) + return floor === undefined ? undefined : (floor - DEFAULT_CLOCK_EPOCH_MS) / 60_000 + }) + + // Minutes after creation the floor sits at, after each edit one minute apart. + assert.strictEqual(yield* edit({ name: "Renamed", severity: "critical", threshold: 1 }), 0) + assert.strictEqual(yield* edit({ windowMinutes: 5 }), 0) + assert.strictEqual(yield* edit({ windowMinutes: 30 }), 3) + assert.strictEqual(yield* edit({ filters: { category: "credential" } }), 4) + assert.strictEqual(yield* edit({ enabled: false }), 4) + assert.strictEqual(yield* edit({ enabled: true }), 6) + }).pipe(Effect.provide(layer)) + }) + + it.effect("refuses an unknown destination and saves nothing", () => { + const { layer } = harness() + return Effect.gen(function* () { + const detections = yield* AuditDetectionsService + const unknown = AlertDestinationId.make("00000000-0000-4000-8000-000000000000") + + const refused = yield* detections + .create(orgId, detectionInput({ destinationIds: [unknown] })) + .pipe(Effect.flip) + assert.strictEqual(refused._tag, "@maple/http/errors/AuditDetectionInvalidError") + assert.lengthOf(yield* detections.list(orgId), 0) + + const created = yield* detections.create(orgId, detectionInput()) + const refusedEdit = yield* detections + .update(orgId, created.id, { destinationIds: [unknown], updatedBy: userId }) + .pipe(Effect.flip) + assert.strictEqual(refusedEdit._tag, "@maple/http/errors/AuditDetectionInvalidError") + assert.deepStrictEqual(yield* detections.get(orgId, created.id), created) + }).pipe(Effect.provide(layer)) + }) + + // Another org's destination must read as unknown, not as usable. + it.effect("refuses a destination that belongs to another org", () => { + const { layer } = harness() + return Effect.gen(function* () { + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const theirs = yield* createWebhookDestination(alerts, otherOrgId, userId) + + const refused = yield* detections + .create(orgId, detectionInput({ destinationIds: [theirs.id] })) + .pipe(Effect.flip) + + assert.strictEqual(refused._tag, "@maple/http/errors/AuditDetectionInvalidError") + }).pipe(Effect.provide(layer)) + }) + + // Rule names are unique per org across every kind of rule: the index would answer a 500. + it.effect("refuses a name another detection or another alert rule already has", () => { + const { layer } = harness() + return Effect.gen(function* () { + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const destination = yield* createWebhookDestination(alerts, orgId, userId) + const rule = yield* createErrorRateRule(alerts, orgId, userId, destination.id) + yield* detections.create(orgId, detectionInput({ name: "Members added" })) + const other = yield* detections.create(orgId, detectionInput({ name: "Tokens created" })) + + const duplicate = yield* detections + .create(orgId, detectionInput({ name: "Members added" })) + .pipe(Effect.flip) + const sameAsRule = yield* detections + .create(orgId, detectionInput({ name: rule.name })) + .pipe(Effect.flip) + const renamed = yield* detections + .update(orgId, other.id, { name: "Members added", updatedBy: userId }) + .pipe(Effect.flip) + + assert.deepStrictEqual( + [duplicate._tag, sameAsRule._tag, renamed._tag], + [ + "@maple/http/errors/AuditDetectionInvalidError", + "@maple/http/errors/AuditDetectionInvalidError", + "@maple/http/errors/AuditDetectionInvalidError", + ], + ) + // The same name is free in another org. + yield* detections.create(otherOrgId, detectionInput({ name: "Members added" })) + }).pipe(Effect.provide(layer)) + }) + + it.effect("caps an org's detections and leaves another org's room alone", () => { + const { layer } = harness() + return Effect.gen(function* () { + const detections = yield* AuditDetectionsService + const create = (index: number) => + detections.create(orgId, detectionInput({ name: `Detection ${index}` })) + yield* Effect.forEach(Arr.range(1, MAX_AUDIT_DETECTIONS_PER_ORG), create, { discard: true }) + + const refused = yield* create(MAX_AUDIT_DETECTIONS_PER_ORG + 1).pipe(Effect.flip) + + assert.strictEqual(refused._tag, "@maple/http/errors/AuditDetectionInvalidError") + assert.lengthOf(yield* detections.list(orgId), MAX_AUDIT_DETECTIONS_PER_ORG) + yield* detections.create(otherOrgId, detectionInput()) + }).pipe(Effect.provide(layer)) + }) + + it.effect("removes the rule with its state and incidents, open ones included", () => { + const { layer, testDb } = harness(3) + return Effect.gen(function* () { + yield* TestClock.setTime(DEFAULT_CLOCK_EPOCH_MS) + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const created = yield* detections.create(orgId, detectionInput()) + yield* alerts.runSchedulerTick() + const open = (yield* alerts.listIncidents(orgId)).incidents + assert.deepStrictEqual( + Arr.map(open, (incident) => [incident.ruleId, incident.status]), + [[created.id, "open"]], + ) + + assert.strictEqual(yield* detections.remove(orgId, created.id), created.id) + + const left = yield* Effect.promise(() => + queryFirstRow<{ rules: number; incidents: number; states: number }>( + testDb, + `select (select count(*)::int from alert_rules where id = $1) as rules, + (select count(*)::int from alert_incidents where rule_id = $1) as incidents, + (select count(*)::int from alert_rule_states where rule_id = $1) as states`, + [created.id], + ), + ) + assert.deepStrictEqual(left, { rules: 0, incidents: 0, states: 0 }) + assert.lengthOf(yield* detections.list(orgId), 0) + }).pipe(Effect.provide(layer)) + }) + + it.effect("answers not found for an id that is not one of the org's detections", () => { + const { layer } = harness() + return Effect.gen(function* () { + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const destination = yield* createWebhookDestination(alerts, orgId, userId) + // Another kind of rule is edited and deleted through its own owner, never from here. + const rule = yield* createErrorRateRule(alerts, orgId, userId, destination.id) + const theirs = yield* detections.create(otherOrgId, detectionInput()) + + const outcomes = yield* Effect.all([ + refusal(detections.get(orgId, theirs.id)), + refusal(detections.update(orgId, theirs.id, { threshold: 9, updatedBy: userId })), + refusal(detections.remove(orgId, theirs.id)), + refusal(detections.update(orgId, rule.id, { threshold: 9, updatedBy: userId })), + refusal(detections.remove(orgId, rule.id)), + ]) + + assert.deepStrictEqual(Arr.dedupe(outcomes), ["@maple/http/errors/AuditDetectionNotFoundError"]) + assert.lengthOf((yield* alerts.listRules(orgId)).rules, 1) + assert.strictEqual((yield* detections.get(otherOrgId, theirs.id)).threshold, 3) + }).pipe(Effect.provide(layer)) + }) + + it.effect( + "the generic rule APIs refuse to create, edit or delete a detection's rule and do not list it", + () => { + const { layer } = harness() + return Effect.gen(function* () { + const alerts = yield* alertServices + const detections = yield* AuditDetectionsService + const created = yield* detections.create(orgId, detectionInput()) + const request = new AlertRuleUpsertRequest({ + name: "Sneaky detection", + severity: "critical", + signalType: "audit", + comparator: "gte", + threshold: 1, + windowMinutes: 5, + }) + + assert.lengthOf((yield* alerts.listRules(orgId)).rules, 0) + const refusedCreate = yield* alerts + .createRule(orgId, userId, adminRoles, request) + .pipe(Effect.flip) + const refusedEdit = yield* alerts + .updateRule(orgId, userId, adminRoles, created.id, request) + .pipe(Effect.flip) + const refusedDelete = yield* alerts + .deleteRule(orgId, adminRoles, created.id) + .pipe(Effect.flip) + + assert.deepStrictEqual( + [refusedCreate._tag, refusedEdit._tag, refusedDelete._tag], + [ + "@maple/http/errors/AlertValidationError", + "@maple/http/errors/AlertValidationError", + "@maple/http/errors/AlertRuleNotFoundError", + ], + ) + // The refusal says where a detection is managed instead. + assert.include(refusedEdit.message, "Security hub") + assert.deepStrictEqual(yield* detections.list(orgId), [created]) + }).pipe(Effect.provide(layer)) + }, + ) +}) diff --git a/packages/backend/src/services/audit-trail/AuditDetectionsService.ts b/packages/backend/src/services/audit-trail/AuditDetectionsService.ts new file mode 100644 index 0000000000..3fb0d62173 --- /dev/null +++ b/packages/backend/src/services/audit-trail/AuditDetectionsService.ts @@ -0,0 +1,317 @@ +import { randomUUID } from "node:crypto" +import * as PG from "@maple-dev/effect-orm/postgres" +import type { MapleDb } from "@maple/db/client" +import { AlertDestinations, AlertRuleClaims, AlertRules } from "@maple/db/tables" +import type { AuditDetectionFilters } from "@maple/domain/audit-trail" +import { + type AlertSeverity, + type AuditDetection, + AuditDetectionInvalidError, + AuditDetectionNotFoundError, + AuditTrailPersistenceError, + MAX_AUDIT_DETECTIONS_PER_ORG, +} from "@maple/domain/http" +import { type AlertDestinationId, AlertRuleId, type OrgId, type UserId } from "@maple/primitives" +import { Array as Arr, Clock, Context, Effect, Layer, Option } from "effect" +import { Database } from "@maple/backend/platform/DatabaseLive" +import { makeDbExecute, makePersistenceErrorMapper } from "@maple/backend/platform/db-execute" +import { deleteRuleRows } from "@maple/backend/services/alerts/AlertRulesService" +import { lockOrgAlertRules } from "@maple/backend/services/alerts/alert-persistence" +import { + auditDetectionFromRow, + auditDetectionRuleFields, + type AuditDetectionRuleInput, +} from "./audit-detection-rule" + +export interface CreateAuditDetectionInput { + readonly name: string + readonly enabled?: boolean + readonly filters?: AuditDetectionFilters + readonly windowMinutes: number + readonly threshold: number + readonly severity: AlertSeverity + readonly destinationIds?: ReadonlyArray + readonly createdBy: UserId +} + +export interface UpdateAuditDetectionInput { + readonly name?: string + readonly enabled?: boolean + /** Replaces the stored filters whole. */ + readonly filters?: AuditDetectionFilters + readonly windowMinutes?: number + readonly threshold?: number + readonly severity?: AlertSeverity + readonly destinationIds?: ReadonlyArray + readonly updatedBy: UserId +} + +type DetectionReadError = AuditDetectionNotFoundError | AuditTrailPersistenceError + +export interface AuditDetectionsServiceApi { + /** One org's detections, newest first. */ + readonly list: (orgId: OrgId) => Effect.Effect, AuditTrailPersistenceError> + readonly get: (orgId: OrgId, id: AlertRuleId) => Effect.Effect + readonly create: ( + orgId: OrgId, + input: CreateAuditDetectionInput, + ) => Effect.Effect + readonly update: ( + orgId: OrgId, + id: AlertRuleId, + input: UpdateAuditDetectionInput, + ) => Effect.Effect + /** Deletes the rule with its state, incidents and deliveries, open incidents included. */ + readonly remove: (orgId: OrgId, id: AlertRuleId) => Effect.Effect +} + +const makeDetectionPersistenceError = makePersistenceErrorMapper( + AuditTrailPersistenceError, + "Audit detection persistence failed", +) + +const invalid = (message: string) => new AuditDetectionInvalidError({ message }) +const notFound = (detectionId: AlertRuleId) => + new AuditDetectionNotFoundError({ detectionId, message: "Detection not found" }) +const unreadable = () => new AuditTrailPersistenceError({ message: "The stored detection cannot be read" }) + +/** The columns a detection reads back from; add a `where` to it. */ +const selectDetections = () => + PG.from(AlertRules).select(($) => ({ + id: $.id, + name: $.name, + enabled: $.enabled, + auditFiltersJson: PG.undecoded($.auditFiltersJson), + windowMinutes: $.windowMinutes, + threshold: $.threshold, + thresholdsJson: $.thresholdsJson, + createdAt: $.createdAt, + updatedAt: $.updatedAt, + })) + +const selectDetection = (orgId: OrgId, id: AlertRuleId) => + selectDetections() + .where(($) => [$.orgId.eq(orgId), $.id.eq(id), $.signalType.eq("audit")]) + .limit(1) + +/** Checks a detection write: its destinations exist and its name is free among alert rules. */ +const claimRuleWrite = Effect.fnUntraced(function* ( + db: MapleDb, + orgId: OrgId, + ruleId: AlertRuleId, + rule: AuditDetectionRuleInput, +) { + if (rule.destinationIds.length > 0) { + const found = yield* db.run( + PG.from(AlertDestinations) + .select("id") + .where(($) => [$.orgId.eq(orgId), $.id.in_(...rule.destinationIds)]), + ) + const known = new Set(Arr.map(found, (row) => row.id)) + const missing = Arr.findFirst(rule.destinationIds, (id) => !known.has(id)) + if (Option.isSome(missing)) return yield* invalid(`Unknown alert destination ${missing.value}`) + } + // Alert rule names are unique per org, across every kind of rule. + const sameName = yield* db.run( + PG.from(AlertRules) + .select("id") + .where(($) => [$.orgId.eq(orgId), $.name.eq(rule.name), $.id.neq(ruleId)]) + .limit(1), + ) + if (sameName.length > 0) { + return yield* invalid( + `A detection or alert rule named '${rule.name}' already exists; pick another name`, + ) + } +}) + +const sameIds = (a: ReadonlyArray, b: ReadonlyArray) => + a.length === b.length && Arr.every(a, (id, index) => id === b[index]) + +const sameFilters = (a: AuditDetectionFilters, b: AuditDetectionFilters) => + a.provider === b.provider && a.category === b.category && a.outcome === b.outcome && a.action === b.action + +/** Audit log detections: each one is a hidden `audit` alert rule, written only from here. */ +export class AuditDetectionsService extends Context.Service< + AuditDetectionsService, + AuditDetectionsServiceApi +>()("@maple/backend/services/audit-trail/AuditDetectionsService", { + make: Effect.gen(function* () { + const database = yield* Database + const dbExecute = makeDbExecute(database, "AuditDetectionsService", makeDetectionPersistenceError) + + const selectOne = Effect.fnUntraced(function* (orgId: OrgId, id: AlertRuleId) { + const rows = yield* dbExecute((db) => db.run(selectDetection(orgId, id))) + const row = rows[0] + if (row === undefined) return yield* notFound(id) + const detection = auditDetectionFromRow(row) + if (Option.isNone(detection)) return yield* unreadable() + return detection.value + }) + + const list = Effect.fn("AuditDetectionsService.list")(function* (orgId: OrgId) { + yield* Effect.annotateCurrentSpan({ orgId }) + const rows = yield* dbExecute((db) => + db.run( + selectDetections() + .where(($) => [$.orgId.eq(orgId), $.signalType.eq("audit")]) + .orderBy(["createdAt", "desc"], ["id", "desc"]), + ), + ) + // A row this build cannot read is left out, so the rest of the list still shows. + return Arr.getSomes(Arr.map(rows, auditDetectionFromRow)) + }) + + const get = Effect.fn("AuditDetectionsService.get")(function* (orgId: OrgId, id: AlertRuleId) { + yield* Effect.annotateCurrentSpan({ orgId, "maple.alert.rule_id": id }) + return yield* selectOne(orgId, id) + }) + + const create = Effect.fn("AuditDetectionsService.create")(function* ( + orgId: OrgId, + input: CreateAuditDetectionInput, + ) { + yield* Effect.annotateCurrentSpan({ orgId }) + const now = yield* Clock.currentTimeMillis + const id = AlertRuleId.make(randomUUID()) + const rule: AuditDetectionRuleInput = { + name: input.name, + enabled: input.enabled ?? true, + filters: input.filters ?? {}, + windowMinutes: input.windowMinutes, + threshold: input.threshold, + severity: input.severity, + destinationIds: Arr.dedupe(input.destinationIds ?? []), + } + yield* dbExecute((db) => + db.transaction( + Effect.gen(function* () { + yield* lockOrgAlertRules(db, orgId) + const [existing] = yield* db.run( + PG.from(AlertRules) + .select(() => ({ count: PG.count() })) + .where(($) => [$.orgId.eq(orgId), $.signalType.eq("audit")]), + ) + if (Number(existing?.count ?? 0) >= MAX_AUDIT_DETECTIONS_PER_ORG) { + return yield* invalid( + `An organization can have at most ${MAX_AUDIT_DETECTIONS_PER_ORG} detections`, + ) + } + yield* claimRuleWrite(db, orgId, id, rule) + yield* db.run( + PG.insertInto(AlertRules).values({ + id, + orgId, + ...auditDetectionRuleFields(rule), + // Counts from here: what is already in the log did not happen on its watch. + auditCountFrom: now, + lastScheduledAt: null, + createdAt: now, + updatedAt: now, + createdBy: input.createdBy, + updatedBy: input.createdBy, + }), + ) + }), + ), + ) + return yield* selectOne(orgId, id).pipe( + Effect.catchTag("@maple/http/errors/AuditDetectionNotFoundError", () => + Effect.fail(new AuditTrailPersistenceError({ message: "Created detection not found" })), + ), + ) + }) + + const update = Effect.fn("AuditDetectionsService.update")(function* ( + orgId: OrgId, + id: AlertRuleId, + input: UpdateAuditDetectionInput, + ) { + yield* Effect.annotateCurrentSpan({ orgId, "maple.alert.rule_id": id }) + const now = yield* Clock.currentTimeMillis + yield* dbExecute((db) => + db.transaction( + Effect.gen(function* () { + // Lock, then re-read: the rule is rebuilt from a row no other write changes + // before commit, and a concurrent delete either finished first or waits. + yield* lockOrgAlertRules(db, orgId) + const rows = yield* db.run(selectDetection(orgId, id)) + const row = rows[0] + if (row === undefined) return yield* notFound(id) + const stored = auditDetectionFromRow(row) + if (Option.isNone(stored)) return yield* unreadable() + const locked = stored.value + const rule: AuditDetectionRuleInput = { + name: input.name ?? locked.name, + enabled: input.enabled ?? locked.enabled, + filters: input.filters ?? locked.filters, + windowMinutes: input.windowMinutes ?? locked.windowMinutes, + threshold: input.threshold ?? locked.threshold, + severity: input.severity ?? locked.severity, + destinationIds: + input.destinationIds === undefined + ? locked.destinationIds + : Arr.dedupe(input.destinationIds), + } + // The rule is written only when it changes: alert_rules is synced to browsers. + const changed = + rule.name !== locked.name || + rule.enabled !== locked.enabled || + !sameFilters(rule.filters, locked.filters) || + rule.windowMinutes !== locked.windowMinutes || + rule.threshold !== locked.threshold || + rule.severity !== locked.severity || + !sameIds(rule.destinationIds, locked.destinationIds) + if (!changed) return + // The floor moves only when the detection starts counting events it did not + // before. A rename or a new severity keeps the window, and an open incident. + const countsMore = + !sameFilters(rule.filters, locked.filters) || + rule.windowMinutes > locked.windowMinutes || + (rule.enabled && !locked.enabled) + yield* claimRuleWrite(db, orgId, id, rule) + // Ends a tick's claim on the old config, as a rule edit does. + yield* db.run(PG.deleteFrom(AlertRuleClaims).where(($) => [$.ruleId.eq(id)])) + yield* db.run( + PG.update(AlertRules) + .set({ + ...auditDetectionRuleFields(rule), + ...(countsMore ? { auditCountFrom: now } : undefined), + updatedAt: now, + updatedBy: input.updatedBy, + }) + .where(($) => [$.orgId.eq(orgId), $.id.eq(id)]), + ) + }), + ), + ) + return yield* selectOne(orgId, id) + }) + + const remove = Effect.fn("AuditDetectionsService.remove")(function* (orgId: OrgId, id: AlertRuleId) { + yield* Effect.annotateCurrentSpan({ orgId, "maple.alert.rule_id": id }) + yield* dbExecute((db) => + db.transaction( + Effect.gen(function* () { + yield* lockOrgAlertRules(db, orgId) + // Only a detection's rule: another kind of rule is deleted through its owner. + const rows = yield* db.run( + PG.from(AlertRules) + .select("id") + .where(($) => [$.orgId.eq(orgId), $.id.eq(id), $.signalType.eq("audit")]) + .limit(1), + ) + if (rows.length === 0) return yield* notFound(id) + // The rule delete's own cascade: state, deliveries and incidents go with it. + yield* deleteRuleRows(db, orgId, id) + }), + ), + ) + return id + }) + + return { list, get, create, update, remove } satisfies AuditDetectionsServiceApi + }), +}) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend/src/services/audit-trail/AuditTrailService.test.ts b/packages/backend/src/services/audit-trail/AuditTrailService.test.ts index 67ed89a3d4..86312c29c1 100644 --- a/packages/backend/src/services/audit-trail/AuditTrailService.test.ts +++ b/packages/backend/src/services/audit-trail/AuditTrailService.test.ts @@ -206,6 +206,82 @@ describe("AuditTrailService.list", () => { ) }) +describe("AuditTrailService.count", () => { + const window = { + startMs: Date.UTC(2026, 7, 29, 9, 0, 0), + endMs: Date.UTC(2026, 7, 29, 9, 10, 0), + } + + it.effect("returns the number the warehouse counted, whichever way it spells it", () => + Effect.gen(function* () { + const asNumber = yield* withTrail(fakeWarehouse([{ events: 3 }]), (trail) => + trail.count(ORG, {}, window), + ) + // UInt64 arrives as a string from a warehouse that quotes 64-bit integers. + const asString = yield* withTrail(fakeWarehouse([{ events: "12" }]), (trail) => + trail.count(ORG, {}, window), + ) + + expect([asNumber, asString]).toEqual([3, 12]) + }), + ) + + // A redelivery is a second row until the table merges it: counting rows would fire a + // detection on one event delivered twice. + it.effect("counts distinct events that arrived after the window's start, up to its end", () => + Effect.gen(function* () { + const warehouse = fakeWarehouse([{ events: 0 }]) + yield* withTrail(warehouse, (trail) => trail.count(ORG, {}, window)) + + expect(warehouse.sql[0]).toContain("uniqExact(audit_trail_events.Id)") + expect(warehouse.sql[0]).toContain(`OrgId = '${ORG}'`) + expect(warehouse.sql[0]).toContain("IngestedAt > '2026-08-29 09:00:00'") + expect(warehouse.sql[0]).toContain("IngestedAt <= '2026-08-29 09:10:00'") + }), + ) + + // The table is partitioned and sorted by when an event happened, not when it arrived. + it.effect("bounds when the events happened to a week before the window, so partitions prune", () => + Effect.gen(function* () { + const warehouse = fakeWarehouse([{ events: 0 }]) + yield* withTrail(warehouse, (trail) => trail.count(ORG, {}, window)) + + expect(warehouse.sql[0]).toContain("OccurredAt >= '2026-08-29 09:00:00' - INTERVAL 604800 SECOND") + }), + ) + + it.effect("applies only the filters the detection sets", () => + Effect.gen(function* () { + const warehouse = fakeWarehouse([{ events: 0 }]) + yield* withTrail(warehouse, (trail) => + Effect.gen(function* () { + yield* trail.count(ORG, { provider: "vercel", action: "team.member.added" }, window) + yield* trail.count(ORG, { category: "membership", outcome: "denied" }, window) + }), + ) + const [byAction, byCategory] = warehouse.sql + + expect(byAction).toContain("Provider = 'vercel'") + expect(byAction).toContain("Action = 'team.member.added'") + expect(byAction).not.toContain("Category =") + expect(byAction).not.toContain("Outcome =") + expect(byCategory).toContain("Category = 'membership'") + expect(byCategory).toContain("Outcome = 'denied'") + expect(byCategory).not.toContain("Provider =") + }), + ) + + it.effect("fails with a persistence error when the warehouse is down", () => + Effect.gen(function* () { + const error = yield* withTrail(fakeWarehouse([], true), (trail) => + trail.count(ORG, {}, window), + ).pipe(Effect.flip) + + expect(error._tag).toBe("@maple/http/errors/AuditTrailPersistenceError") + }), + ) +}) + describe("AuditTrailService.write", () => { const event: AuditTrailEvent = { provider: "maple", diff --git a/packages/backend/src/services/audit-trail/AuditTrailService.ts b/packages/backend/src/services/audit-trail/AuditTrailService.ts index a27854a288..4bf7c5e274 100644 --- a/packages/backend/src/services/audit-trail/AuditTrailService.ts +++ b/packages/backend/src/services/audit-trail/AuditTrailService.ts @@ -1,4 +1,4 @@ -import type { AuditTrailEvent } from "@maple/domain/audit-trail" +import type { AuditDetectionFilters, AuditTrailEvent } from "@maple/domain/audit-trail" import { AuditTrailPersistenceError } from "@maple/domain/http" import type { AuditTrailEntry } from "@maple/domain/http" import type { OrgId } from "@maple/primitives" @@ -48,6 +48,12 @@ export interface AuditTrailServiceApi { orgId: OrgId, filters: AuditTrailListFilters, ) => Effect.Effect + /** How many matching events arrived in `(startMs, endMs]`; a redelivered event counts once. */ + readonly count: ( + orgId: OrgId, + filters: AuditDetectionFilters, + window: { readonly startMs: number; readonly endMs: number }, + ) => Effect.Effect } const persistenceError = (message: string) => (error: { readonly _tag: string }) => @@ -146,7 +152,34 @@ export class AuditTrailService extends Context.Service = {}): AuditDetectionRuleInput => ({ + name: "Members added outside business hours", + enabled: true, + filters: { provider: "vercel", category: "membership" }, + windowMinutes: 10, + threshold: 3, + severity: "critical", + destinationIds: [PAGER], + ...overrides, +}) + +/** The row a detection reads back from, as the database returns the columns `fields` wrote. */ +const storedRow = ( + fields: ReturnType, + overrides: Partial = {}, +): AuditDetectionRuleRow => ({ + id: RULE_ID, + name: fields.name, + enabled: fields.enabled, + auditFiltersJson: fields.auditFiltersJson, + windowMinutes: fields.windowMinutes, + threshold: fields.threshold, + thresholdsJson: fields.thresholdsJson, + createdAt: 1_700_000_000_000, + updatedAt: 1_700_000_060_000, + ...overrides, +}) + +describe("auditDetectionRuleFields", () => { + it("is a managed audit rule that fires at the event count, one tier to the detection's destinations", () => { + const fields = auditDetectionRuleFields(input()) + + expect(fields).toMatchObject({ + signalType: "audit", + comparator: "gte", + threshold: 3, + severity: "critical", + windowMinutes: 10, + auditFiltersJson: { provider: "vercel", category: "membership" }, + destinationIdsJson: [PAGER], + thresholdsJson: [{ severity: "critical", value: 3, destinationIds: [PAGER] }], + }) + }) + + // One event is enough to open and an empty window enough to resolve: nothing has to wait + // for a second check, and a count of zero must never be skipped for too few samples. + it("opens and resolves on a single check, with no minimum sample count", () => { + const fields = auditDetectionRuleFields(input()) + + expect(fields).toMatchObject({ + evaluationIntervalMinutes: 1, + minimumSampleCount: 0, + consecutiveBreachesRequired: 1, + consecutiveHealthyRequired: 1, + breachesOutOf: null, + }) + }) + + it("carries no query, service scope or grouping: the evaluator counts the audit trail", () => { + const fields = auditDetectionRuleFields(input()) + + expect(fields).toMatchObject({ + querySpecJson: null, + rawQuerySql: null, + queryBuilderDraftJson: null, + serviceNamesJson: null, + environmentsJson: null, + groupBy: null, + }) + }) +}) + +describe("auditDetectionFromRow", () => { + it("reads back the detection its rule fields were written from", () => { + const detection = auditDetectionFromRow(storedRow(auditDetectionRuleFields(input()))) + + expect(Option.getOrNull(detection)).toEqual({ + id: RULE_ID, + name: "Members added outside business hours", + enabled: true, + filters: { provider: "vercel", category: "membership" }, + windowMinutes: 10, + threshold: 3, + severity: "critical", + destinationIds: [PAGER], + createdAt: 1_700_000_000_000, + updatedAt: 1_700_000_060_000, + }) + }) + + it("reads a rule stored without a filters column as counting every event", () => { + const row = storedRow(auditDetectionRuleFields(input()), { auditFiltersJson: null }) + + expect(Option.getOrNull(auditDetectionFromRow(row))?.filters).toEqual({}) + }) + + // Showing a guess would let the next save overwrite what is stored. + it("does not read a rule whose filters or tier this build cannot decode", () => { + const fields = auditDetectionRuleFields(input()) + + expect( + Option.isNone( + auditDetectionFromRow( + storedRow(fields, { auditFiltersJson: { provider: "a-new-platform" } }), + ), + ), + ).toBe(true) + expect(Option.isNone(auditDetectionFromRow(storedRow(fields, { thresholdsJson: null })))).toBe(true) + }) +}) + +describe("auditCountReason", () => { + it("says how many events arrived and over which window", () => { + expect(auditCountReason(3, 10)).toBe("3 matching audit events in the last 10 minutes") + expect(auditCountReason(1, 60)).toBe("1 matching audit event in the last 60 minutes") + expect(auditCountReason(0, 5)).toBe("0 matching audit events in the last 5 minutes") + }) +}) diff --git a/packages/backend/src/services/audit-trail/audit-detection-rule.ts b/packages/backend/src/services/audit-trail/audit-detection-rule.ts new file mode 100644 index 0000000000..74149755bb --- /dev/null +++ b/packages/backend/src/services/audit-trail/audit-detection-rule.ts @@ -0,0 +1,118 @@ +// The hidden `audit` alert rule behind each audit log detection, and how its row reads back. +import { AuditDetectionFilters } from "@maple/domain/audit-trail" +import { + AlertSeverity, + AlertThresholds, + type AlertDestinationId, + type AlertRuleId, + type AuditDetection, +} from "@maple/domain/http" +import { Array as Arr, Option, Schema } from "effect" +import { MANAGED_RULE_PLAN } from "@maple/backend/services/alerts/AlertRuleModel" + +export interface AuditDetectionRuleInput { + readonly name: string + readonly enabled: boolean + readonly filters: AuditDetectionFilters + readonly windowMinutes: number + readonly threshold: number + readonly severity: AlertSeverity + readonly destinationIds: ReadonlyArray +} + +/** + * The `alert_rules` columns of a detection's rule: one tier at the event count, checked every + * minute. One check at the count opens and one below it resolves: the window already smooths + * it, and a day between reminders keeps a long-running match from paging every half hour. + */ +export const auditDetectionRuleFields = (input: AuditDetectionRuleInput) => { + const thresholds: AlertThresholds = [ + { + severity: input.severity, + value: input.threshold, + upper: null, + recoveryValue: null, + destinationIds: [...input.destinationIds], + }, + ] + return { + name: input.name, + notes: null, + notificationTemplateJson: null, + enabled: input.enabled, + severity: input.severity, + serviceNamesJson: null, + excludeServiceNamesJson: null, + environmentsJson: null, + auditFiltersJson: input.filters, + tagsJson: null, + groupBy: null, + signalType: "audit", + comparator: "gte", + threshold: input.threshold, + thresholdUpper: null, + thresholdsJson: thresholds, + windowMinutes: input.windowMinutes, + evaluationIntervalMinutes: 1, + slowedIntervalMinutes: null, + slowedAt: null, + minimumSampleCount: 0, + consecutiveBreachesRequired: 1, + breachesOutOf: null, + consecutiveHealthyRequired: 1, + renotifyIntervalMinutes: 1440, + apdexThresholdMs: null, + queryBuilderDraftJson: null, + rawQuerySql: null, + destinationIdsJson: [...input.destinationIds], + querySpecJson: null, + reducer: MANAGED_RULE_PLAN.reducer, + sampleCountStrategy: null, + noDataBehavior: MANAGED_RULE_PLAN.noDataBehavior, + } as const +} + +/** The columns a detection reads back from; the jsonb ones undecoded, so one bad row fails alone. */ +export interface AuditDetectionRuleRow { + readonly id: AlertRuleId + readonly name: string + readonly enabled: boolean + readonly auditFiltersJson: unknown + readonly windowMinutes: number + readonly threshold: number + readonly thresholdsJson: unknown + readonly createdAt: number + readonly updatedAt: number +} + +const decodeFilters = Schema.decodeUnknownOption(AuditDetectionFilters) +const decodeThresholds = Schema.decodeUnknownOption(AlertThresholds) + +/** + * A stored rule as the detection it is. None when its tier or filters do not read: showing a + * guess would let a save overwrite what is stored. No filters column counts every event. + */ +export const auditDetectionFromRow = (row: AuditDetectionRuleRow): Option.Option => + Option.all({ + filters: row.auditFiltersJson == null ? Option.some({}) : decodeFilters(row.auditFiltersJson), + tier: Option.flatMap(decodeThresholds(row.thresholdsJson), Arr.head), + }).pipe( + Option.map(({ filters, tier }) => ({ + id: row.id, + name: row.name, + enabled: row.enabled, + filters, + windowMinutes: row.windowMinutes, + threshold: row.threshold, + severity: tier.severity, + destinationIds: tier.destinationIds, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + })), + ) + +const formatMinutes = (minutes: number) => (minutes === 1 ? "minute" : `${minutes} minutes`) + +/** What one check of a detection saw, as the incident and its notifications say it. */ +export const auditCountReason = (count: number, windowMinutes: number): string => + `${count} matching audit ${count === 1 ? "event" : "events"} in the last ${formatMinutes(windowMinutes)}` diff --git a/packages/db/drizzle/20261113122938_alert_rules_audit_detection/migration.sql b/packages/db/drizzle/20261113122938_alert_rules_audit_detection/migration.sql new file mode 100644 index 0000000000..03992747cb --- /dev/null +++ b/packages/db/drizzle/20261113122938_alert_rules_audit_detection/migration.sql @@ -0,0 +1,5 @@ +-- metadata +ALTER TABLE "alert_rules" ADD COLUMN IF NOT EXISTS "audit_filters_json" jsonb; +--> statement-breakpoint +-- metadata +ALTER TABLE "alert_rules" ADD COLUMN IF NOT EXISTS "audit_count_from" timestamp with time zone; diff --git a/packages/db/drizzle/20261113122938_alert_rules_audit_detection/snapshot.json b/packages/db/drizzle/20261113122938_alert_rules_audit_detection/snapshot.json new file mode 100644 index 0000000000..054e3a849b --- /dev/null +++ b/packages/db/drizzle/20261113122938_alert_rules_audit_detection/snapshot.json @@ -0,0 +1,22611 @@ +{ + "version": "1", + "dialect": "postgres", + "id": "40fb0af3a14abb9406279f8005c4fd9abdb47349652f3f3e18369df9450ed873", + "prevIds": [ + "67bf09ed830b16a5d08ab4b0bc5eb1cf7cdb14d990a5a5d8817a133b73c9b593" + ], + "entities": [ + { + "kind": "table", + "name": "actors", + "primaryKey": { + "name": "actors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "agent_feedback", + "primaryKey": { + "name": "agent_feedback_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "ai_triage_settings", + "primaryKey": { + "name": "ai_triage_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "alert_delivery_events", + "primaryKey": { + "name": "alert_delivery_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_destinations", + "primaryKey": { + "name": "alert_destinations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_escalation_runs", + "primaryKey": { + "name": "alert_escalation_runs_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_incident_events", + "primaryKey": { + "name": "alert_incident_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_incidents", + "primaryKey": { + "name": "alert_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_linear_issues", + "primaryKey": { + "name": "alert_linear_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_maintenance_windows", + "primaryKey": { + "name": "alert_maintenance_windows_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_member_notifications", + "primaryKey": { + "name": "alert_member_notifications_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_rule_claims", + "primaryKey": { + "name": "alert_rule_claims_pkey", + "columns": [ + "rule_id" + ] + } + }, + { + "kind": "table", + "name": "alert_rule_states", + "primaryKey": { + "name": "alert_rule_states_org_id_rule_id_group_key_pk", + "columns": [ + "org_id", + "rule_id", + "group_key" + ] + } + }, + { + "kind": "table", + "name": "alert_rules", + "primaryKey": { + "name": "alert_rules_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "alert_silences", + "primaryKey": { + "name": "alert_silences_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "anomaly_detector_settings", + "primaryKey": { + "name": "anomaly_detector_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "anomaly_detector_states", + "primaryKey": { + "name": "anomaly_detector_states_org_id_detector_key_pk", + "columns": [ + "org_id", + "detector_key" + ] + } + }, + { + "kind": "table", + "name": "anomaly_incidents", + "primaryKey": { + "name": "anomaly_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "api_keys", + "primaryKey": { + "name": "api_keys_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "aws_connections", + "primaryKey": { + "name": "aws_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cancellation_reviews", + "primaryKey": { + "name": "cancellation_reviews_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "chat_identities", + "primaryKey": { + "name": "chat_identities_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "chat_workspaces", + "primaryKey": { + "name": "chat_workspaces_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cli_device_authorizations", + "primaryKey": { + "name": "cli_device_authorizations_pkey", + "columns": [ + "device_code_hash" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_analytics_state", + "primaryKey": { + "name": "cloudflare_analytics_state_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_hyperdrive_configs", + "primaryKey": { + "name": "cloudflare_hyperdrive_configs_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_logpush_connectors", + "primaryKey": { + "name": "cloudflare_logpush_connectors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "cloudflare_zone_settings", + "primaryKey": { + "name": "cloudflare_zone_settings_pkey", + "columns": [ + "org_id", + "account_id" + ] + } + }, + { + "kind": "table", + "name": "cost_sources", + "primaryKey": { + "name": "cost_sources_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboard_shares", + "primaryKey": { + "name": "dashboard_shares_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboard_versions", + "primaryKey": { + "name": "dashboard_versions_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dashboards", + "primaryKey": { + "name": "dashboards_org_id_id_pk", + "columns": [ + "org_id", + "id" + ] + } + }, + { + "kind": "table", + "name": "dependency_findings", + "primaryKey": { + "name": "dependency_findings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "dependency_scan_matches", + "primaryKey": { + "name": "dependency_scan_matches_pkey", + "columns": [ + "scan_id", + "lockfile_path", + "ecosystem", + "package_name", + "version", + "advisory_id" + ] + } + }, + { + "kind": "table", + "name": "dependency_scans", + "primaryKey": { + "name": "dependency_scans_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "device_credentials", + "primaryKey": { + "name": "device_credentials_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "device_proof_replays", + "primaryKey": { + "name": "device_proof_replays_pkey", + "columns": [ + "key" + ] + } + }, + { + "kind": "table", + "name": "digest_subscriptions", + "primaryKey": { + "name": "digest_subscriptions_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_fingerprint_candidates", + "primaryKey": { + "name": "error_fingerprint_candidates_org_id_fingerprint_hash_pk", + "columns": [ + "org_id", + "fingerprint_hash" + ] + } + }, + { + "kind": "table", + "name": "error_incidents", + "primaryKey": { + "name": "error_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_events", + "primaryKey": { + "name": "error_issue_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_fingerprints", + "primaryKey": { + "name": "error_issue_fingerprints_org_id_fingerprint_hash_pk", + "columns": [ + "org_id", + "fingerprint_hash" + ] + } + }, + { + "kind": "table", + "name": "error_issue_pull_requests", + "primaryKey": { + "name": "error_issue_pull_requests_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_states", + "primaryKey": { + "name": "error_issue_states_org_id_issue_id_pk", + "columns": [ + "org_id", + "issue_id" + ] + } + }, + { + "kind": "table", + "name": "error_issue_verifications", + "primaryKey": { + "name": "error_issue_verifications_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_issues", + "primaryKey": { + "name": "error_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_notification_deliveries", + "primaryKey": { + "name": "error_notification_deliveries_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "error_notification_policies", + "primaryKey": { + "name": "error_notification_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "error_tick_states", + "primaryKey": { + "name": "error_tick_states_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "escalation_chains", + "primaryKey": { + "name": "escalation_chains_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "external_data_source_catalog_entries", + "primaryKey": { + "name": "external_data_source_catalog_entries_pk", + "columns": [ + "source_id", + "name" + ] + } + }, + { + "kind": "table", + "name": "external_data_sources", + "primaryKey": { + "name": "external_data_sources_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "gcp_connectors", + "primaryKey": { + "name": "gcp_connectors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "gcp_resources", + "primaryKey": { + "name": "gcp_resources_pkey", + "columns": [ + "connector_id", + "name" + ] + } + }, + { + "kind": "table", + "name": "investigations", + "primaryKey": { + "name": "investigations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "issue_escalation_policies", + "primaryKey": { + "name": "issue_escalation_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "issue_escalations", + "primaryKey": { + "name": "issue_escalations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "live_activities", + "primaryKey": { + "name": "live_activities_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_authorizations", + "primaryKey": { + "name": "mcp_oauth_authorizations_pkey", + "columns": [ + "request_id_hash" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_clients", + "primaryKey": { + "name": "mcp_oauth_clients_pkey", + "columns": [ + "client_id" + ] + } + }, + { + "kind": "table", + "name": "mcp_oauth_refresh_tokens", + "primaryKey": { + "name": "mcp_oauth_refresh_tokens_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "mobile_devices", + "primaryKey": { + "name": "mobile_devices_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "oauth_auth_states", + "primaryKey": { + "name": "oauth_auth_states_pkey", + "columns": [ + "state" + ] + } + }, + { + "kind": "table", + "name": "oauth_connections", + "primaryKey": { + "name": "oauth_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "oncall_overrides", + "primaryKey": { + "name": "oncall_overrides_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "oncall_schedules", + "primaryKey": { + "name": "oncall_schedules_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "openrouter_cost_connections", + "primaryKey": { + "name": "openrouter_cost_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_billing_links", + "primaryKey": { + "name": "org_billing_links_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_clickhouse_schema_apply_runs", + "primaryKey": { + "name": "org_clickhouse_schema_apply_runs_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_clickhouse_settings", + "primaryKey": { + "name": "org_clickhouse_settings_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_attribute_mappings", + "primaryKey": { + "name": "org_ingest_attribute_mappings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_keys", + "primaryKey": { + "name": "org_ingest_keys_org_id_pk", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_rule_settings", + "primaryKey": { + "name": "org_ingest_rule_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_rule_stats", + "primaryKey": { + "name": "org_ingest_rule_stats_pkey", + "columns": [ + "org_id", + "rule_id", + "bucket", + "signal" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_rules", + "primaryKey": { + "name": "org_ingest_rules_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_ingest_sampling_policies", + "primaryKey": { + "name": "org_ingest_sampling_policies_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_onboarding_state", + "primaryKey": { + "name": "org_onboarding_state_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_openrouter_keys", + "primaryKey": { + "name": "org_openrouter_keys_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "org_recommendation_issues", + "primaryKey": { + "name": "org_recommendation_issues_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "org_support_channels", + "primaryKey": { + "name": "org_support_channels_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_connections", + "primaryKey": { + "name": "planetscale_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_databases", + "primaryKey": { + "name": "planetscale_databases_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_events", + "primaryKey": { + "name": "planetscale_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_issue_receipts", + "primaryKey": { + "name": "planetscale_issue_receipts_org_id_event_id_pk", + "columns": [ + "org_id", + "event_id" + ] + } + }, + { + "kind": "table", + "name": "planetscale_poll_state", + "primaryKey": { + "name": "planetscale_poll_state_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_edits", + "primaryKey": { + "name": "pr_review_edits_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_finding_embeddings", + "primaryKey": { + "name": "pr_review_finding_embeddings_pkey", + "columns": [ + "finding_id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_findings", + "primaryKey": { + "name": "pr_review_findings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_merge_steps", + "primaryKey": { + "name": "pr_review_merge_steps_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_replies", + "primaryKey": { + "name": "pr_review_replies_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "pr_review_settings", + "primaryKey": { + "name": "pr_review_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "pr_reviews", + "primaryKey": { + "name": "pr_reviews_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "railway_connections", + "primaryKey": { + "name": "railway_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "railway_environments", + "primaryKey": { + "name": "railway_environments_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "scrape_target_checks", + "primaryKey": { + "name": "scrape_target_checks_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "scrape_targets", + "primaryKey": { + "name": "scrape_targets_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "search_console_sites", + "primaryKey": { + "name": "search_console_sites_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_events", + "primaryKey": { + "name": "security_audit_events_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_findings", + "primaryKey": { + "name": "security_audit_findings_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_org_reservations", + "primaryKey": { + "name": "security_audit_org_reservations_pkey", + "columns": [ + "run_id", + "agent_id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_org_settings", + "primaryKey": { + "name": "security_audit_org_settings_pkey", + "columns": [ + "org_id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_run_findings", + "primaryKey": { + "name": "security_audit_run_findings_pkey", + "columns": [ + "run_id", + "fingerprint" + ] + } + }, + { + "kind": "table", + "name": "security_audit_runs", + "primaryKey": { + "name": "security_audit_runs_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_targets", + "primaryKey": { + "name": "security_audit_targets_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "security_audit_units", + "primaryKey": { + "name": "security_audit_units_pkey", + "columns": [ + "run_id", + "coverage_id" + ] + } + }, + { + "kind": "table", + "name": "service_map_world_entities", + "primaryKey": { + "name": "service_map_world_entities_pkey", + "columns": [ + "org_id", + "entity_id" + ] + } + }, + { + "kind": "table", + "name": "slos", + "primaryKey": { + "name": "slos_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "sources", + "primaryKey": { + "name": "sources_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "status_page_cleanups", + "primaryKey": { + "name": "status_page_cleanups_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "status_page_components", + "primaryKey": { + "name": "status_page_components_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "status_page_custom_domains", + "primaryKey": { + "name": "status_page_custom_domains_pkey", + "columns": [ + "page_id" + ] + } + }, + { + "kind": "table", + "name": "status_page_incident_components", + "primaryKey": { + "name": "status_page_incident_components_pk", + "columns": [ + "incident_id", + "component_id" + ] + } + }, + { + "kind": "table", + "name": "status_page_incident_updates", + "primaryKey": { + "name": "status_page_incident_updates_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "status_page_incidents", + "primaryKey": { + "name": "status_page_incidents_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "status_pages", + "primaryKey": { + "name": "status_pages_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "supabase_connections", + "primaryKey": { + "name": "supabase_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "supabase_project_insights", + "primaryKey": { + "name": "supabase_project_insights_pkey", + "columns": [ + "project_id" + ] + } + }, + { + "kind": "table", + "name": "supabase_projects", + "primaryKey": { + "name": "supabase_projects_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "uptime_monitor_daily", + "primaryKey": { + "name": "uptime_monitor_daily_pk", + "columns": [ + "monitor_id", + "day" + ] + } + }, + { + "kind": "table", + "name": "uptime_monitor_states", + "primaryKey": { + "name": "uptime_monitor_states_pkey", + "columns": [ + "monitor_id" + ] + } + }, + { + "kind": "table", + "name": "uptime_monitors", + "primaryKey": { + "name": "uptime_monitors_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_commits", + "primaryKey": { + "name": "vcs_commits_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_installations", + "primaryKey": { + "name": "vcs_installations_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_repositories", + "primaryKey": { + "name": "vcs_repositories_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vcs_repository_branches", + "primaryKey": { + "name": "vcs_repository_branches_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vercel_connections", + "primaryKey": { + "name": "vercel_connections_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vercel_deployments", + "primaryKey": { + "name": "vercel_deployments_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "table", + "name": "vercel_projects", + "primaryKey": { + "name": "vercel_projects_pkey", + "columns": [ + "id" + ] + } + }, + { + "kind": "column", + "table": "actors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "user_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "agent_name", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "model", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "capabilities_json", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "created_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "actors", + "name": "last_active_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "impact", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "summary", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "reason", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "details", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "related_to", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_type", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_name", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_model", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "agent_version", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "source", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "agent_feedback", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "max_runs_per_day", + "position": 2, + "type": "integer", + "notNull": true, + "default": "20", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "max_passes_per_day", + "position": 3, + "type": "integer", + "notNull": true, + "default": "90", + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "updated_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "ai_triage_settings", + "name": "updated_by", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "rule_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "destination_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "delivery_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "event_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "attempt_number", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "scheduled_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claimed_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claim_expires_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "claimed_by", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "attempted_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "provider_message", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "provider_reference", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "response_code", + "position": 16, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "error_message", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "silence_id", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "payload_json", + "position": 19, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "created_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_delivery_events", + "name": "updated_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "config_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "secret_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_tested_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_test_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "consecutive_failures", + "position": 11, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "last_failure_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "disabled_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "disabled_reason", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "created_by", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_destinations", + "name": "updated_by", + "position": 18, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "chain_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "chain_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "severity", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "steps_json", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "repeat_count", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "step_index", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "pass_index", + "position": 9, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "next_step_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "status", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "payload_json", + "position": 12, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "steps_sent", + "position": 13, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "last_step_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "ended_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_escalation_runs", + "name": "updated_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "actor_user_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "actor_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "source", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incident_events", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "rule_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "incident_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "rule_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "group_key", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "signal_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "severity", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "peak_severity", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "comparator", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "threshold", + "position": 11, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "threshold_upper", + "position": 12, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "first_triggered_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_triggered_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "resolved_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_observed_value", + "position": 16, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_sample_count", + "position": 17, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_evaluated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "dedupe_key", + "position": 19, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_delivered_event_type", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "last_notified_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "hold_reason", + "position": 22, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "held_since", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "acknowledged_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "acknowledged_by_user_id", + "position": 25, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "acknowledged_by_name", + "position": 26, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "acknowledged_via", + "position": 27, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "error_issue_id", + "position": 28, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "anomaly_incident_id", + "position": 29, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "group_label", + "position": 30, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "labels_json", + "position": 31, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "silence_id", + "position": 32, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "silenced_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "created_at", + "position": 34, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_incidents", + "name": "updated_at", + "position": 35, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "destination_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "thread_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "linear_team_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "linear_issue_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "linear_identifier", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "linear_url", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "incident_id", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_linear_issues", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "reason", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "matchers_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "schedule_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "exclude_from_baselines", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "enabled", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "once_ends_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "revision", + "position": 9, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "created_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_maintenance_windows", + "name": "updated_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "run_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "user_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "channel", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "status", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "attempt_number", + "position": 7, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "scheduled_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "claimed_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "claim_expires_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "payload_json", + "position": 11, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "dedupe_key", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "error_message", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "attempted_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_member_notifications", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "rule_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "last_scheduled_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_claims", + "name": "consecutive_limit_failures", + "position": 3, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "rule_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "group_key", + "position": 2, + "type": "text", + "notNull": true, + "default": "'__total__'", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "consecutive_breaches", + "position": 3, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "consecutive_healthy", + "position": 4, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "history_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_status", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_value", + "position": 7, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_sample_count", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_evaluated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "last_error_kind", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rule_states", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "notes", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "notification_template_json", + "position": 4, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "severity", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "service_names_json", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "exclude_service_names_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "environments_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "anomaly_signals_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "audit_filters_json", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "audit_count_from", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "tags_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "signal_type", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "comparator", + "position": 15, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold", + "position": 16, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold_upper", + "position": 17, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "thresholds_json", + "position": 18, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "window_minutes", + "position": 19, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "evaluation_interval_minutes", + "position": 20, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "slowed_interval_minutes", + "position": 21, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "slowed_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "minimum_sample_count", + "position": 23, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_breaches_required", + "position": 24, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "breaches_out_of", + "position": 25, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_healthy_required", + "position": 26, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "renotify_interval_minutes", + "position": 27, + "type": "integer", + "notNull": true, + "default": "30", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "apdex_threshold_ms", + "position": 28, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_builder_draft_json", + "position": 29, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "raw_query_sql", + "position": 30, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "group_by", + "position": 31, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "destination_ids_json", + "position": 32, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_spec_json", + "position": 33, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "reducer", + "position": 34, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "sample_count_strategy", + "position": 35, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "no_data_behavior", + "position": 36, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "last_scheduled_at", + "position": 37, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_at", + "position": 38, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_at", + "position": 39, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_by", + "position": 40, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_by", + "position": 41, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "reason", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "matchers_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "starts_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "ends_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "maintenance_window_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "exclude_from_baselines", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "created_by", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_silences", + "name": "updated_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "sensitivity", + "position": 2, + "type": "text", + "notNull": true, + "default": "'normal'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "muted_signals_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "last_tick_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_settings", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "detector_key", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "signal_type", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "service_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "deployment_env", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "fingerprint_hash", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "consecutive_breaches", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "consecutive_healthy", + "position": 7, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_status", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_value", + "position": 9, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "baseline_median", + "position": 10, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_sample_count", + "position": 11, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_evaluated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "open_incident_id", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_resolved_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "last_incident_id", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_detector_states", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "detector_key", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "signal_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "deployment_env", + "position": 5, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "fingerprint_hash", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "error_issue_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "opened_value", + "position": 10, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "baseline_median", + "position": 11, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "baseline_sigma", + "position": 12, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "threshold_value", + "position": 13, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_observed_value", + "position": 14, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_sample_count", + "position": 15, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "first_triggered_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_triggered_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "resolved_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "resolve_reason", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "triage_status", + "position": 20, + "type": "text", + "notNull": true, + "default": "'none'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "dedupe_key", + "position": 21, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "fingerprints_json", + "position": 22, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "reopen_count", + "position": 23, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "last_reopened_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "created_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "anomaly_incidents", + "name": "updated_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "key_hash", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "key_prefix", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "revoked", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "revoked_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "last_used_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "metadata_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "scopes", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "kind", + "position": 12, + "type": "text", + "notNull": true, + "default": "'standard'", + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "api_keys", + "name": "created_by_email", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "account_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "modules", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "granted_modules", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "template_version", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "verified_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "last_checked_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "last_check_error", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "check_lease_until", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "metric_stream", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "metric_stream_changed_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "stream_secret_ciphertext", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "stream_secret_iv", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "stream_secret_tag", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "stream_secret_hash", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "stream_last_received_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "stream_last_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "created_by", + "position": 19, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "created_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "aws_connections", + "name": "updated_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "plan_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "subscription_started_at", + "position": 3, + "type": "bigint", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "canceled_at", + "position": 4, + "type": "bigint", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "snapshot_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "rule_reason", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "model_read", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "posted_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cancellation_reviews", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "connector", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "external_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "user_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "display_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_identities", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "connector", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "external_workspace_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "settings", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_ciphertext", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_iv", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "credentials_tag", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "chat_workspaces", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "device_code_hash", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "user_code_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "device_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_org_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_user_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_roles", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_user_email", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "api_key_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_ciphertext", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_iv", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "token_tag", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "approved_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "denied_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cli_device_authorizations", + "name": "expires_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "account_id", + "position": 2, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "dataset", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "zone_id", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "zone_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "enabled", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "watermark_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "backfill_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "settings_json", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "settings_fetched_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "quantiles_available", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "discovered_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "live_scripts_json", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "grant_call_cap", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_success_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "last_error_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "lease_until", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "backoff_until", + "position": 19, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "backoff_streak", + "position": 20, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "backoff_reason", + "position": 21, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "created_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_analytics_state", + "name": "updated_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "account_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "config_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_host", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_port", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_scheme", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_database", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "origin_user", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "deleted_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_hyperdrive_configs", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "zone_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "service_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "dataset", + "position": 6, + "type": "text", + "notNull": true, + "default": "'http_requests'", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_ciphertext", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_iv", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_tag", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_hash", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "enabled", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "last_received_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "last_error", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "secret_rotated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "created_by", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_logpush_connectors", + "name": "updated_by", + "position": 18, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "account_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "watch_new_zones", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "zone_overrides", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cloudflare_zone_settings", + "name": "updated_by", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "connection_ref", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "account_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "watermark_date", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "backfill_date", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "last_synced_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "last_settled_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "last_error", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "last_error_reason", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "lease_until", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "next_attempt_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "consecutive_failures", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "requests_month", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "requests_count", + "position": 16, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_sources", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "dashboard_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "widget_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "mode", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_hash", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "token_suffix", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "created_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "updated_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_shares", + "name": "revoked_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "dashboard_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "version_number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "snapshot_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "change_kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "change_summary", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "source_version_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboard_versions", + "name": "created_by", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "payload_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "created_by", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dashboards", + "name": "version", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "lockfile_path", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "ecosystem", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "package_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "advisory_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "aliases", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "severity", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "summary", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "versions", + "position": 10, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "fixed_in", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "relation", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "status", + "position": 13, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "first_seen_scan_id", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "last_seen_scan_id", + "position": 15, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "first_seen_sha", + "position": 16, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "last_seen_sha", + "position": 17, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "fixed_scan_id", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "fixed_sha", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "status_reason", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "status_changed_by", + "position": 21, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "status_changed_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "created_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_findings", + "name": "updated_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "scan_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "lockfile_path", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "ecosystem", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "package_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "version", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "relation", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "advisory_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "aliases", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "summary", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scan_matches", + "name": "fixed_in", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "commit_sha", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "ref", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "trigger", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "default_branch_head", + "position": 6, + "type": "boolean", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "status", + "position": 7, + "type": "text", + "notNull": true, + "default": "'running'", + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "lockfiles", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "package_count", + "position": 9, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "vulnerable_count", + "position": 10, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "started_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "dependency_scans", + "name": "finished_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "installation_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "public_jwk", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "jkt", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "roles", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "scopes", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "attested", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "attestation", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "last_used_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "expires_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "hard_expires_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_credentials", + "name": "revoked_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_proof_replays", + "name": "key", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "device_proof_replays", + "name": "expires_at", + "position": 1, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "email", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "opted_out_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "day_of_week", + "position": 6, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "timezone", + "position": 7, + "type": "text", + "notNull": true, + "default": "'UTC'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "send_hour", + "position": 8, + "type": "integer", + "notNull": true, + "default": "9", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "namespaces_json", + "position": 9, + "type": "text", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "environments_json", + "position": 10, + "type": "text", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "last_sent_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "last_attempted_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_enabled", + "position": 13, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_opted_out_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_last_sent_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "web_analytics_last_attempted_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "digest_subscriptions", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "fingerprint_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "service_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "exception_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "exception_message", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "error_label", + "position": 5, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "top_frame", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "service_versions_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "first_seen_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "last_seen_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_fingerprint_candidates", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "status", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "reason", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "first_triggered_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "last_triggered_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "resolved_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_incidents", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "actor_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "type", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "from_state", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "to_state", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "payload_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_events", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_fingerprints", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_fingerprints", + "name": "fingerprint_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_fingerprints", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_fingerprints", + "name": "source", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_fingerprints", + "name": "origin_issue_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_fingerprints", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "provider", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "external_repo_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "repo_full_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "number", + "position": 6, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "url", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "title", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "author_login", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "state", + "position": 10, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "merged_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "merge_commit_sha", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "link_source", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "linked_by_actor_id", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_pull_requests", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "issue_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "last_observed_occurrence_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "last_evaluated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "open_incident_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_states", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "pull_request_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "status", + "position": 4, + "type": "text", + "notNull": true, + "default": "'waiting'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "merged_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verify_after", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_versions_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_occurrence_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "baseline_rate_per_hour", + "position": 9, + "type": "double precision", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "investigation_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verdict", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "verdict_note", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "post_merge_occurrence_count", + "position": 13, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "attempt", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issue_verifications", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "kind", + "position": 2, + "type": "text", + "notNull": true, + "default": "'error'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "source_ref_json", + "position": 3, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "fingerprint_hash", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "fingerprint_version", + "position": 5, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "service_name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "exception_type", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "exception_message", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "error_label", + "position": 9, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "top_frame", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "workflow_state", + "position": 11, + "type": "text", + "notNull": true, + "default": "'triage'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "priority", + "position": 12, + "type": "integer", + "notNull": true, + "default": "3", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "severity", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "severity_source", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "assigned_actor_id", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "lease_holder_actor_id", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "lease_expires_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "claimed_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "notes", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "first_seen_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_seen_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "occurrence_count", + "position": 22, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_by_actor_id", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_resolved_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "last_regressed_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "regression_count", + "position": 27, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "seen_versions_json", + "position": 28, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "resolved_versions_json", + "position": 29, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "snooze_until", + "position": 30, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "archived_at", + "position": 31, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "merged_into_issue_id", + "position": 32, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "created_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_issues", + "name": "updated_at", + "position": 34, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "destination_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "delivery_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "payload_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "status", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "attempt_count", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "scheduled_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claimed_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claim_expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "claimed_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "attempted_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "error_message", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_deliveries", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "destination_ids_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_first_seen", + "position": 3, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_regression", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_resolve", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_transition_in_review", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_transition_done", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "notify_on_claim", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "min_occurrence_count", + "position": 9, + "type": "integer", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "severity", + "position": 10, + "type": "text", + "notNull": true, + "default": "'warning'", + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_notification_policies", + "name": "updated_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "processed_through", + "position": 1, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "bootstrap_completed", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "claim_token", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "claim_expires_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "error_tick_states", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "steps_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "repeat_count", + "position": 5, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "updated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "created_by", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "escalation_chains", + "name": "updated_by", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "source_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "row_estimate", + "position": 4, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "source_note", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "detected_time_field", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "fields", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "exposed", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "note", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "time_field_override", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "field_notes", + "position": 11, + "type": "jsonb", + "notNull": true, + "default": "'{}'::jsonb", + "identity": null + }, + { + "kind": "column", + "table": "external_data_source_catalog_entries", + "name": "seen_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "description", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "environment", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "tags", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": "'[]'::jsonb", + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "agent_access", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "agent_pii", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "config", + "position": 9, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "secret_ciphertext", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "secret_iv", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "secret_tag", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "secret_hint", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "status", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "last_checked_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "last_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "catalog_refreshed_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "catalog_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "examples", + "position": 19, + "type": "jsonb", + "notNull": true, + "default": "'[]'::jsonb", + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "created_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "updated_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "created_by", + "position": 22, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "external_data_sources", + "name": "updated_by", + "position": 23, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "scope_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "scope_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "project_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "logs_enabled", + "position": 6, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_enabled", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_ciphertext", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_iv", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_tag", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "secret_hash", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_received_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_error", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "applied_logs_enabled", + "position": 14, + "type": "boolean", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "applied_metrics_enabled", + "position": 15, + "type": "boolean", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "setup_reported_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_watermark_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_metrics_received_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_metrics_error", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "metrics_lease_until", + "position": 20, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "resources_synced_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "last_resources_error", + "position": 22, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "created_by", + "position": 23, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "created_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_connectors", + "name": "updated_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "connector_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "asset_type", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "project_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "location", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "display_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "state", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "labels", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "details", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "resource_created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "resource_updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "gcp_resources", + "name": "last_seen_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": "'investigating'", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "seeded_by", + "position": 3, + "type": "text", + "notNull": true, + "default": "'user'", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "subject_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "snapshot_json", + "position": 5, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "incident_kind", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "incident_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "issue_id", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "report_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "progress_json", + "position": 10, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "severity", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "confidence", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "model", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "input_tokens", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "output_tokens", + "position": 15, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "started_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "autonomous_turns", + "position": 18, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "created_by", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "created_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "diagnosed_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "investigations", + "name": "updated_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "enabled", + "position": 1, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "rules_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "updated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalation_policies", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "issue_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "severity", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "source", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "reason", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "run_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "investigation_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "payload_json", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "delivery_results_json", + "position": 9, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "attempts", + "position": 11, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "claimed_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "dedupe_key", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "error", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "issue_escalations", + "name": "processed_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "device_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "incident_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "activity_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "push_token", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "ended_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "ended_reason", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "live_activities", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "request_id_hash", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "client_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "client_name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "redirect_uri", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "state", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "resource", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "scopes", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "code_challenge", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "authorization_code_hash", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_org_id", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_user_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_roles", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_user_email", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "approved_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "denied_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "used_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_authorizations", + "name": "expires_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_name", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "redirect_uris", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "client_uri", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_clients", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "token_hash", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "family_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "client_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "resource", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "scopes", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "org_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "user_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "roles", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "user_email", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "access_key_id", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "replaced_by_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "revoked_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "expires_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mcp_oauth_refresh_tokens", + "name": "family_expires_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "user_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "platform", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "token", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "environment", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "bundle_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "app_version", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "live_activity_start_token", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "device_name", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "preferences", + "position": 10, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "disabled_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "disabled_reason", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "last_pushed_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "last_seen_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "mobile_devices", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "state", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "initiated_by_user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "redirect_uri", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "return_to", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "code_verifier", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "github_installation_ids", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_auth_states", + "name": "expires_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "provider", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_user_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_user_email", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "external_account_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "granted_accounts_json", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "connected_by_user_id", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "scope", + "position": 9, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_ciphertext", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_iv", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "access_token_tag", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_ciphertext", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_iv", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "refresh_token_tag", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "expires_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "revoked_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "created_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oauth_connections", + "name": "updated_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "schedule_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "user_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "starts_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "ends_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "created_by", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_overrides", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "time_zone", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "layers_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "updated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "created_by", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "oncall_schedules", + "name": "updated_by", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "key_ciphertext", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "key_iv", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "key_tag", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "key_label", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "account_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "updated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "openrouter_cost_connections", + "name": "updated_by", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_billing_links", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_billing_links", + "name": "billing_org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_billing_links", + "name": "linked_by", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_billing_links", + "name": "created_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "workflow_instance_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "phase", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "current_migration", + "position": 4, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "steps_total", + "position": 5, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "steps_done", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "applied_versions", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "skipped", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "error_message", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "started_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "finished_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "created_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_schema_apply_runs", + "name": "updated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_url", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_user", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_ciphertext", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_iv", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_password_tag", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "ch_database", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "sync_status", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "last_sync_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "last_sync_error", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "schema_version", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "created_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "updated_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "created_by", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_clickhouse_settings", + "name": "updated_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "source_context", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "source_key", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "target_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "operation", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "enabled", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_attribute_mappings", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_key", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_key_hash", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_ciphertext", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_iv", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_tag", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_key_hash", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "public_rotated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "private_rotated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "created_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_keys", + "name": "updated_by", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_settings", + "name": "hash_salt", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_settings", + "name": "created_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "rule_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "bucket", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "signal", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "matched", + "position": 4, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "dropped", + "position": 5, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "dropped_bytes", + "position": 6, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rule_stats", + "name": "redacted_values", + "position": 7, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "enabled", + "position": 3, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "position", + "position": 4, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "spec_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_rules", + "name": "updated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "trace_sample_ratio", + "position": 1, + "type": "double precision", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "always_keep_error_spans", + "position": 2, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "always_keep_slow_spans_ms", + "position": 3, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "created_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_ingest_sampling_policies", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "user_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "email", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "role", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "demo_data_requested", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "onboarding_completed_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "checklist_dismissed_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "first_data_received_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "welcome_email_sent_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "connect_nudge_email_sent_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "stalled_email_sent_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "activation_email_sent_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "reward_claimed_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "reward_reserved_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_onboarding_state", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_ciphertext", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_iv", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_tag", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "key_hint", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_openrouter_keys", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "number", + "position": 2, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "recommendation_key", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "source_key", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "canonical_key", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "status", + "position": 7, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "usage_count", + "position": 8, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "opened_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": "now()", + "identity": null + }, + { + "kind": "column", + "table": "org_recommendation_issues", + "name": "resolved_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "slack_channel_id", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "slack_channel_name", + "position": 2, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "reservation_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "reserved_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "created_by_user_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "created_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "org_support_channels", + "name": "updated_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "ps_organization", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "connected_by_user_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "scrape_target_id", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_ciphertext", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_iv", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "webhook_secret_tag", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "detected_permissions_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "last_inventory_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "last_inventory_error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "created_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_connections", + "name": "updated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "database_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": "'mysql'", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "state", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "region", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "plan", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "branches_json", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "webhook_id", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "webhook_secret_ciphertext", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "webhook_secret_iv", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "webhook_secret_tag", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "webhook_error", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "deleted_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_databases", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "database_id", + "position": 2, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "database_name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "branch_name", + "position": 4, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "category", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "event_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "state", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "external_id", + "position": 8, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "title", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "source", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "actor_login", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "url", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "payload_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "occurred_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_events", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "event_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_issue_receipts", + "name": "processed_at", + "position": 2, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "dataset", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "database_id", + "position": 3, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "enabled", + "position": 4, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "watermark_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_success_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_error", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "last_error_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "lease_until", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "planetscale_poll_state", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "reply_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "seq", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "path", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "old_text", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "new_text", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_edits", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "finding_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "model", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "embedding", + "position": 4, + "type": "real[]", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_finding_embeddings", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "review_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "handle", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "path", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "line", + "position": 7, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "category", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "severity", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "title", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "status", + "position": 11, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "comment_id", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "resolved_sha", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "reactions_up", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "reactions_down", + "position": 15, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "created_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_findings", + "name": "updated_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "key", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "title", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "source", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "path", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "done_by", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "done_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "reminded_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "first_review_id", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "last_review_id", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_merge_steps", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "comment_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "surface", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "thread_root_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "author_login", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "command", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "head_sha", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "session_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "reply_url", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "commit_sha", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "error", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_replies", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "model", + "position": 1, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "defaults", + "position": 2, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "updated_at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_review_settings", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "repository_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "number", + "position": 3, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "head_sha", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "base_sha", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "url", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "title", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "author_login", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'queued'", + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "skip_reason", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "session_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "comment_attempt", + "position": 12, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "report_json", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "score", + "position": 14, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "check_run_url", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "comment_url", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "review_url", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "publish_error", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "error", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "model", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "input_tokens", + "position": 21, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "output_tokens", + "position": 22, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "cost_microusd", + "position": 23, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "started_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "finished_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "merged_at", + "position": 26, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "telemetry_json", + "position": 27, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "merge_commit_sha", + "position": 28, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_status", + "position": 29, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_after", + "position": 30, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "post_merge_json", + "position": 31, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "created_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "pr_reviews", + "name": "updated_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_ciphertext", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_iv", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "token_tag", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "workspace_names", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "connected_by_user_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "auth_failed_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "discovered_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "lease_until", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_success_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_error", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "last_error_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_connections", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "project_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "project_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "environment_id", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "environment_name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "services_json", + "position": 7, + "type": "text", + "notNull": true, + "default": "'{}'", + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "enabled", + "position": 8, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "watermark_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_success_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_error", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "last_error_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "railway_environments", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "id", + "position": 0, + "type": "integer", + "notNull": true, + "default": null, + "identity": "by default" + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "target_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "sub_target_key", + "position": 3, + "type": "text", + "notNull": true, + "default": "''", + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "checked_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "error", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "duration_ms", + "position": 6, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "samples_scraped", + "position": 7, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_target_checks", + "name": "samples_post_relabel", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "url", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "target_type", + "position": 6, + "type": "text", + "notNull": true, + "default": "'prometheus'", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "discovery_config_json", + "position": 7, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "scrape_interval_seconds", + "position": 8, + "type": "integer", + "notNull": true, + "default": "15", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "labels_json", + "position": 9, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_type", + "position": 10, + "type": "text", + "notNull": true, + "default": "'none'", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "managed_by", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_ciphertext", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_iv", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "auth_credentials_tag", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "enabled", + "position": 15, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "last_scrape_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "last_scrape_error", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "created_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "scrape_targets", + "name": "updated_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "oauth_connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "site_url", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "permission_level", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "watermark_date", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "backfill_date", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "last_synced_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "last_error", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "lease_until", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "next_attempt_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "consecutive_failures", + "position": 12, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "search_console_sites", + "name": "updated_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "run_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "at", + "position": 3, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "kind", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "role", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "agent_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "outcome", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "detail", + "position": 8, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "input_tokens", + "position": 9, + "type": "bigint", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_events", + "name": "output_tokens", + "position": 10, + "type": "bigint", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "target_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "fingerprint", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "verdict", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "overall_severity", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "estimated_severity", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "record", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "status", + "position": 8, + "type": "text", + "notNull": true, + "default": "'open'", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "first_seen_run_id", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "last_seen_run_id", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "first_seen_ref", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "last_seen_ref", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "fixed_run_id", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "status_reason", + "position": 14, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "status_changed_by", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "status_changed_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "created_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_findings", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_reservations", + "name": "run_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_reservations", + "name": "agent_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_reservations", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_reservations", + "name": "cost_usd", + "position": 3, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_reservations", + "name": "sandbox_fee_usd", + "position": 4, + "type": "double precision", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_reservations", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_settings", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_settings", + "name": "monthly_spend_limit_usd", + "position": 1, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_settings", + "name": "discovery_model", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_settings", + "name": "validation_model", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_settings", + "name": "updated_by", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_org_settings", + "name": "updated_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "run_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "fingerprint", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "verdict", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "overall_severity", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "estimated_severity", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_run_findings", + "name": "record", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "target_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "source_ref", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "profile", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "scope_paths", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "budget", + "position": 7, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "metadata", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "status", + "position": 9, + "type": "text", + "notNull": true, + "default": "'in_progress'", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "incomplete_reason", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "architecture_md", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "reports", + "position": 12, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "execution_profile", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "invocations_spent", + "position": 14, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "input_tokens", + "position": 15, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "output_tokens", + "position": 16, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "cost_usd", + "position": 17, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "sandbox_usd", + "position": 18, + "type": "double precision", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "discovery_model", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "validation_model", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "workflow_instance_id", + "position": 21, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "started_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "finished_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_runs", + "name": "error", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "repository", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "vcs_repository_id", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "branch", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "enabled", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "profile", + "position": 6, + "type": "text", + "notNull": true, + "default": "'deep'", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "scope_paths", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "slice_interval_hours", + "position": 8, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "revalidate_on_push", + "position": 9, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "budget_per_run", + "position": 10, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "monthly_invocation_budget", + "position": 11, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "discovery_model", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "validation_model", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "created_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_targets", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "run_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "coverage_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "status", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "wave", + "position": 4, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "agent_id", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "security_audit_units", + "name": "unit", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "org_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "entity_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "kind", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "data", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "revision", + "position": 4, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "created_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "updated_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "updated_by", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "service_map_world_entities", + "name": "deleted_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "service_name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "environments_json", + "position": 5, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "sli_json", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "target_percent", + "position": 7, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "window_days", + "position": 8, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "alert_rule_id", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "created_by", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "updated_by", + "position": 11, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "created_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "slos", + "name": "updated_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "description", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "platform", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "kind", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "key_hash", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "key_prefix", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "allowed_signals", + "position": 8, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "rate_limit_rps", + "position": 9, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "resource_attributes", + "position": 10, + "type": "jsonb", + "notNull": true, + "default": "'[]'::jsonb", + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "expires_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "paused_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "paused_until", + "position": 13, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "last_used_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "last_rejected_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "last_rejection", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "last_poll_at", + "position": 17, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "last_poll_ok_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "last_poll_error", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "disconnected_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "poll_resources", + "position": 21, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "rolled_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "created_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "updated_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "created_by", + "position": 25, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "sources", + "name": "updated_by", + "position": 26, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "page_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "slug", + "position": 3, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "delete_snapshot", + "position": 4, + "type": "boolean", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "hostname", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "cloudflare_hostname_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "cloudflare_route_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_cleanups", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "page_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "name", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "description", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "group_name", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "position", + "position": 6, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "monitor_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "manual_status", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "show_uptime", + "position": 9, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_components", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "page_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "hostname", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "status", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "cloudflare_hostname_id", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "cloudflare_route_id", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "dns_records", + "position": 6, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "last_error", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "last_checked_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "activated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_custom_domains", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_components", + "name": "incident_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_components", + "name": "component_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_components", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "incident_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "status", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "body", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "source", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "created_by", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "overlay_entry_id", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incident_updates", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "page_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "title", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "impact", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "status", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "published", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "alert_incident_id", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "scheduled_start", + "position": 9, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "scheduled_end", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "started_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "resolved_at", + "position": 12, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "created_by", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "created_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_page_incidents", + "name": "updated_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "slug", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "description", + "position": 4, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "website_url", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "logo_url", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "logo_dark_url", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "favicon_url", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "support_url", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "privacy_url", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "terms_url", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "theme", + "position": 12, + "type": "jsonb", + "notNull": true, + "default": "'{\"preset\":\"default\",\"appearance\":\"auto\",\"accent\":null,\"background\":null,\"text\":null,\"card\":null,\"border\":null,\"radius\":\"md\",\"font\":\"sans\"}'", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "show_powered_by", + "position": 13, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "auto_incidents", + "position": 14, + "type": "text", + "notNull": true, + "default": "'off'", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "break_glass_token_sha256", + "position": 15, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "break_glass_token_suffix", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "visibility", + "position": 17, + "type": "text", + "notNull": true, + "default": "'public'", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "password_hash", + "position": 18, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "draft", + "position": 19, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "revision", + "position": 20, + "type": "bigint", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "published_revision", + "position": 21, + "type": "bigint", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "published_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "last_publish_error", + "position": 23, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "published_hash", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "publish_attempted_at", + "position": 25, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "created_by", + "position": 26, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "created_at", + "position": 27, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "status_pages", + "name": "updated_at", + "position": 28, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "supabase_organization_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "supabase_organization_slug", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "supabase_organization_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "connected_by_user_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "discovery_lease_until", + "position": 7, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "last_discovery_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "last_discovery_error", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "created_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_connections", + "name": "updated_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "project_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "advisors_json", + "position": 2, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "top_queries_json", + "position": 3, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "advisors_checked_at", + "position": 4, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "queries_checked_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "advisors_error", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "queries_error", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "lease_until", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "created_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_project_insights", + "name": "updated_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "project_ref", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "region", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "status", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "metrics_enabled", + "position": 7, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "scrape_target_id", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "log_drain_enabled", + "position": 9, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "log_drain_id", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "log_drain_source_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "log_drain_error", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "log_drains_json", + "position": 13, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "log_drains_checked_at", + "position": 14, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "logs_enabled", + "position": 15, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "logs_watermark_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "logs_boundary_ids", + "position": 17, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "logs_lease_until", + "position": 18, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "logs_last_polled_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "logs_error", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "last_seen_at", + "position": 21, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "removed_at", + "position": 22, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "created_at", + "position": 23, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "supabase_projects", + "name": "updated_at", + "position": 24, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "monitor_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "day", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "org_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "checks", + "position": 3, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "failures", + "position": 4, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "down_checks", + "position": 5, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_daily", + "name": "last_check_at", + "position": 6, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "monitor_id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "status", + "position": 2, + "type": "text", + "notNull": true, + "default": "'pending'", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "consecutive_failures", + "position": 3, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "consecutive_successes", + "position": 4, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "status_changed_at", + "position": 5, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "last_check_json", + "position": 6, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "recent_json", + "position": 7, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "last_ping_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitor_states", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "name", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "kind", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "config_json", + "position": 4, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "headers_ciphertext", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "headers_iv", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "headers_tag", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "interval_seconds", + "position": 8, + "type": "integer", + "notNull": true, + "default": "60", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "failures_to_down", + "position": 9, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "service_name", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "alert_rule_id", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "heartbeat_token", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "enabled", + "position": 13, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "created_by", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "uptime_monitors", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "repository_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "sha", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "message", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_name", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_email", + "position": 7, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_login", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "author_avatar_url", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "authored_at", + "position": 10, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "committed_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "html_url", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_commits", + "name": "created_at", + "position": 13, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "provider", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "external_installation_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_login", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_type", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "external_account_id", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "account_avatar_url", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "repository_selection", + "position": 9, + "type": "text", + "notNull": true, + "default": "'all'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "status", + "position": 10, + "type": "text", + "notNull": true, + "default": "'active'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "suspended_at", + "position": 11, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "ci_traces_enabled", + "position": 12, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "repository_discovery", + "position": 13, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "installed_by_user_id", + "position": 14, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "created_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_installations", + "name": "updated_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "installation_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "external_repo_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "owner", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "name", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "full_name", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "default_branch", + "position": 8, + "type": "text", + "notNull": true, + "default": "'main'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "tracked_branch", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "html_url", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "is_private", + "position": 11, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "is_archived", + "position": 12, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "status", + "position": 13, + "type": "text", + "notNull": true, + "default": "'active'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "sync_status", + "position": 14, + "type": "text", + "notNull": true, + "default": "'pending'", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "last_synced_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "last_sync_error", + "position": 16, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "pr_review_enabled", + "position": 17, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "pr_review_config", + "position": 18, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "created_at", + "position": 19, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repositories", + "name": "updated_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "repository_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "is_default", + "position": 5, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "head_sha", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vcs_repository_branches", + "name": "updated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "source_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "team_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "team_slug", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "team_name", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "token_ciphertext", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "token_iv", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "token_tag", + "position": 8, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "connected_by_user_id", + "position": 9, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "project_scope", + "position": 10, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "project_ids", + "position": 11, + "type": "jsonb", + "notNull": true, + "default": "'[]'", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "sampling_rate", + "position": 12, + "type": "double precision", + "notNull": true, + "default": "1", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "logs_enabled", + "position": 13, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "traces_enabled", + "position": 14, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "ai_gateway_enabled", + "position": 15, + "type": "boolean", + "notNull": true, + "default": "true", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "speed_insights_enabled", + "position": 16, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "web_analytics_enabled", + "position": 17, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "audit_log_enabled", + "position": 18, + "type": "boolean", + "notNull": true, + "default": "false", + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "log_drain_id", + "position": 19, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "trace_drain_id", + "position": 20, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "ai_gateway_drain_id", + "position": 21, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "speed_insights_drain_id", + "position": 22, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "web_analytics_drain_id", + "position": 23, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "audit_log_drain_id", + "position": 24, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "log_drain_state", + "position": 25, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "trace_drain_state", + "position": 26, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "ai_gateway_drain_state", + "position": 27, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "speed_insights_drain_state", + "position": 28, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "web_analytics_drain_state", + "position": 29, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "audit_log_drain_state", + "position": 30, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "drain_key_hash", + "position": 31, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "auth_failed_at", + "position": 32, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "discovered_at", + "position": 33, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "deployments_watermark_at", + "position": 34, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "lease_until", + "position": 35, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "last_success_at", + "position": 36, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "last_error", + "position": 37, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "last_error_at", + "position": 38, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "created_at", + "position": 39, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_connections", + "name": "updated_at", + "position": 40, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "project_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "deployment_id", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "url", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "target", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "state", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "branch", + "position": 8, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "commit_sha", + "position": 9, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "commit_message", + "position": 10, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "creator", + "position": 11, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "error_code", + "position": 12, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "error_message", + "position": 13, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "created_at_vercel", + "position": 14, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "building_at", + "position": 15, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "ready_at", + "position": 16, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "inspector_url", + "position": 17, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_deployments", + "name": "updated_at", + "position": 18, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "connection_id", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "project_id", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "name", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "framework", + "position": 5, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "production_deployment_id", + "position": 6, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "created_at", + "position": 7, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "vercel_projects", + "name": "updated_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_agent_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"agent_name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_type_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"type\"" + ], + "where": null + }, + { + "kind": "index", + "table": "actors", + "name": "actors_org_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "agent_feedback", + "name": "agent_feedback_kind_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"kind\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "agent_feedback", + "name": "agent_feedback_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_claim_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"claim_expires_at\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_delivery_attempt_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"delivery_key\"", + "\"attempt_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_delivery_events", + "name": "alert_delivery_events_org_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_destinations", + "name": "alert_destinations_org_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_escalation_runs", + "name": "alert_escalation_runs_live_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"next_step_at\"" + ], + "where": "\"status\" in ('active', 'acknowledged')" + }, + { + "kind": "index", + "table": "alert_escalation_runs", + "name": "alert_escalation_runs_live_incident_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"incident_id\"" + ], + "where": "\"status\" IN ('active', 'acknowledged')" + }, + { + "kind": "index", + "table": "alert_escalation_runs", + "name": "alert_escalation_runs_org_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incident_events", + "name": "alert_incident_events_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_incident_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"incident_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_open_group_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"rule_id\"", + "\"group_key\"" + ], + "where": "\"status\" = 'open'" + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"error_issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_rule_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"rule_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_incidents", + "name": "alert_incidents_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_linear_issues", + "name": "alert_linear_issues_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_linear_issues", + "name": "alert_linear_issues_thread_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"destination_id\"", + "\"thread_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_maintenance_windows", + "name": "alert_maintenance_windows_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"enabled\"", + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_maintenance_windows", + "name": "alert_maintenance_windows_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_member_notifications", + "name": "alert_member_notifications_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"dedupe_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_member_notifications", + "name": "alert_member_notifications_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_member_notifications", + "name": "alert_member_notifications_org_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rule_claims", + "name": "alert_rule_claims_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rule_states", + "name": "alert_rule_states_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_rules", + "name": "alert_rules_org_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "alert_silences", + "name": "alert_silences_maintenance_occurrence_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"maintenance_window_id\"", + "\"starts_at\"" + ], + "where": "\"maintenance_window_id\" IS NOT NULL" + }, + { + "kind": "index", + "table": "alert_silences", + "name": "alert_silences_org_ends_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"ends_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_detector_states", + "name": "anomaly_detector_states_evaluated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"last_evaluated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_detector_states", + "name": "anomaly_detector_states_open_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"open_incident_id\"" + ], + "where": "\"open_incident_id\" is not null" + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_open_detector_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"detector_key\"" + ], + "where": "\"status\" = 'open'" + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_detector_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"detector_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"error_issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_status_triggered_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"", + "\"last_triggered_at\"", + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "anomaly_incidents", + "name": "anomaly_incidents_org_triggered_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_triggered_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "api_keys", + "name": "api_keys_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "api_keys", + "name": "api_keys_org_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "aws_connections", + "name": "aws_connections_org_account_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"account_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "aws_connections", + "name": "aws_connections_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "aws_connections", + "name": "aws_connections_stream_secret_hash_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"stream_secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cancellation_reviews", + "name": "cancellation_reviews_subscription_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"plan_id\"", + "\"subscription_started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_connector_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"connector\"", + "\"external_user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_connector_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"connector\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_identities", + "name": "chat_identities_org_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_workspaces", + "name": "chat_workspaces_connector_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"connector\"", + "\"external_workspace_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "chat_workspaces", + "name": "chat_workspaces_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cli_device_authorizations", + "name": "cli_device_authorizations_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cli_device_authorizations", + "name": "cli_device_authorizations_user_code_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"user_code_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_analytics_state", + "name": "cf_analytics_state_org_account_dataset_zone_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"account_id\"", + "\"dataset\"", + "\"zone_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_analytics_state", + "name": "cf_analytics_state_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_hyperdrive_configs", + "name": "cloudflare_hyperdrive_configs_org_config_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"config_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_hyperdrive_configs", + "name": "cloudflare_hyperdrive_configs_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_secret_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "cost_sources", + "name": "cost_sources_org_provider_ref_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"connection_ref\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_live_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "coalesce(widget_id, '')" + ], + "where": "revoked_at is null" + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_org_dashboard_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_shares", + "name": "dashboard_shares_token_hash_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"token_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_versions", + "name": "dashboard_versions_org_dashboard_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "\"version_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboard_versions", + "name": "dashboard_versions_org_dashboard_version_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dashboard_id\"", + "\"version_number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboards", + "name": "dashboards_org_name_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dashboards", + "name": "dashboards_org_updated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"updated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_findings", + "name": "dependency_findings_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"lockfile_path\"", + "\"ecosystem\"", + "\"package_name\"", + "\"advisory_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_findings", + "name": "dependency_findings_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_findings", + "name": "dependency_findings_org_updated_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"updated_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_scan_matches", + "name": "dependency_scan_matches_org_scan_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"scan_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_scans", + "name": "dependency_scans_org_started_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_scans", + "name": "dependency_scans_repo_commit_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"commit_sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "dependency_scans", + "name": "dependency_scans_repo_started_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "device_credentials", + "name": "device_credentials_jkt_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"jkt\"" + ], + "where": null + }, + { + "kind": "index", + "table": "device_credentials", + "name": "device_credentials_live_installation_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"installation_id\"" + ], + "where": "\"revoked_at\" IS NULL" + }, + { + "kind": "index", + "table": "device_credentials", + "name": "device_credentials_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "device_proof_replays", + "name": "device_proof_replays_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "digest_subscriptions", + "name": "digest_subscriptions_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "digest_subscriptions", + "name": "digest_subscriptions_org_user_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_fingerprint_candidates", + "name": "error_fingerprint_candidates_last_seen_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_seen_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_incidents", + "name": "error_incidents_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_incidents", + "name": "error_incidents_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"", + "\"last_triggered_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_actor_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"actor_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_events", + "name": "error_issue_events_type_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"type\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_fingerprints", + "name": "error_issue_fingerprints_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_issue_pr_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"", + "\"provider\"", + "\"repo_full_name\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_pull_requests", + "name": "error_issue_pull_requests_repo_number_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"repo_full_name\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"verify_after\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issue_verifications", + "name": "error_issue_verifications_open_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": "\"status\" in ('waiting', 'running')" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_lease_expiry_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"lease_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_archived_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"archived_at\"" + ], + "where": "\"archived_at\" is not null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_assignee_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"assigned_actor_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_fp_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"fingerprint_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_fp_version_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"fingerprint_version\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_kind_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"kind\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_live_seen_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"last_seen_at\"", + "\"id\"" + ], + "where": "\"archived_at\" is null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_merged_into_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"merged_into_issue_id\"" + ], + "where": "\"merged_into_issue_id\" is not null" + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_severity_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"severity\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_issues", + "name": "error_issues_org_workflow_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"workflow_state\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"scheduled_at\"", + "\"claim_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_key_destination_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"delivery_key\"", + "\"destination_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_notification_deliveries", + "name": "error_notification_deliveries_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "error_tick_states", + "name": "error_tick_states_claim_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"claim_expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "escalation_chains", + "name": "escalation_chains_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "external_data_source_catalog_entries", + "name": "external_data_source_catalog_entries_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "external_data_sources", + "name": "external_data_sources_org_name_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_org_scope_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"scope_type\"", + "\"scope_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_secret_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"secret_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "gcp_connectors", + "name": "gcp_connectors_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_incident_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_kind\"", + "\"incident_id\"" + ], + "where": "\"incident_id\" is not null" + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "investigations", + "name": "investigations_org_status_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"status\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"dedupe_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"status\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "issue_escalations", + "name": "issue_escalations_org_issue_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"issue_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "live_activities", + "name": "live_activities_device_incident_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"device_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "live_activities", + "name": "live_activities_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_authorizations", + "name": "mcp_oauth_authorizations_code_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"authorization_code_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_authorizations", + "name": "mcp_oauth_authorizations_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_family_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"family_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mcp_oauth_refresh_tokens", + "name": "mcp_oauth_refresh_tokens_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"token_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_org_platform_token_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"platform\"", + "\"token\"" + ], + "where": null + }, + { + "kind": "index", + "table": "mobile_devices", + "name": "mobile_devices_user_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"user_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_auth_states", + "name": "oauth_auth_states_expires_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"expires_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_org_provider_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oauth_connections", + "name": "oauth_connections_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oncall_overrides", + "name": "oncall_overrides_org_schedule_ends_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"schedule_id\"", + "\"ends_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "oncall_schedules", + "name": "oncall_schedules_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "openrouter_cost_connections", + "name": "openrouter_cost_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_billing_links", + "name": "org_billing_links_billing_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"billing_org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_attribute_mappings", + "name": "org_ingest_attribute_mappings_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_private_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"private_key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_public_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"public_key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_keys", + "name": "org_ingest_keys_public_key_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"public_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_rule_stats", + "name": "org_ingest_rule_stats_org_bucket_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"bucket\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_ingest_rules", + "name": "org_ingest_rules_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_recommendation_issues", + "name": "org_recommendation_issues_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "org_recommendation_issues", + "name": "org_recommendation_issues_org_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"recommendation_key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_connections", + "name": "planetscale_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_connections", + "name": "planetscale_connections_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_databases", + "name": "planetscale_databases_org_db_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_databases", + "name": "planetscale_databases_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_dedupe_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_name\"", + "\"event_type\"", + "\"external_id\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_org_db_time_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"database_name\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_events", + "name": "planetscale_events_org_time_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"occurred_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_issue_receipts", + "name": "planetscale_issue_receipts_processed_at_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"processed_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_poll_state", + "name": "planetscale_poll_state_org_dataset_db_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"dataset\"", + "\"database_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "planetscale_poll_state", + "name": "planetscale_poll_state_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_edits", + "name": "pr_review_edits_reply_seq_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"reply_id\"", + "\"seq\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_finding_embeddings", + "name": "pr_review_finding_embeddings_org_model_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"model\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_findings", + "name": "pr_review_findings_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_findings", + "name": "pr_review_findings_pr_handle_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"handle\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_merge_steps", + "name": "pr_review_merge_steps_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_merge_steps", + "name": "pr_review_merge_steps_pr_key_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"key\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_replies", + "name": "pr_review_replies_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_review_replies", + "name": "pr_review_replies_repo_comment_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"comment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_post_merge_due_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"post_merge_after\"" + ], + "where": "\"post_merge_status\" = 'waiting'" + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_repo_number_head_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"", + "\"head_sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "pr_reviews", + "name": "pr_reviews_repo_number_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"number\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_connections", + "name": "railway_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_connections", + "name": "railway_connections_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_environments", + "name": "railway_environments_connection_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"connection_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "railway_environments", + "name": "railway_environments_org_env_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"environment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_target_checks", + "name": "scrape_target_checks_target_checked_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"target_id\"", + "\"checked_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_org_enabled_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"enabled\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "scrape_targets", + "name": "scrape_targets_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "search_console_sites", + "name": "search_console_sites_connection_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"oauth_connection_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "search_console_sites", + "name": "search_console_sites_org_site_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"site_url\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_events", + "name": "security_audit_events_run_at_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"run_id\"", + "\"at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_findings", + "name": "security_audit_findings_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_findings", + "name": "security_audit_findings_target_fingerprint_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"target_id\"", + "\"fingerprint\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_org_reservations", + "name": "security_audit_org_reservations_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_runs", + "name": "security_audit_runs_one_running_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"target_id\"" + ], + "where": "\"finished_at\" IS NULL" + }, + { + "kind": "index", + "table": "security_audit_runs", + "name": "security_audit_runs_org_started_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_runs", + "name": "security_audit_runs_target_started_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"target_id\"", + "\"started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_targets", + "name": "security_audit_targets_org_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "security_audit_targets", + "name": "security_audit_targets_org_repo_branch_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"repository\"", + "\"branch\"" + ], + "where": null + }, + { + "kind": "index", + "table": "slos", + "name": "slos_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "slos", + "name": "slos_org_name_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "sources", + "name": "sources_key_hash_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"key_hash\"" + ], + "where": null + }, + { + "kind": "index", + "table": "sources", + "name": "sources_org_default_kind_unique", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": "\"kind\" = 'default'" + }, + { + "kind": "index", + "table": "sources", + "name": "sources_org_id_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_cleanups", + "name": "status_page_cleanups_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_components", + "name": "status_page_components_monitor_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"monitor_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_components", + "name": "status_page_components_page_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"page_id\"", + "\"position\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_custom_domains", + "name": "status_page_custom_domains_checked_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"last_checked_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_custom_domains", + "name": "status_page_custom_domains_hostname_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"hostname\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_custom_domains", + "name": "status_page_custom_domains_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_incident_components", + "name": "status_page_incident_components_component_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"component_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_incident_updates", + "name": "status_page_incident_updates_incident_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"incident_id\"", + "\"created_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_incident_updates", + "name": "status_page_incident_updates_overlay_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"overlay_entry_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_incidents", + "name": "status_page_incidents_alert_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"alert_incident_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_page_incidents", + "name": "status_page_incidents_open_maintenance_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"scheduled_start\"" + ], + "where": "\"kind\" = 'maintenance' and \"resolved_at\" is null" + }, + { + "kind": "index", + "table": "status_page_incidents", + "name": "status_page_incidents_page_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"page_id\"", + "\"started_at\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_pages", + "name": "status_pages_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_pages", + "name": "status_pages_slug_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"slug\"" + ], + "where": null + }, + { + "kind": "index", + "table": "status_pages", + "name": "status_pages_unpublished_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"revision\"", + "\"published_revision\"" + ], + "where": "revision > published_revision" + }, + { + "kind": "index", + "table": "supabase_connections", + "name": "supabase_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "supabase_connections", + "name": "supabase_connections_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "supabase_project_insights", + "name": "supabase_project_insights_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "supabase_projects", + "name": "supabase_projects_connection_ref_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"connection_id\"", + "\"project_ref\"" + ], + "where": null + }, + { + "kind": "index", + "table": "supabase_projects", + "name": "supabase_projects_log_drain_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"log_drain_source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "supabase_projects", + "name": "supabase_projects_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "uptime_monitor_daily", + "name": "uptime_monitor_daily_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "uptime_monitor_states", + "name": "uptime_monitor_states_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "uptime_monitors", + "name": "uptime_monitors_enabled_kind_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"enabled\"", + "\"kind\"" + ], + "where": null + }, + { + "kind": "index", + "table": "uptime_monitors", + "name": "uptime_monitors_heartbeat_token_unq", + "unique": true, + "method": "btree", + "columns": [ + "\"heartbeat_token\"" + ], + "where": null + }, + { + "kind": "index", + "table": "uptime_monitors", + "name": "uptime_monitors_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_commits", + "name": "vcs_commits_org_sha_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_commits", + "name": "vcs_commits_repo_sha_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"sha\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_provider_external_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"provider\"", + "\"external_installation_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_installations", + "name": "vcs_installations_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_installation_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"installation_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repositories", + "name": "vcs_repositories_org_repo_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"", + "\"external_repo_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repository_branches", + "name": "vcs_repository_branches_org_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vcs_repository_branches", + "name": "vcs_repository_branches_repo_name_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"repository_id\"", + "\"name\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vercel_connections", + "name": "vercel_connections_org_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vercel_connections", + "name": "vercel_connections_source_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"source_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vercel_deployments", + "name": "vercel_deployments_org_deployment_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"deployment_id\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vercel_deployments", + "name": "vercel_deployments_org_project_created_idx", + "unique": false, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"project_id\"", + "\"created_at_vercel\"" + ], + "where": null + }, + { + "kind": "index", + "table": "vercel_projects", + "name": "vercel_projects_org_project_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"project_id\"" + ], + "where": null + }, + { + "kind": "foreign_key", + "table": "alert_escalation_runs", + "name": "alert_escalation_runs_incident_id_alert_incidents_id_fk", + "columns": [ + "incident_id" + ], + "foreignTable": "alert_incidents", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "alert_incident_events", + "name": "alert_incident_events_incident_id_alert_incidents_id_fk", + "columns": [ + "incident_id" + ], + "foreignTable": "alert_incidents", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "alert_linear_issues", + "name": "alert_linear_issues_destination_id_alert_destinations_id_fk", + "columns": [ + "destination_id" + ], + "foreignTable": "alert_destinations", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "alert_member_notifications", + "name": "alert_member_notifications_incident_id_alert_incidents_id_fk", + "columns": [ + "incident_id" + ], + "foreignTable": "alert_incidents", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "aws_connections", + "name": "aws_connections_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "cloudflare_logpush_connectors", + "name": "cloudflare_logpush_connectors_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "SET NULL", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "dashboard_shares", + "name": "dashboard_shares_dashboard_fk", + "columns": [ + "org_id", + "dashboard_id" + ], + "foreignTable": "dashboards", + "foreignColumns": [ + "org_id", + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "external_data_source_catalog_entries", + "name": "external_data_source_catalog_entries_x60VonL2VphD_fk", + "columns": [ + "source_id" + ], + "foreignTable": "external_data_sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "gcp_connectors", + "name": "gcp_connectors_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "gcp_resources", + "name": "gcp_resources_connector_id_gcp_connectors_id_fk", + "columns": [ + "connector_id" + ], + "foreignTable": "gcp_connectors", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "oauth_connections", + "name": "oauth_connections_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "oncall_overrides", + "name": "oncall_overrides_schedule_id_oncall_schedules_id_fk", + "columns": [ + "schedule_id" + ], + "foreignTable": "oncall_schedules", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "planetscale_connections", + "name": "planetscale_connections_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "railway_connections", + "name": "railway_connections_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "scrape_target_checks", + "name": "scrape_target_checks_target_id_scrape_targets_id_fk", + "columns": [ + "target_id" + ], + "foreignTable": "scrape_targets", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "scrape_targets", + "name": "scrape_targets_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "search_console_sites", + "name": "search_console_sites_jBAHYyW36dnu_fk", + "columns": [ + "oauth_connection_id" + ], + "foreignTable": "oauth_connections", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_components", + "name": "status_page_components_monitor_id_uptime_monitors_id_fk", + "columns": [ + "monitor_id" + ], + "foreignTable": "uptime_monitors", + "foreignColumns": [ + "id" + ], + "onDelete": "SET NULL", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_components", + "name": "status_page_components_page_id_status_pages_id_fk", + "columns": [ + "page_id" + ], + "foreignTable": "status_pages", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_custom_domains", + "name": "status_page_custom_domains_page_id_status_pages_id_fk", + "columns": [ + "page_id" + ], + "foreignTable": "status_pages", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_incident_components", + "name": "status_page_incident_components_Bnqlt6wwKJty_fk", + "columns": [ + "component_id" + ], + "foreignTable": "status_page_components", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_incident_components", + "name": "status_page_incident_components_tBn6mEz4FenY_fk", + "columns": [ + "incident_id" + ], + "foreignTable": "status_page_incidents", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_incident_updates", + "name": "status_page_incident_updates_3LDDUAhznHPM_fk", + "columns": [ + "incident_id" + ], + "foreignTable": "status_page_incidents", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "status_page_incidents", + "name": "status_page_incidents_page_id_status_pages_id_fk", + "columns": [ + "page_id" + ], + "foreignTable": "status_pages", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "supabase_connections", + "name": "supabase_connections_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "supabase_project_insights", + "name": "supabase_project_insights_project_id_supabase_projects_id_fk", + "columns": [ + "project_id" + ], + "foreignTable": "supabase_projects", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "supabase_projects", + "name": "supabase_projects_connection_id_supabase_connections_id_fk", + "columns": [ + "connection_id" + ], + "foreignTable": "supabase_connections", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "supabase_projects", + "name": "supabase_projects_log_drain_source_id_sources_id_fk", + "columns": [ + "log_drain_source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "SET NULL", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "uptime_monitor_daily", + "name": "uptime_monitor_daily_monitor_id_uptime_monitors_id_fk", + "columns": [ + "monitor_id" + ], + "foreignTable": "uptime_monitors", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "uptime_monitor_states", + "name": "uptime_monitor_states_monitor_id_uptime_monitors_id_fk", + "columns": [ + "monitor_id" + ], + "foreignTable": "uptime_monitors", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "vcs_installations", + "name": "vcs_installations_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + }, + { + "kind": "foreign_key", + "table": "vercel_connections", + "name": "vercel_connections_source_id_sources_id_fk", + "columns": [ + "source_id" + ], + "foreignTable": "sources", + "foreignColumns": [ + "id" + ], + "onDelete": "CASCADE", + "onUpdate": "NO ACTION" + } + ] +} diff --git a/packages/db/src/tables/alerts.ts b/packages/db/src/tables/alerts.ts index cf1e9ef99d..8d88a6fcf7 100644 --- a/packages/db/src/tables/alerts.ts +++ b/packages/db/src/tables/alerts.ts @@ -15,6 +15,7 @@ import { import { StoredAlertMaintenanceSchedule } from "@maple/domain/alert-maintenance-windows" import { AlertIncidentLabels } from "@maple/domain/alerts" import { AnomalySignalType } from "@maple/domain/anomalies" +import { AuditDetectionFilters } from "@maple/domain/audit-trail" import { Schema } from "effect" const StringArray = Schema.Array(Schema.String) @@ -83,6 +84,12 @@ export const AlertRules = PG.table("alert_rules", { anomalySignalsJson: PG.column(PG.nullable(PG.jsonb(Schema.Array(AnomalySignalType))), { name: "anomaly_signals_json", }), + /** Audit detections only: which events the rule counts. Null/empty means every event. */ + auditFiltersJson: PG.column(PG.nullable(PG.jsonb(AuditDetectionFilters)), { + name: "audit_filters_json", + }), + /** Audit detections only: events that arrived before this do not count. Moves when what is counted changes. */ + auditCountFrom: PG.column(PG.nullable(PG.timestamptzMillis), { name: "audit_count_from" }), /** JSON-encoded `string[]` of free-form tags used to group and filter rules. */ tagsJson: PG.column(PG.nullable(PG.jsonb(StringArray)), { name: "tags_json" }), signalType: PG.column(PG.text, { name: "signal_type" }), diff --git a/packages/domain/src/audit-trail.ts b/packages/domain/src/audit-trail.ts index da87d4c27c..753f00cd40 100644 --- a/packages/domain/src/audit-trail.ts +++ b/packages/domain/src/audit-trail.ts @@ -1,7 +1,7 @@ import { Schema } from "effect" /** Platforms whose audit events the trail holds. One literal per built adapter; `maple` is Maple's own audit log. */ -export const AuditTrailProvider = Schema.Literals(["maple"]) +export const AuditTrailProvider = Schema.Literals(["maple", "vercel", "gcp"]) export type AuditTrailProvider = typeof AuditTrailProvider.Type /** @@ -30,6 +30,20 @@ export type AuditTrailActorType = typeof AuditTrailActorType.Type export const AuditTrailOutcome = Schema.Literals(["success", "failure", "denied", "unknown"]) export type AuditTrailOutcome = typeof AuditTrailOutcome.Type +/** + * What an audit log detection counts. `action` matches exactly. No actor or origin filter on + * purpose: rule rows sync to every member's browser, so nothing in a rule may name a person. + */ +export const AuditDetectionFilters = Schema.Struct({ + provider: Schema.optionalKey(AuditTrailProvider), + category: Schema.optionalKey(AuditTrailCategory), + outcome: Schema.optionalKey(AuditTrailOutcome), + action: Schema.optionalKey( + Schema.String.check(Schema.isTrimmed(), Schema.isMinLength(1), Schema.isMaxLength(256)), + ), +}) +export type AuditDetectionFilters = typeof AuditDetectionFilters.Type + /** * One audit event as a provider adapter hands it over. `provider`, `account` and * `providerEventId` make the stored row's key: a redelivery collapses only when it also diff --git a/packages/domain/src/generated/anticipated-error-identifiers.ts b/packages/domain/src/generated/anticipated-error-identifiers.ts index 00d1288c9a..7b9048e7ff 100644 --- a/packages/domain/src/generated/anticipated-error-identifiers.ts +++ b/packages/domain/src/generated/anticipated-error-identifiers.ts @@ -33,6 +33,8 @@ export const ANTICIPATED_ERROR_IDENTIFIER_LIST: ReadonlyArray = [ "@maple/http/errors/AlertSilenceNotFoundError", "@maple/http/errors/AlertValidationError", "@maple/http/errors/ApiKeyNotFoundError", + "@maple/http/errors/AuditDetectionInvalidError", + "@maple/http/errors/AuditDetectionNotFoundError", "@maple/http/errors/AuditTrailForbiddenError", "@maple/http/errors/AwsAccountAlreadyConnectedError", "@maple/http/errors/AwsIntegrationUnavailableError", diff --git a/packages/domain/src/http/alerts.ts b/packages/domain/src/http/alerts.ts index a3104b5437..5f20de89f8 100644 --- a/packages/domain/src/http/alerts.ts +++ b/packages/domain/src/http/alerts.ts @@ -55,6 +55,8 @@ export const AlertSignalType = Schema.Literals([ "slo_burn_rate", /** Pages on the anomaly detector's open incidents in the rule's scope; it has no query. */ "anomaly", + /** The hidden rule behind an audit log detection; managed only through the Security hub. */ + "audit", ]).annotate({ identifier: "@maple/AlertSignalType", title: "Alert Signal Type", @@ -65,6 +67,7 @@ export type AlertSignalType = Schema.Schema.Type export const MANAGED_SIGNAL_TYPES = [ "uptime", "slo_burn_rate", + "audit", ] as const satisfies ReadonlyArray export type ManagedSignalType = (typeof MANAGED_SIGNAL_TYPES)[number] export const isManagedSignalType = (signalType: string): signalType is ManagedSignalType => @@ -1359,6 +1362,7 @@ export class AlertRuleStoredConfigInvalidError extends HttpTaggedError()( "@maple/http/errors/AuditTrailPersistenceError", /** `cause` names the failure behind it (its tag), never its details. */ @@ -114,6 +183,35 @@ export class AuditTrailForbiddenError extends HttpTaggedError()( + "@maple/http/errors/AuditDetectionNotFoundError", + { detectionId: AlertRuleId, message: Schema.String }, + { + status: 404, + code: "audit_detection_not_found", + title: "Detection not found", + message: "No such detection.", + param: "id", + retry: "never", + recovery: "none", + exposure: "redacted", + }, +) {} + +/** An unknown destination, a name another alert rule has, or the per-org cap. */ +export class AuditDetectionInvalidError extends HttpTaggedError()( + "@maple/http/errors/AuditDetectionInvalidError", + { message: Schema.String }, + { + status: 400, + code: "audit_detection_invalid", + title: "Invalid detection", + retry: "never", + recovery: "fix_request", + exposure: "public_message", + }, +) {} + export class AuditTrailApiGroup extends HttpApiGroup.make("auditTrail") .add( HttpApiEndpoint.get("list", "/events", { @@ -122,5 +220,38 @@ export class AuditTrailApiGroup extends HttpApiGroup.make("auditTrail") error: [AuditTrailPersistenceError, AuditTrailForbiddenError], }), ) + .add( + HttpApiEndpoint.get("listDetections", "/detections", { + success: AuditDetectionListResponse, + error: [AuditTrailPersistenceError, AuditTrailForbiddenError], + }), + ) + .add( + HttpApiEndpoint.post("createDetection", "/detections", { + payload: AuditDetectionCreateRequest, + success: AuditDetection, + error: [AuditTrailPersistenceError, AuditTrailForbiddenError, AuditDetectionInvalidError], + }), + ) + .add( + HttpApiEndpoint.patch("updateDetection", "/detections/:id", { + params: { id: AlertRuleId }, + payload: AuditDetectionUpdateRequest, + success: AuditDetection, + error: [ + AuditTrailPersistenceError, + AuditTrailForbiddenError, + AuditDetectionNotFoundError, + AuditDetectionInvalidError, + ], + }), + ) + .add( + HttpApiEndpoint.delete("deleteDetection", "/detections/:id", { + params: { id: AlertRuleId }, + success: AuditDetectionDeleteResponse, + error: [AuditTrailPersistenceError, AuditTrailForbiddenError, AuditDetectionNotFoundError], + }), + ) .prefix("/internal/audit-trail") .middleware(SessionAuthorization) {} diff --git a/packages/domain/src/http/query-engine.ts b/packages/domain/src/http/query-engine.ts index 466cdb243b..2a656bb7b0 100644 --- a/packages/domain/src/http/query-engine.ts +++ b/packages/domain/src/http/query-engine.ts @@ -1107,21 +1107,6 @@ export class VercelWebAnalyticsResponse extends Schema.Class("VercelAuditLogResponse")({ - events: Schema.Array(VercelAuditEvent), -}) {} - // Google Cloud infrastructure page (/infra/gcp): `gcp.*` metrics from the Google Cloud poller. const GcpInfraService = Schema.Literals(GCP_INFRA_SERVICE_IDS) @@ -4426,13 +4411,6 @@ export class QueryEngineApiGroup extends HttpApiGroup.make("queryEngine") error: queryEngineEndpointErrors, }), ) - .add( - HttpApiEndpoint.post("vercelAuditLog", "/vercel-audit-log", { - payload: VercelInfraProjectsRequest, - success: VercelAuditLogResponse, - error: queryEngineEndpointErrors, - }), - ) .add( HttpApiEndpoint.post("gcpInfraPresence", "/gcp-infra-presence", { payload: GcpInfraPresenceRequest, diff --git a/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql b/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql index c16dc043e2..c1b39afafe 100644 --- a/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql +++ b/packages/query-engine-integrations/src/__sql_baseline__/integrations.sql @@ -5495,24 +5495,6 @@ SELECT LIMIT 2000 FORMAT JSON --- builder:vercel-event-drains:vercelAuditLogSQL:default -SELECT - logs.Timestamp AS timestamp, - logs.LogAttributes['vercel.audit.action'] AS action, - if(logs.LogAttributes['vercel.audit.actor.name'] != '', logs.LogAttributes['vercel.audit.actor.name'], logs.LogAttributes['vercel.audit.actor.id']) AS actor, - logs.LogAttributes['vercel.audit.actor.type'] AS actorType, - logs.ResourceAttributes['vercel.project.id'] AS projectId - FROM logs - WHERE logs.OrgId = 'org_sql_catalog' - AND logs.ServiceName = 'vercel-audit-log' - AND logs.TimestampTime >= '2026-01-01 10:30:00' - AND logs.TimestampTime <= '2026-01-03 14:15:00' - AND logs.Timestamp >= '2026-01-01 10:30:00' - AND logs.Timestamp <= '2026-01-03 14:15:00' - ORDER BY timestamp DESC - LIMIT 25 - FORMAT JSON - -- builder:vercel-event-drains:vercelSpeedInsightsSQL:device SELECT logs.LogAttributes['vercel.device.type'] AS key, diff --git a/packages/query-engine-integrations/src/benchmark/index.ts b/packages/query-engine-integrations/src/benchmark/index.ts index a7455814a2..eb360a09a1 100644 --- a/packages/query-engine-integrations/src/benchmark/index.ts +++ b/packages/query-engine-integrations/src/benchmark/index.ts @@ -960,12 +960,6 @@ export const integrationFixtures: ReadonlyArray = [ compile: () => compileUnsafe(CH.vercelWebAnalyticsTopSQL("event", { projectId: VERCEL_PROJECT_ID }), window), }, - { - module: "vercel-event-drains", - name: "vercelAuditLogSQL", - label: "default", - compile: () => compileUnsafe(CH.vercelAuditLogSQL(), window), - }, { module: "vercel-ai-gateway", name: "vercelAiGatewaySummarySQL", diff --git a/packages/query-engine-integrations/src/vercel/index.ts b/packages/query-engine-integrations/src/vercel/index.ts index 52293b51ba..ea70f1f249 100644 --- a/packages/query-engine-integrations/src/vercel/index.ts +++ b/packages/query-engine-integrations/src/vercel/index.ts @@ -32,7 +32,6 @@ export { vercelProjectFunctionsSQL, } from "./vercel-functions" export { - vercelAuditLogSQL, vercelSpeedInsightsSQL, vercelWebAnalyticsSummarySQL, vercelWebAnalyticsTopSQL, diff --git a/packages/query-engine-integrations/src/vercel/vercel-event-drains.ts b/packages/query-engine-integrations/src/vercel/vercel-event-drains.ts index b3999aa325..cd3686b5a4 100644 --- a/packages/query-engine-integrations/src/vercel/vercel-event-drains.ts +++ b/packages/query-engine-integrations/src/vercel/vercel-event-drains.ts @@ -1,9 +1,10 @@ -// Vercel Speed Insights, Web Analytics and audit log (/infra/vercel) +// Vercel Speed Insights and Web Analytics (/infra/vercel) // // Each drain lands in `logs` under its own service (ingest maps them in // `apps/ingest/src/vercel_event_drains.rs`), one record per event, the project on -// resource `vercel.project.id` (empty for team events). Filtering on the service -// prunes on the logs sort key. +// resource `vercel.project.id`. Filtering on the service prunes on the logs sort key. +// Audit log events go to the admin-only `audit_trail_events` where the gateway stores the audit +// trail; a gateway that does not keeps them in `logs` under `vercel-audit-log`. import * as CH from "@maple-dev/effect-orm/expr" import { from, param, type ColumnAccessor } from "@maple-dev/effect-orm/clickhouse" @@ -16,7 +17,6 @@ type LogColumns = ColumnAccessor const SPEED_INSIGHTS_SERVICE = "vercel-speed-insights" const WEB_ANALYTICS_SERVICE = "vercel-web-analytics" -const AUDIT_LOG_SERVICE = "vercel-audit-log" const drainWhere = ($: LogColumns, service: string, scope: VercelProjectScope) => [ $.OrgId.eq(orgIdParam), @@ -152,23 +152,3 @@ export function vercelWebAnalyticsTopSQL( .limit(10) .format("JSON") } - -/** The team's newest audit events. */ -export function vercelAuditLogSQL() { - return from(Datasources.logs) - .select(($) => ({ - timestamp: $.Timestamp, - action: $.LogAttributes.get("vercel.audit.action"), - actor: CH.if_( - $.LogAttributes.get("vercel.audit.actor.name").neq(""), - $.LogAttributes.get("vercel.audit.actor.name"), - $.LogAttributes.get("vercel.audit.actor.id"), - ), - actorType: $.LogAttributes.get("vercel.audit.actor.type"), - projectId: $.ResourceAttributes.get("vercel.project.id"), - })) - .where(($) => drainWhere($, AUDIT_LOG_SERVICE, {})) - .orderBy(["timestamp", "desc"]) - .limit(25) - .format("JSON") -} diff --git a/packages/query-engine-integrations/src/vercel/vercel.test.ts b/packages/query-engine-integrations/src/vercel/vercel.test.ts index f7ca9274ae..c5849b901f 100644 --- a/packages/query-engine-integrations/src/vercel/vercel.test.ts +++ b/packages/query-engine-integrations/src/vercel/vercel.test.ts @@ -22,7 +22,6 @@ import { vercelProjectFunctionsSQL, } from "./vercel-functions" import { - vercelAuditLogSQL, vercelSpeedInsightsSQL, vercelWebAnalyticsSummarySQL, vercelWebAnalyticsTopSQL, @@ -281,18 +280,6 @@ describe("Web Analytics", () => { }) }) -describe("vercelAuditLogSQL", () => { - it("lists the newest audit events, the actor by name or else id", () => { - const { sql } = compileUnsafe(vercelAuditLogSQL(), window) - expect(sql).toContain("logs.ServiceName = 'vercel-audit-log'") - expect(sql).toContain( - "if(logs.LogAttributes['vercel.audit.actor.name'] != '', logs.LogAttributes['vercel.audit.actor.name'], logs.LogAttributes['vercel.audit.actor.id']) AS actor", - ) - expect(sql).toContain("ORDER BY timestamp DESC") - expect(sql).toContain("LIMIT 25") - }) -}) - describe("AI Gateway", () => { it("counts gateway server spans, parented or not, and sums the USD cost", () => { const { sql } = compileUnsafe(vercelAiGatewaySummarySQL(), window) diff --git a/packages/query-engine/src/__sql_baseline__/catalog.sql b/packages/query-engine/src/__sql_baseline__/catalog.sql index 76f56639e3..f17df9fdd5 100644 --- a/packages/query-engine/src/__sql_baseline__/catalog.sql +++ b/packages/query-engine/src/__sql_baseline__/catalog.sql @@ -373,6 +373,20 @@ SELECT OFFSET 50 FORMAT JSON +-- builder:audit-trail:auditTrailCountQuery:filtered [ee015597] +SELECT + uniqExact(audit_trail_events.Id) AS events + FROM audit_trail_events + WHERE audit_trail_events.OrgId = 'org_sql_catalog' + AND audit_trail_events.IngestedAt > '2026-01-01 10:30:00' + AND audit_trail_events.IngestedAt <= '2026-01-03 14:15:00' + AND audit_trail_events.OccurredAt >= '2026-01-01 10:30:00' - INTERVAL 604800 SECOND + AND audit_trail_events.Provider = 'vercel' + AND audit_trail_events.Category = 'membership' + AND audit_trail_events.Outcome = 'success' + AND audit_trail_events.Action = 'team.member.added' + FORMAT JSON + -- builder:audit-trail:auditTrailEventsQuery:default [9ab77231] SELECT audit_trail_events.Id AS id, diff --git a/packages/query-engine/src/benchmark/builders.ts b/packages/query-engine/src/benchmark/builders.ts index 0122c2aee8..d4e3a06efe 100644 --- a/packages/query-engine/src/benchmark/builders.ts +++ b/packages/query-engine/src/benchmark/builders.ts @@ -1502,6 +1502,25 @@ export const builderFixtures: ReadonlyArray = [ }, ), }, + // Audit detection count (AuditTrailService.ts `count`), every optional filter bound. + { + module: "audit-trail", + name: "auditTrailCountQuery", + label: "filtered", + compile: () => + CH.compileUnsafe( + CH.auditTrailCountQuery({ provider: true, category: true, outcome: true, action: true }), + { + orgId: ORG_ID, + provider: "vercel", + category: "membership", + outcome: "success", + action: "team.member.added", + start: START_TIME, + end: END_TIME, + }, + ), + }, // Cancellation evidence (CancellationTelemetryEvidence.read). { module: "cancellation-evidence", diff --git a/packages/query-engine/src/ch/queries/audit-trail.ts b/packages/query-engine/src/ch/queries/audit-trail.ts index c3c4fb400e..b1a22ad807 100644 --- a/packages/query-engine/src/ch/queries/audit-trail.ts +++ b/packages/query-engine/src/ch/queries/audit-trail.ts @@ -2,9 +2,11 @@ // Every read is pinned to the managed route, like `audit_log`: the table is written through // `ingest` and does not exist in a BYO ClickHouse. +import { Schema } from "effect" import * as CH from "@maple-dev/effect-orm/expr" import { from, param } from "@maple-dev/effect-orm/clickhouse" import * as Datasources from "@maple/domain/tinybird/datasources" +import { CHNumber } from "../schema" import { orgIdParam } from "../tables" /** @@ -88,3 +90,39 @@ export function auditTrailEventsQuery(opts: AuditTrailEventsOpts) { .route("ingest") ) } + +/** Which optional filters a count applies; each set flag binds a parameter of the same name. */ +export interface AuditTrailCountOpts { + readonly provider?: boolean + readonly category?: boolean + readonly outcome?: boolean + readonly action?: boolean +} + +export const auditTrailCountRowSchema = Schema.Struct({ events: CHNumber }) + +/** How far back an event may have happened and still be counted when it arrives. */ +const AUDIT_TRAIL_LATE_ARRIVAL_SECONDS = 7 * 24 * 60 * 60 + +/** + * How many of one org's events arrived in `(start, end]`, for a detection. Distinct ids, not + * rows: a redelivery is a second row until the table merges it. Params: orgId, start, end. + */ +export function auditTrailCountQuery(opts: AuditTrailCountOpts = {}) { + return from(Datasources.auditTrailEvents) + .select(($) => ({ events: CH.uniqExact($.Id) })) + .where(($) => [ + $.OrgId.eq(orgIdParam), + $.IngestedAt.gt(param.dateTime("start")), + $.IngestedAt.lte(param.dateTime("end")), + // Prunes partitions and the sorting key, which follow OccurredAt. An event can + // arrive late; a week covers every provider's delivery delay. + $.OccurredAt.gte(CH.intervalSub(param.dateTime("start"), AUDIT_TRAIL_LATE_ARRIVAL_SECONDS)), + CH.whenTrue(!!opts.provider, () => $.Provider.eq(param.string("provider"))), + CH.whenTrue(!!opts.category, () => $.Category.eq(param.string("category"))), + CH.whenTrue(!!opts.outcome, () => $.Outcome.eq(param.string("outcome"))), + CH.whenTrue(!!opts.action, () => $.Action.eq(param.string("action"))), + ]) + .format("JSON") + .route("ingest") +} diff --git a/packages/views/src/ui/INDEX.md b/packages/views/src/ui/INDEX.md index 5c38455362..287d029aaa 100644 --- a/packages/views/src/ui/INDEX.md +++ b/packages/views/src/ui/INDEX.md @@ -295,7 +295,7 @@ existing copies onto it (see `apps/web/CLAUDE.md`). - `apps/web/src/page/kit/service-metrics/markers-view.ts` `markersOverlay`: Commit markers over a chart, idle. - `apps/web/src/page/kit/service-metrics/markers.ts` `commitMarkers`, `markerShas`, `markerLayout`: Commit markers over a chart: the markers, their commit labels and their geometry (./marker-layout.ts). - `apps/web/src/page/kit/service-runtimes.ts` `runtimeLink`, `workloadLink`: Where a service runs, as links to the infra pages: its platform resources (`ServiceRuntimesResponse`) and its Kubernetes workloads. -- `apps/web/src/page/kit/session-grants.ts` `fetchSessionGrants`, `acceptGrants`, `canManageAlerts`, `canRespondToIncidents`, `canRunInvestigations`: A page's read of the viewer's permissions: the answer carries the org it was asked for, so a late answer for another org is dropped. +- `apps/web/src/page/kit/session-grants.ts` `fetchSessionGrants`, `acceptGrants`, `canManageAlerts`, `canRespondToIncidents`, `canRunInvestigations`, `canReadAuditLog`: A page's read of the viewer's permissions: the answer carries the org it was asked for, so a late answer for another org is dropped. - `apps/web/src/page/kit/settings-section.ts` `settingsSection`, `settingsSections`, `adminOnlyNote`, `sectionNote`: A settings tab's titled sections, the stack they sit in, and the admin-only note. - `apps/web/src/page/kit/severity-tokens.ts` `statusLabel`: Canonical severity/status → Tailwind class maps for the infrastructure UI. - `apps/web/src/page/kit/sheet-detail-header.ts` `sheetDetailHeader`: A detail sheet's eyebrow kind, title, screen-reader description and meta row. diff --git a/packages/web-data/src/alerts/form-utils.ts b/packages/web-data/src/alerts/form-utils.ts index 650d1e3845..4695753d45 100644 --- a/packages/web-data/src/alerts/form-utils.ts +++ b/packages/web-data/src/alerts/form-utils.ts @@ -143,6 +143,7 @@ export const signalLabels: Record = { uptime: "Uptime check", slo_burn_rate: "SLO burn rate", anomaly: "Anomaly", + audit: "Audit log", } satisfies Record export const RAW_QUERY_REDUCER_LABELS: Record = { @@ -204,6 +205,9 @@ export function formatSignalValue(signalType: AlertSignalType, value: number | n // An anomaly rule observes the open anomaly's severity: 0 none, 1 warning, 2 critical. case "anomaly": return value >= 2 ? "Critical" : value >= 1 ? "Warning" : "Normal" + // An audit detection observes how many matching events arrived in its window. + case "audit": + return formatNumber(value) } } diff --git a/packages/web-data/src/alerts/overview.test.ts b/packages/web-data/src/alerts/overview.test.ts index 28a921c392..daed42d68b 100644 --- a/packages/web-data/src/alerts/overview.test.ts +++ b/packages/web-data/src/alerts/overview.test.ts @@ -14,6 +14,7 @@ const iso = (msAgo: number) => new Date(NOW - msAgo).toISOString() const RULE_A = "00000000-0000-4000-8000-00000000000a" const RULE_B = "00000000-0000-4000-8000-00000000000b" const RULE_C = "00000000-0000-4000-8000-00000000000c" +const RULE_D = "00000000-0000-4000-8000-00000000000d" function makeRule(overrides: Record = {}): AlertRuleDocument { return decodeRuleDoc({ @@ -211,16 +212,18 @@ describe("deriveOverview", () => { expect(overview.timelineRange).toEqual({ min: NOW - DAY_MS, max: NOW }) }) - it("hides monitors' and SLOs' backing rules but keeps their incidents in the inbox", () => { + it("hides monitors', SLOs' and audit detections' backing rules but keeps their incidents in the inbox", () => { const overview = deriveOverview({ rules: [ makeRule({ id: RULE_A }), makeRule({ id: RULE_B, signalType: "uptime", name: "API health" }), makeRule({ id: RULE_C, signalType: "slo_burn_rate", name: "SLO burn: Checkout" }), + makeRule({ id: RULE_D, signalType: "audit", name: "Members added" }), ], incidents: [ makeIncident({ ruleId: RULE_B, ruleName: "API health", signalType: "uptime" }), makeIncident({ ruleId: RULE_C, ruleName: "SLO burn: Checkout", signalType: "slo_burn_rate" }), + makeIncident({ ruleId: RULE_D, ruleName: "Members added", signalType: "audit" }), ], states: [], deliveryEvents: [], @@ -232,6 +235,6 @@ describe("deriveOverview", () => { Arr.map(overview.openIncidents, (incident) => incident.ruleName), Order.String, ), - ).toEqual(["API health", "SLO burn: Checkout"]) + ).toEqual(["API health", "Members added", "SLO burn: Checkout"]) }) }) diff --git a/packages/web-data/src/collections/alerts.ts b/packages/web-data/src/collections/alerts.ts index 59812088b5..1c19685fd5 100644 --- a/packages/web-data/src/collections/alerts.ts +++ b/packages/web-data/src/collections/alerts.ts @@ -71,6 +71,7 @@ export const AlertRuleRowSchema = Schema.Struct({ exclude_service_names_json: Schema.NullOr(Schema.Unknown), environments_json: Schema.NullOr(Schema.Unknown), anomaly_signals_json: Schema.optionalKey(Schema.NullOr(Schema.Unknown)), + audit_filters_json: Schema.optionalKey(Schema.NullOr(Schema.Unknown)), tags_json: Schema.NullOr(Schema.Unknown), signal_type: Schema.String, comparator: Schema.String, diff --git a/packages/web-data/src/warehouse/vercel-infra.ts b/packages/web-data/src/warehouse/vercel-infra.ts index fd899ba465..b4fa1905a2 100644 --- a/packages/web-data/src/warehouse/vercel-infra.ts +++ b/packages/web-data/src/warehouse/vercel-infra.ts @@ -287,24 +287,6 @@ export const getVercelWebAnalytics = Effect.fn("QueryEngine.getVercelWebAnalytic ) }) -export const getVercelAuditLog = Effect.fn("QueryEngine.getVercelAuditLog")(function* ({ - data, -}: { - data: VercelTimeRangeInput -}) { - const input = yield* decodeInput(TimeRangeInputSchema, data, "getVercelAuditLog") - const nowMillis = yield* Clock.currentTimeMillis - const range = resolveRange(input, nowMillis) - return yield* runWarehouseQuery("vercelAuditLog", () => - Effect.gen(function* () { - const client = yield* MapleInternalHttp - return yield* client.queryEngine.vercelAuditLog({ - payload: new VercelInfraProjectsRequest(range), - }) - }), - ) -}) - /** One project's functions per bucket, for every route or one. */ export const getVercelFunctionTimeseries = Effect.fn("QueryEngine.getVercelFunctionTimeseries")(function* ({ data,