From b67357dae862e46ab361795d4a07f81e33005325 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 12 Nov 2026 23:32:37 +0100 Subject: [PATCH 1/9] feat(cost): Cloudflare as a cost source, read with a pasted API token An org's Cloudflare charges are read from the billable usage API: one row per day, product and priced metric, in the account's billing currency. Paid subscriptions add their fee on the day each billing period starts, stepped back month by month from the current period at today's price. The token is saved through PUT /internal/cost/cloudflare. It is checked with Cloudflare first: it must read exactly one account, that account must have usage-based billing data, and one usage read must succeed. A replacement token must read the same account, and makes a failing source due again. The credential lives in a new generic table, cost_connections, one row per org and provider, with one secret slot bound to the org and the provider. A shared store seals, upserts, removes and loads it, and one reconcile keeps cost_sources in step for every provider listed in COST_CONNECTION_PROVIDERS, so the next provider read with a pasted credential needs no migration. OpenRouter keeps its own table. Two facts of the usage API shape the pull. Its `to` date is exclusive, and a billing period's rows come back only when the period starts inside the asked range. So each request asks from 31 days before its first day to the day after its last, in pieces of 55 days to stay under the 90 day cap, and keeps only its own days. The sync profile serves from the eleventh month back to yesterday. --- .../routes/internal/cloudflare-cost.http.ts | 50 + apps/api/src/runtime/http-graph.ts | 2 + apps/api/src/runtime/service-graph.ts | 2 + apps/web/src/page/cost/providers.ts | 5 + apps/web/src/page/cost/view-sources.ts | 4 +- packages/backend/src/platform/secret-slots.ts | 10 + .../CloudflareCostConnectionService.test.ts | 320 + .../cost/CloudflareCostConnectionService.ts | 186 + .../src/services/cost/CostConnectionStore.ts | 212 + .../src/services/cost/CostSyncService.test.ts | 94 + .../src/services/cost/CostSyncService.ts | 26 +- .../services/cost/cost-connection-sources.ts | 98 + .../cloudflare/CloudflareCostProvider.test.ts | 310 + .../cloudflare/CloudflareCostProvider.ts | 152 + .../cloudflare/cloudflare-billing-api.ts | 221 + .../providers/cloudflare/fake-cloudflare.ts | 169 + .../providers/cloudflare/normalize.test.ts | 266 + .../cost/providers/cloudflare/normalize.ts | 160 + .../services/cost/providers/fake-cost-env.ts | 22 + .../providers/openrouter/fake-openrouter.ts | 26 +- .../src/services/cost/sync-plan.test.ts | 64 + .../backend/src/services/cost/sync-plan.ts | 6 + .../src/services/org/OrganizationService.ts | 2 + .../src/services/secrets/reseal-targets.ts | 9 + .../migration.sql | 18 + .../snapshot.json | 22723 ++++++++++++++++ packages/db/src/tables/cost-connections.ts | 34 + packages/db/src/tables/index.ts | 3 + packages/domain/src/cost.ts | 2 +- .../anticipated-error-identifiers.ts | 2 + packages/domain/src/http/cloudflare-cost.ts | 43 + packages/domain/src/http/cost-connections.ts | 57 + packages/domain/src/http/index.ts | 2 + packages/domain/src/http/internal-api.ts | 2 + 34 files changed, 25275 insertions(+), 27 deletions(-) create mode 100644 apps/api/src/routes/internal/cloudflare-cost.http.ts create mode 100644 packages/backend/src/services/cost/CloudflareCostConnectionService.test.ts create mode 100644 packages/backend/src/services/cost/CloudflareCostConnectionService.ts create mode 100644 packages/backend/src/services/cost/CostConnectionStore.ts create mode 100644 packages/backend/src/services/cost/cost-connection-sources.ts create mode 100644 packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts create mode 100644 packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts create mode 100644 packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts create mode 100644 packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts create mode 100644 packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts create mode 100644 packages/backend/src/services/cost/providers/cloudflare/normalize.ts create mode 100644 packages/backend/src/services/cost/providers/fake-cost-env.ts create mode 100644 packages/db/drizzle/20261112221939_cost_connections/migration.sql create mode 100644 packages/db/drizzle/20261112221939_cost_connections/snapshot.json create mode 100644 packages/db/src/tables/cost-connections.ts create mode 100644 packages/domain/src/http/cloudflare-cost.ts create mode 100644 packages/domain/src/http/cost-connections.ts diff --git a/apps/api/src/routes/internal/cloudflare-cost.http.ts b/apps/api/src/routes/internal/cloudflare-cost.http.ts new file mode 100644 index 0000000000..f584dfeadb --- /dev/null +++ b/apps/api/src/routes/internal/cloudflare-cost.http.ts @@ -0,0 +1,50 @@ +import { HttpApiBuilder } from "effect/http-api" +import { + CostConnectionForbiddenError, + CostConnectionRemoved, + CostConnectionSaved, + CurrentTenant, + MapleInternalApi, +} from "@maple/domain/http" +import { Effect } from "effect" +import * as Policy from "@maple/domain/policy" +import { recordHttpAudit } from "@maple/backend/services/audit/AuditLogService" +import { CloudflareCostConnectionService } from "@maple/backend/services/cost/CloudflareCostConnectionService" + +// A cost source is a connection like any other: the role that manages integrations manages it. +const requireIntegrationsManager = (roles: CurrentTenant.TenantSchema["roles"]) => + Policy.check( + roles, + "integrations:manage", + (denial) => new CostConnectionForbiddenError({ message: denial.message }), + ) + +export const HttpCloudflareCostLive = HttpApiBuilder.group(MapleInternalApi, "cloudflareCost", (handlers) => + Effect.gen(function* () { + const connections = yield* CloudflareCostConnectionService + + return handlers + .handle("set", ({ payload }) => + Effect.gen(function* () { + const tenant = yield* CurrentTenant.Context + yield* requireIntegrationsManager(tenant.roles) + const saved = yield* connections.set(tenant.orgId, tenant.userId, payload.apiToken) + yield* recordHttpAudit("cost_source_key.updated", { + metadata: { provider: "cloudflare", accountId: saved.accountId }, + }) + return new CostConnectionSaved(saved) + }).pipe(Effect.withSpan("HttpCloudflareCost.set")), + ) + .handle("remove", () => + Effect.gen(function* () { + const tenant = yield* CurrentTenant.Context + yield* requireIntegrationsManager(tenant.roles) + yield* connections.remove(tenant.orgId) + yield* recordHttpAudit("cost_source_key.deleted", { + metadata: { provider: "cloudflare" }, + }) + return new CostConnectionRemoved({ removed: true }) + }).pipe(Effect.withSpan("HttpCloudflareCost.remove")), + ) + }), +) diff --git a/apps/api/src/runtime/http-graph.ts b/apps/api/src/runtime/http-graph.ts index 7f4669a5f0..b27e534baf 100644 --- a/apps/api/src/runtime/http-graph.ts +++ b/apps/api/src/runtime/http-graph.ts @@ -11,6 +11,7 @@ import { HttpAiSessionsInternalLive } from "@/routes/internal/ai-sessions.http" import { HttpMcpInternalLive } from "@/routes/internal/mcp.http" import { HttpUsersInternalLive } from "@/routes/internal/users.http" import { HttpAiTriageLive } from "@/routes/internal/ai-triage.http" +import { HttpCloudflareCostLive } from "@/routes/internal/cloudflare-cost.http" import { HttpOpenRouterCostLive } from "@/routes/internal/openrouter-cost.http" import { HttpOpenRouterKeyLive } from "@/routes/internal/openrouter-key.http" import { HttpServiceMapWorldLive } from "@/routes/internal/service-map-world.http" @@ -165,6 +166,7 @@ const ApiInternalRoutes = HttpApiBuilder.layer(MapleInternalApi).pipe( HttpDemoLive, HttpDependencyScanLive, HttpDigestLive, + HttpCloudflareCostLive, HttpOpenRouterCostLive, HttpOpenRouterKeyLive, HttpServiceMapWorldLive, diff --git a/apps/api/src/runtime/service-graph.ts b/apps/api/src/runtime/service-graph.ts index 79a5af8d58..e3d35322e5 100644 --- a/apps/api/src/runtime/service-graph.ts +++ b/apps/api/src/runtime/service-graph.ts @@ -66,6 +66,7 @@ import { SupabaseConnectionService } from "@maple/backend/services/integrations/ import { SupabaseInsightsService } from "@maple/backend/services/integrations/SupabaseInsightsService" import { SupabaseLogsService } from "@maple/backend/services/integrations/SupabaseLogsService" import { SupabaseOAuthService } from "@maple/backend/services/auth/SupabaseOAuthService" +import { CloudflareCostConnectionService } from "@maple/backend/services/cost/CloudflareCostConnectionService" import { OpenRouterCostConnectionService } from "@maple/backend/services/cost/OpenRouterCostConnectionService" import { OrgOpenRouterKeyService } from "@maple/backend/services/org/OrgOpenRouterKeyService" import { ServiceMapWorldService } from "@maple/backend/services/service-map/ServiceMapWorldService" @@ -188,6 +189,7 @@ export const HttpServicesLive = Layer.mergeAll( AwsIngestService.layer, OrgOpenRouterKeyService.layer, OpenRouterCostConnectionService.layer, + CloudflareCostConnectionService.layer, ServiceMapWorldService.layer, AuditLogService.layer, WarehouseQueryService.layer, diff --git a/apps/web/src/page/cost/providers.ts b/apps/web/src/page/cost/providers.ts index 83bf9a0025..b6a90e9696 100644 --- a/apps/web/src/page/cost/providers.ts +++ b/apps/web/src/page/cost/providers.ts @@ -27,4 +27,9 @@ export const COST_PROVIDERS = { iconClassName: "text-foreground", requestUsd: null, }, + cloudflare: { + name: PROVIDERS.cloudflare.name, + icon: PROVIDERS.cloudflare.icon, + requestUsd: null, + }, } as const satisfies Record diff --git a/apps/web/src/page/cost/view-sources.ts b/apps/web/src/page/cost/view-sources.ts index 4eff5e349d..06f3ed57d6 100644 --- a/apps/web/src/page/cost/view-sources.ts +++ b/apps/web/src/page/cost/view-sources.ts @@ -13,7 +13,7 @@ import { typography } from "@maple/views/ui/typography" import { relativeTime } from "../kit/relative-time" import { formatDay, requestsCost } from "./derive" import { Message } from "./message" -import { COST_PROVIDERS } from "./providers" +import { COST_PROVIDERS, type CostProviderEntry } from "./providers" import { AWS_INTEGRATION_HREF } from "./view-add-source" type H = HtmlBuilder @@ -113,7 +113,7 @@ const sourceRow = ( timeZone: string, options: Readonly<{ isBusy: boolean; canManage: boolean }>, ): Html => { - const provider = COST_PROVIDERS[source.provider] + const provider: CostProviderEntry = COST_PROVIDERS[source.provider] const status = STATUS[source.status] return hstack( h, diff --git a/packages/backend/src/platform/secret-slots.ts b/packages/backend/src/platform/secret-slots.ts index 2b0e2dd46c..b6a7d1c5c5 100644 --- a/packages/backend/src/platform/secret-slots.ts +++ b/packages/backend/src/platform/secret-slots.ts @@ -96,6 +96,16 @@ export const openRouterCostKeySlot: SecretSlot = { binding: orgBinding, } +export interface CostConnectionRef extends OrgRef { + readonly provider: string +} + +/** A pasted credential one provider's spend is read with; the table is one row per org and provider. */ +export const costConnectionSecretSlot: SecretSlot = { + name: "cost_connections.secret", + binding: (ref) => [ref.orgId, ref.provider], +} + export const planetScaleWebhookSecretSlot: SecretSlot = { name: "planetscale_connections.webhook_secret", binding: orgRowBinding, diff --git a/packages/backend/src/services/cost/CloudflareCostConnectionService.test.ts b/packages/backend/src/services/cost/CloudflareCostConnectionService.test.ts new file mode 100644 index 0000000000..806358c0fe --- /dev/null +++ b/packages/backend/src/services/cost/CloudflareCostConnectionService.test.ts @@ -0,0 +1,320 @@ +import { afterEach, assert, describe, it } from "@effect/vitest" +import { OrgId, UserId } from "@maple/domain/http" +import { DateTime, Effect, Layer, Schema, Array as Arr, String as Str } from "effect" +import { TestClock } from "effect/testing" +import { cleanupTestDbs, createTestDb, queryFirstRow, type TestDb } from "@maple/backend/testing/test-pglite" +import { CloudflareCostConnectionService } from "./CloudflareCostConnectionService" +import { + FAKE_ACCOUNT, + type FakeCloudflare, + fakeCloudflareDeps, + makeFakeCloudflare, +} from "./providers/cloudflare/fake-cloudflare" + +const trackedDbs: TestDb[] = [] +afterEach(() => cleanupTestDbs(trackedDbs)) + +const ORG = Schema.decodeSync(OrgId)("org_cost") +const USER = Schema.decodeSync(UserId)("user_cost") +const TOKEN = "cfut_0123456789abcdefghijABCDEFGHIJ0123456789" +const OTHER_TOKEN = "cfat_9876543210zyxwvutsrqZYXWVUTSRQ9876543210" +const ACCOUNT_PATH = `/client/v4/accounts/${FAKE_ACCOUNT.id}` +const NOW = DateTime.toEpochMillis(DateTime.makeUnsafe("2026-11-11T06:00:00Z")) +const VALIDATION = "@maple/http/cost-connections/CostConnectionValidationError" +const PERSISTENCE = "@maple/http/cost-connections/CostConnectionPersistenceError" + +const run = ( + testDb: TestDb, + world: FakeCloudflare, + effect: Effect.Effect, +) => + Effect.andThen(TestClock.setTime(NOW), effect).pipe( + Effect.provide( + Layer.effect(CloudflareCostConnectionService, CloudflareCostConnectionService.make).pipe( + Layer.provide(fakeCloudflareDeps(testDb, world)), + ), + ), + ) + +const set = (token: string) => CloudflareCostConnectionService.use((service) => service.set(ORG, USER, token)) +const remove = CloudflareCostConnectionService.use((service) => service.remove(ORG)) + +interface Stored { + readonly id: string + readonly provider: string + readonly secret_ciphertext: string + readonly account_id: string + readonly account_label: string +} + +const storedToken = (testDb: TestDb) => + Effect.promise(() => + queryFirstRow( + testDb, + "SELECT id, provider, secret_ciphertext, account_id, account_label FROM cost_connections WHERE org_id = 'org_cost'", + ), + ) + +interface Source { + readonly id: string + readonly connection_ref: string + readonly account_id: string + readonly enabled: boolean +} + +const source = (testDb: TestDb) => + Effect.promise(() => + queryFirstRow( + testDb, + "SELECT id, connection_ref, account_id, enabled FROM cost_sources WHERE org_id = 'org_cost' AND provider = 'cloudflare'", + ), + ) + +describe("CloudflareCostConnectionService", () => { + it.effect("checks a token with Cloudflare, seals it and makes the cost source", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + const saved = yield* set(` ${TOKEN} `) + assert.deepStrictEqual(saved, { accountId: FAKE_ACCOUNT.id, accountLabel: "Example Account" }) + // Which account, then whether it has billing data, then proof the token reads it. + assert.deepStrictEqual(world.urls, [ + "/client/v4/accounts?per_page=50", + `${ACCOUNT_PATH}/billable-usage/info`, + `${ACCOUNT_PATH}/billable-usage?from=2026-10-10&to=2026-11-11`, + ]) + assert.deepStrictEqual(world.authorizations, Arr.replicate(`Bearer ${TOKEN}`, 3)) + + const stored = yield* storedToken(testDb) + assert.isFalse(Str.includes(TOKEN)(stored?.secret_ciphertext ?? TOKEN)) + assert.deepStrictEqual( + [stored?.provider, stored?.account_id, stored?.account_label], + ["cloudflare", FAKE_ACCOUNT.id, "Example Account"], + ) + const made = yield* source(testDb) + // The source is named after the token's account, which a later token keeps. + assert.deepStrictEqual( + [made?.connection_ref, made?.account_id, made?.enabled], + [stored?.id, FAKE_ACCOUNT.id, true], + ) + }), + ) + }) + + it.effect("refuses a token that reads several accounts or none, before asking for billing", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ + accounts: [FAKE_ACCOUNT, { id: "fedcba9876543210fedcba9876543210", name: "Second Account" }], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const several = yield* Effect.flip(set(TOKEN)) + assert.strictEqual(several._tag, VALIDATION) + assert.strictEqual( + several.message, + "This token can read 2 accounts. Create one limited to a single account.", + ) + world.accounts = [] + assert.include((yield* Effect.flip(set(TOKEN))).message, "can read 0 accounts") + assert.deepStrictEqual(world.urls, Arr.replicate("/client/v4/accounts?per_page=50", 2)) + assert.isUndefined(yield* storedToken(testDb)) + assert.isUndefined(yield* source(testDb)) + }), + ) + }) + + it.effect("refuses a token Cloudflare does not know, and one without billing access", () => { + const testDb = createTestDb(trackedDbs) + // Cloudflare answers 403 for an unknown token on the account listing. + const world = makeFakeCloudflare({ accountsStatus: 403 }) + return run( + testDb, + world, + Effect.gen(function* () { + const unknown = yield* Effect.flip(set(TOKEN)) + assert.deepStrictEqual( + [unknown._tag, unknown.message], + [VALIDATION, "Cloudflare rejected this token."], + ) + world.accountsStatus = 401 + assert.strictEqual( + (yield* Effect.flip(set(TOKEN))).message, + "Cloudflare rejected this token.", + ) + + world.accountsStatus = 200 + world.billingStatus = 403 + const noBilling = yield* Effect.flip(set(TOKEN)) + assert.strictEqual(noBilling._tag, VALIDATION) + assert.include(noBilling.message, "Billing: Read permission") + assert.isUndefined(yield* storedToken(testDb)) + }), + ) + }) + + it.effect("refuses an account Cloudflare has no usage-based billing data for", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ covered: false }) + return run( + testDb, + world, + Effect.gen(function* () { + const error = yield* Effect.flip(set(TOKEN)) + assert.strictEqual(error._tag, VALIDATION) + assert.include(error.message, "no usage-based billing data") + assert.isUndefined(yield* storedToken(testDb)) + assert.isUndefined(yield* source(testDb)) + }), + ) + }) + + it.effect("reports a Cloudflare outage as a retryable failure, not as a bad token", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ billingStatus: 503 }) + return run( + testDb, + world, + Effect.gen(function* () { + const error = yield* Effect.flip(set(TOKEN)) + assert.strictEqual(error._tag, PERSISTENCE) + world.billingStatus = 200 + world.usage = "maintenance" + assert.strictEqual((yield* Effect.flip(set(TOKEN)))._tag, PERSISTENCE) + assert.isUndefined(yield* storedToken(testDb)) + }), + ) + }) + + it.effect("never sends text that cannot be a token, nor any token of an org outside the rollout", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + const refusals = yield* Effect.forEach( + ["", " ", "two words", `cfut_${Str.repeat(300)("a")}`, "line\nbreak"], + (text) => Effect.map(Effect.flip(set(text)), (error) => error._tag), + ) + assert.deepStrictEqual(refusals, Arr.replicate(VALIDATION, 5)) + + world.cloudCost = false + const outside = yield* Effect.flip(set(TOKEN)) + assert.strictEqual(outside._tag, "@maple/http/errors/CloudCostDisabledError") + assert.deepStrictEqual(world.urls, []) + }), + ) + }) + + it.effect("keeps the connection and its source when the account's token is replaced", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + yield* set(TOKEN) + const first = yield* storedToken(testDb) + const firstSource = yield* source(testDb) + + // The account was renamed since: the label follows, the id is what is compared. + world.accounts = [{ id: FAKE_ACCOUNT.id, name: "Renamed Account" }] + const replaced = yield* set(OTHER_TOKEN) + assert.strictEqual(replaced.accountLabel, "Renamed Account") + const second = yield* storedToken(testDb) + assert.strictEqual(second?.id, first?.id) + assert.strictEqual(second?.account_label, "Renamed Account") + assert.notStrictEqual(second?.secret_ciphertext, first?.secret_ciphertext) + assert.deepStrictEqual(yield* source(testDb), firstSource) + }), + ) + }) + + it.effect( + "refuses a token of another Cloudflare account while a source exists, and keeps the saved one", + () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + yield* set(TOKEN) + const first = yield* storedToken(testDb) + + // The other account's charges would overwrite the days the source already holds. + world.accounts = [{ id: "fedcba9876543210fedcba9876543210", name: "Second Account" }] + const refused = yield* Effect.flip(set(OTHER_TOKEN)) + assert.strictEqual(refused._tag, VALIDATION) + assert.include(refused.message, "another Cloudflare account") + assert.deepStrictEqual(yield* storedToken(testDb), first) + }), + ) + }, + ) + + it.effect("makes a source held for its old token's failure due again once the token is replaced", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + const held = Effect.promise(() => + queryFirstRow<{ + error: string | null + reason: string | null + held: boolean + failures: number + leased: boolean + }>( + testDb, + `SELECT last_error AS error, last_error_reason AS reason, next_attempt_at IS NOT NULL AS held, + consecutive_failures AS failures, lease_until IS NOT NULL AS leased + FROM cost_sources WHERE org_id = 'org_cost'`, + ), + ) + return run( + testDb, + world, + Effect.gen(function* () { + yield* set(TOKEN) + yield* Effect.promise(() => + testDb.pglite.query( + // A pull on the old token is in flight: it holds the source's lease. + `UPDATE cost_sources SET last_error = 'Cloudflare rejected the saved token.', + last_error_reason = 'auth', next_attempt_at = now() + interval '6 hours', + consecutive_failures = 3, lease_until = now() + interval '20 minutes'`, + ), + ) + assert.strictEqual((yield* held)?.held, true) + yield* set(OTHER_TOKEN) + // The lease goes too: the old pull's failure, arriving later, finds the claim gone and writes nothing. + assert.deepStrictEqual(yield* held, { + error: null, + reason: null, + held: false, + failures: 0, + leased: false, + }) + }), + ) + }) + + it.effect("removes the token and its source together, without needing the rollout", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + yield* set(TOKEN) + world.cloudCost = false + yield* remove + assert.isUndefined(yield* storedToken(testDb)) + assert.isUndefined(yield* source(testDb)) + }), + ) + }) +}) diff --git a/packages/backend/src/services/cost/CloudflareCostConnectionService.ts b/packages/backend/src/services/cost/CloudflareCostConnectionService.ts new file mode 100644 index 0000000000..9f9e38f055 --- /dev/null +++ b/packages/backend/src/services/cost/CloudflareCostConnectionService.ts @@ -0,0 +1,186 @@ +/** + * The Cloudflare API token an org's Cloudflare charges are read with: checked with Cloudflare, + * then kept in the shared cost connection store. Saving it makes the org's Cloudflare cost source, + * replacing it keeps the source and its history, removing it removes the source. + */ +import { + CLOUDFLARE_COST_TOKEN_MAX_LENGTH, + CloudCostDisabledError, + CostConnectionPersistenceError, + CostConnectionValidationError, + type OrgId, + type UserId, +} from "@maple/domain/http" +import { Clock, Context, Duration, Effect, Layer, Redacted, String as Str } from "effect" +import { FetchHttpClient, HttpClient } from "effect/http" +import { OrganizationFeatureFlagsService } from "@maple/backend/services/org/OrganizationFeatureFlagsService" +import { CostConnectionStore, type CostConnectionStoreError } from "./CostConnectionStore" +import * as CloudflareBilling from "./providers/cloudflare/cloudflare-billing-api" +import { addDays, utcToday } from "./sync-plan" + +const VALIDATE_TIMEOUT = Duration.seconds(20) + +/** What the usage read of the check asks for: enough days back to include a billing period's start. */ +const CHECK_DAYS = 32 + +const NO_BILLING_ACCESS = + "This token cannot read the account's billing. Create a custom token with the Billing: Read permission." + +const refused = (message: string) => new CostConnectionValidationError({ message }) + +/** Which call of the check failed decides what a 403 means: the token itself, or its permission. */ +const toTokenError = + (step: "accounts" | "billing") => + ( + error: CloudflareBilling.CloudflareBillingError, + ): CostConnectionValidationError | CostConnectionPersistenceError => { + // Listing accounts answers 403 for a token Cloudflare does not know, where billing answers 401. + if (error.kind === "unauthorized" || (error.kind === "forbidden" && step === "accounts")) { + return refused("Cloudflare rejected this token.") + } + if (error.kind === "forbidden") return refused(NO_BILLING_ACCESS) + return new CostConnectionPersistenceError({ + message: "Could not check this token with Cloudflare. Try again.", + }) + } + +const toPersistenceError = (error: CostConnectionStoreError) => + new CostConnectionPersistenceError({ message: error.message }) + +/** Printable and unbroken, as every Cloudflare token is: nothing else is ever sent as one. */ +const couldBeToken = (token: string): boolean => + token.length <= CLOUDFLARE_COST_TOKEN_MAX_LENGTH && /^[\x21-\x7e]+$/.test(token) + +export interface CloudflareCostConnectionServiceApi { + /** Saves the token and answers the Cloudflare account it reads. */ + readonly set: ( + orgId: OrgId, + userId: UserId, + apiToken: string, + ) => Effect.Effect< + { readonly accountId: string; readonly accountLabel: string }, + CostConnectionPersistenceError | CostConnectionValidationError | CloudCostDisabledError + > + readonly remove: (orgId: OrgId) => Effect.Effect +} + +export class CloudflareCostConnectionService extends Context.Service< + CloudflareCostConnectionService, + CloudflareCostConnectionServiceApi +>()("@maple/backend/services/cost/CloudflareCostConnectionService", { + make: Effect.gen(function* () { + const connections = yield* CostConnectionStore + const featureFlags = yield* OrganizationFeatureFlagsService + const httpClient = yield* HttpClient.HttpClient + + const asked = ( + step: "accounts" | "billing", + call: Effect.Effect, + ) => + call.pipe( + // The page shows the mapped message; which failure it was stays on the span. + Effect.tapError((error) => + Effect.annotateCurrentSpan({ + "maple.cost.cloudflare.error": error.kind, + "maple.cost.cloudflare.status": error.status ?? 0, + }), + ), + Effect.mapError(toTokenError(step)), + ) + + /** + * The one account the token reads, once it has proved it can read that account's billing: + * a token that cannot would save and then fail every sync. + */ + const validateWithCloudflare = Effect.fn("CloudflareCostConnectionService.validateWithCloudflare")( + function* (token: Redacted.Redacted) { + const { accounts, total } = yield* asked( + "accounts", + CloudflareBilling.listAccounts(httpClient, token), + ) + const [account] = accounts + // A token for several accounts would make one source of whichever is listed first. + if (account === undefined || total !== 1) { + return yield* refused( + `This token can read ${total} accounts. Create one limited to a single account.`, + ) + } + const info = yield* asked( + "billing", + CloudflareBilling.getBillableUsageInfo(httpClient, token, account.id), + ) + if (!info.covered) { + return yield* refused( + "Cloudflare has no usage-based billing data for this account. Accounts on an enterprise contract are not supported yet.", + ) + } + const now = yield* Clock.currentTimeMillis + const today = utcToday(now) + yield* asked( + "billing", + CloudflareBilling.getBillableUsage(httpClient, token, account.id, { + from: addDays(today, -CHECK_DAYS), + to: today, + }), + ) + return { accountId: account.id, accountLabel: account.name } + }, + ) + + const set = Effect.fn("CloudflareCostConnectionService.set")(function* ( + orgId: OrgId, + userId: UserId, + rawToken: string, + ) { + yield* Effect.annotateCurrentSpan({ orgId }) + const flags = yield* featureFlags.flags(orgId) + if (!flags.cloudCost) { + return yield* new CloudCostDisabledError({ + message: "Cloud cost is not enabled for this organization.", + }) + } + const apiToken = Str.trim(rawToken) + if (!couldBeToken(apiToken)) { + return yield* refused("That is not a Cloudflare API token.") + } + const secret = Redacted.make(apiToken) + const account = yield* validateWithCloudflare(secret).pipe( + Effect.timeoutOrElse({ + duration: VALIDATE_TIMEOUT, + orElse: () => + Effect.fail( + new CostConnectionPersistenceError({ + message: "Could not reach Cloudflare to check this token. Try again.", + }), + ), + }), + ) + const saved = yield* connections + .save({ orgId, userId, provider: "cloudflare", secret, ...account }) + .pipe(Effect.mapError(toPersistenceError)) + if (saved === "other_account") { + return yield* refused( + "This token reads another Cloudflare account than the source does. Remove the source first to switch accounts.", + ) + } + return account + }) + + const remove = Effect.fn("CloudflareCostConnectionService.remove")(function* (orgId: OrgId) { + yield* Effect.annotateCurrentSpan({ orgId }) + yield* connections.remove(orgId, "cloudflare").pipe(Effect.mapError(toPersistenceError)) + }) + + return { set, remove } satisfies CloudflareCostConnectionServiceApi + }), +}) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide( + Layer.mergeAll( + FetchHttpClient.layer, + OrganizationFeatureFlagsService.layer, + CostConnectionStore.layer, + ), + ), + ) +} diff --git a/packages/backend/src/services/cost/CostConnectionStore.ts b/packages/backend/src/services/cost/CostConnectionStore.ts new file mode 100644 index 0000000000..efb3930485 --- /dev/null +++ b/packages/backend/src/services/cost/CostConnectionStore.ts @@ -0,0 +1,212 @@ +/** + * Where a pasted cost credential lives: sealed in `cost_connections`, one per org and provider. + * Saving one makes the org's cost source for that provider, replacing it keeps the source and its + * history, removing it removes the source. Each provider checks its credential with its vendor + * first, then stores it here, and its adapter reads it back for a pull. + */ +import * as PG from "@maple-dev/effect-orm/postgres" +import { CostConnections, CostSources } from "@maple/db/tables" +import type { OrgId, UserId } from "@maple/primitives" +import { Clock, Context, Effect, Layer, Option, Redacted, Schema } from "effect" +import { randomUUID } from "node:crypto" +import { Database } from "@maple/backend/platform/DatabaseLive" +import { makeDbExecute } from "@maple/backend/platform/db-execute" +import { Env } from "@maple/backend/platform/Env" +import { openSecret, sealSecret, secretKeyringFromEnv } from "@maple/backend/platform/SecretBox" +import { costConnectionSecretSlot } from "@maple/backend/platform/secret-slots" +import type { CostPullSource } from "./CostProvider" +import { type CostConnectionProvider, reconcileCostConnectionSources } from "./cost-connection-sources" + +/** `secret`: the deployment's key could not seal or open the value. `database`: Postgres failed. */ +export class CostConnectionStoreError extends Schema.TaggedError()( + "@maple/backend/cost/CostConnectionStoreError", + { + message: Schema.String, + reason: Schema.Literals(["database", "secret"]), + }, +) {} + +const secretFailed = (message: string) => new CostConnectionStoreError({ message, reason: "secret" }) + +export interface CostConnectionInput { + readonly orgId: OrgId + readonly userId: UserId + readonly provider: CostConnectionProvider + readonly secret: Redacted.Redacted + /** The vendor's account the credential reads, as the vendor answered when it was checked. */ + readonly accountId: string + readonly accountLabel: string +} + +export interface CostConnectionCredential { + readonly secret: Redacted.Redacted + readonly accountId: string + readonly accountLabel: string +} + +export interface CostConnectionStoreApi { + /** + * Seals and saves the credential, and makes the provider's cost source. `other_account`: the + * org's source reads another account, whose days this one's spend would overwrite, so nothing + * is written. + */ + readonly save: ( + input: CostConnectionInput, + ) => Effect.Effect<"saved" | "other_account", CostConnectionStoreError> + /** Deletes the credential and its cost source. */ + readonly remove: ( + orgId: OrgId, + provider: CostConnectionProvider, + ) => Effect.Effect + /** The credential a source's pull reads with; none when the connection is gone or another org's. */ + readonly load: ( + provider: CostConnectionProvider, + source: CostPullSource, + ) => Effect.Effect, CostConnectionStoreError> +} + +export class CostConnectionStore extends Context.Service()( + "@maple/backend/services/cost/CostConnectionStore", + { + make: Effect.gen(function* () { + const database = yield* Database + const env = yield* Env + // Read at first use, not here: an unusable key must not stop the sync of other providers. + const keyring = yield* Effect.cached(secretKeyringFromEnv(env, secretFailed)) + const dbExecute = makeDbExecute( + database, + "CostConnectionStore", + (error) => new CostConnectionStoreError({ message: error.message, reason: "database" }), + ) + + const save = Effect.fn("CostConnectionStore.save")(function* (input: CostConnectionInput) { + const { orgId, provider, accountId } = input + yield* Effect.annotateCurrentSpan({ orgId, "maple.cost.provider": provider }) + const secrets = yield* keyring + // Bound to the org and the provider: a row copied onto another of either no longer opens. + const sealed = yield* sealSecret( + secrets, + costConnectionSecretSlot, + { orgId, provider }, + Redacted.value(input.secret), + secretFailed, + ) + const now = yield* Clock.currentTimeMillis + const next = { + secretCiphertext: sealed.ciphertext, + secretIv: sealed.iv, + secretTag: sealed.tag, + accountLabel: input.accountLabel, + updatedAt: now, + updatedBy: input.userId, + } + const saved = yield* dbExecute((db) => + db.transaction( + Effect.gen(function* () { + // A replaced credential keeps the row's id, which the cost source points at. + // Another account's credential matches no row. + const [row] = yield* db.run( + PG.insertInto(CostConnections) + .values({ + id: randomUUID(), + orgId, + provider, + accountId, + createdAt: now, + ...next, + }) + .onConflictDoUpdate({ + target: ["orgId", "provider"], + set: next, + where: ($) => $.accountId.eq(accountId), + }) + .returning(($) => ({ id: $.id })), + ) + if (row === undefined) return false + // The source shows on the Cost page at once, not at the next sync tick. + yield* reconcileCostConnectionSources(db, now, provider, orgId) + // A source held for its old credential's failure is due again with the new one. A + // pull still in flight on the old one loses its claim, so its failure writes no hold. + yield* db.run( + PG.update(CostSources) + .set({ + leaseUntil: null, + lastError: null, + lastErrorReason: null, + nextAttemptAt: null, + consecutiveFailures: 0, + updatedAt: now, + }) + .where(($) => [ + $.orgId.eq(orgId), + $.provider.eq(provider), + $.connectionRef.eq(row.id), + ]), + ) + return true + }), + ), + ) + return saved ? "saved" : "other_account" + }) + + const remove = Effect.fn("CostConnectionStore.remove")(function* ( + orgId: OrgId, + provider: CostConnectionProvider, + ) { + yield* Effect.annotateCurrentSpan({ orgId, "maple.cost.provider": provider }) + const now = yield* Clock.currentTimeMillis + yield* dbExecute((db) => + db.transaction( + Effect.gen(function* () { + yield* db.run( + PG.deleteFrom(CostConnections).where(($) => [ + $.orgId.eq(orgId), + $.provider.eq(provider), + ]), + ) + yield* reconcileCostConnectionSources(db, now, provider, orgId) + }), + ), + ) + }) + + const load = Effect.fn("CostConnectionStore.load")(function* ( + provider: CostConnectionProvider, + source: CostPullSource, + ) { + // By org and provider as well as id: a source never reads another org's credential. + const [row] = yield* dbExecute((db) => + db.run( + PG.from(CostConnections) + .select() + .where(($) => [ + $.orgId.eq(source.orgId), + $.provider.eq(provider), + $.id.eq(source.connectionRef), + ]) + .limit(1), + ), + ) + if (row === undefined) return Option.none() + const secrets = yield* keyring + const secret = yield* openSecret( + secrets, + costConnectionSecretSlot, + { orgId: row.orgId, provider: row.provider }, + { ciphertext: row.secretCiphertext, iv: row.secretIv, tag: row.secretTag }, + secretFailed, + ) + return Option.some({ + secret: Redacted.make(secret), + accountId: row.accountId, + accountLabel: row.accountLabel, + }) + }) + + return { save, remove, load } satisfies CostConnectionStoreApi + }), + }, +) { + static readonly layer = Layer.effect(this, this.make) +} diff --git a/packages/backend/src/services/cost/CostSyncService.test.ts b/packages/backend/src/services/cost/CostSyncService.test.ts index ddfe364642..96bef50ff4 100644 --- a/packages/backend/src/services/cost/CostSyncService.test.ts +++ b/packages/backend/src/services/cost/CostSyncService.test.ts @@ -28,6 +28,7 @@ import { WarehouseQueryService } from "@maple/backend/services/warehouse/Warehou import { type CostProviderAdapter, CostProviderError, type CostPullRow } from "./CostProvider" import { CostSyncService } from "./CostSyncService" import { AwsCostProvider } from "./providers/aws/AwsCostProvider" +import { CloudflareCostProvider } from "./providers/cloudflare/CloudflareCostProvider" import { OpenRouterCostProvider } from "./providers/openrouter/OpenRouterCostProvider" import { BACKFILL_REQUEST_BUDGET, daysOfWindow, MONTHLY_REQUEST_BUDGET } from "./sync-plan" @@ -43,6 +44,7 @@ const CONNECTION_A = "0190f2b3-0000-7000-8000-0000000000a1" const CONNECTION_B = "0190f2b3-0000-7000-8000-0000000000b1" const CONNECTION_C = "0190f2b3-0000-7000-8000-0000000000c1" const OPENROUTER_KEY_A = "0190f2b3-0000-7000-8000-0000000000d1" +const CLOUDFLARE_TOKEN_A = "0190f2b3-0000-7000-8000-0000000000e1" /** What every fake pull reports as billable. */ const REQUESTS_PER_PULL = 2 @@ -240,6 +242,7 @@ const harness = (options: FakeProviderOptions = {}) => { Layer.mergeAll( Layer.succeed(AwsCostProvider, adapterOf("aws")), Layer.succeed(OpenRouterCostProvider, adapterOf("openrouter")), + Layer.succeed(CloudflareCostProvider, adapterOf("cloudflare")), warehouse, featureFlags, ), @@ -282,6 +285,18 @@ const harness = (options: FakeProviderOptions = {}) => { VALUES ($1, $2, 'sealed', 'iv', 'tag', 'sk-or-v1-abc...123', 'user_account_a', now(), now(), 'user_a')`, [key.id, key.orgId ?? "org_a"], ) + /** An org's pasted credential for a provider that keeps it in `cost_connections`. */ + const saveCostConnection = (connection: { + readonly id: string + readonly provider: string + readonly orgId?: string + }) => + sql( + `INSERT INTO cost_connections + (id, org_id, provider, secret_ciphertext, secret_iv, secret_tag, account_id, account_label, created_at, updated_at, updated_by) + VALUES ($1, $2, $3, 'sealed', 'iv', 'tag', 'account_a', 'Example Account', now(), now(), 'user_a')`, + [connection.id, connection.orgId ?? "org_a", connection.provider], + ) const sourceRefs = Effect.promise(() => testDb.pglite.query<{ org: string; ref: string; account: string }>( "SELECT org_id AS org, connection_ref AS ref, account_id AS account FROM cost_sources ORDER BY org_id, connection_ref", @@ -322,6 +337,7 @@ const harness = (options: FakeProviderOptions = {}) => { sql, connect, saveOpenRouterKey, + saveCostConnection, sourceRefs, sourceState, sourceStateOf, @@ -430,6 +446,84 @@ describe("CostSyncService OpenRouter sources", () => { }) }) +describe("CostSyncService sources of a pasted credential", () => { + it.effect( + "makes a Cloudflare source for a saved token and pulls up to yesterday, then walks a year back", + () => { + const h = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(NOW) + yield* h.saveCostConnection({ id: CLOUDFLARE_TOKEN_A, provider: "cloudflare" }) + const summary = yield* poll + assert.deepStrictEqual(yield* h.sourceRefs, [["org_a", CLOUDFLARE_TOKEN_A, "account_a"]]) + assert.deepStrictEqual([summary.due, summary.synced], [1, 1]) + // Last month and this one through yesterday: Cloudflare has no rows for today. + assert.deepStrictEqual(h.windows()[0], ["2026-10-01", "2026-11-10"]) + assert.isTrue(Arr.every(h.writtenRows(), (row) => row.Provider === "cloudflare")) + + // History then walks down a month per step and stops at the eleventh month back. + yield* Effect.forEach(Arr.range(1, 12), () => + Effect.andThen(TestClock.adjust("5 minutes"), poll), + ) + const state = yield* h.sourceStateOf("org_a", CLOUDFLARE_TOKEN_A) + assert.deepStrictEqual( + [state?.watermark, state?.backfill, state?.error], + ["2026-11-10", "2025-12-01", null], + ) + assert.deepStrictEqual(Arr.last(h.windows()), Option.some(["2025-12-01", "2025-12-31"])) + assert.isTrue(Arr.every(h.windows(), ([startDay]) => startDay >= "2025-12-01")) + }).pipe(Effect.provide(h.layer)) + }, + ) + + it.effect("removes the source with its token, and leaves the org's other sources alone", () => { + const h = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(NOW) + yield* h.saveCostConnection({ id: CLOUDFLARE_TOKEN_A, provider: "cloudflare" }) + yield* h.saveOpenRouterKey({ id: OPENROUTER_KEY_A }) + yield* h.connect({ id: CONNECTION_A }) + yield* poll + assert.deepStrictEqual(yield* h.sourceRefs, [ + ["org_a", CONNECTION_A, "123456789012"], + ["org_a", OPENROUTER_KEY_A, "user_account_a"], + ["org_a", CLOUDFLARE_TOKEN_A, "account_a"], + ]) + + yield* h.sql("DELETE FROM cost_connections") + yield* TestClock.adjust("1 hour") + yield* poll + assert.deepStrictEqual(yield* h.sourceRefs, [ + ["org_a", CONNECTION_A, "123456789012"], + ["org_a", OPENROUTER_KEY_A, "user_account_a"], + ]) + }).pipe(Effect.provide(h.layer)) + }) + + it.effect( + "makes one source per org, and none for a credential of a provider it does not reconcile", + () => { + const h = harness() + return Effect.gen(function* () { + yield* TestClock.setTime(NOW) + yield* h.saveCostConnection({ id: CLOUDFLARE_TOKEN_A, provider: "cloudflare" }) + yield* h.saveCostConnection({ + id: "0190f2b3-0000-7000-8000-0000000000e2", + provider: "cloudflare", + orgId: "org_b", + }) + // A row for a provider that keeps its credential elsewhere is not this table's to reconcile. + yield* h.saveCostConnection({ id: "0190f2b3-0000-7000-8000-0000000000e3", provider: "aws" }) + yield* poll + assert.deepStrictEqual(yield* h.sourceRefs, [ + ["org_a", CLOUDFLARE_TOKEN_A, "account_a"], + ["org_b", "0190f2b3-0000-7000-8000-0000000000e2", "account_a"], + ]) + }).pipe(Effect.provide(h.layer)) + }, + ) +}) + describe("CostSyncService sources", () => { it.effect("makes a source only for a verified connection that asks for cost and is granted it", () => { const h = harness() diff --git a/packages/backend/src/services/cost/CostSyncService.ts b/packages/backend/src/services/cost/CostSyncService.ts index c978694bca..81edfd7eed 100644 --- a/packages/backend/src/services/cost/CostSyncService.ts +++ b/packages/backend/src/services/cost/CostSyncService.ts @@ -18,6 +18,7 @@ import { Schedule, Schema, Array as Arr, + Number as Num, String as Str, } from "effect" import { Database } from "@maple/backend/platform/DatabaseLive" @@ -29,8 +30,10 @@ import { leaseClaimable, leaseStillOurs } from "../integrations/lease" import { pollFleet } from "../integrations/poll-fleet" import type { CostProviderAdapter, CostProviderError } from "./CostProvider" import { reconcileAwsCostSources } from "./aws-cost-sources" +import { COST_CONNECTION_PROVIDERS, reconcileCostConnectionSources } from "./cost-connection-sources" import { reconcileOpenRouterCostSources } from "./openrouter-cost-sources" import { AwsCostProvider } from "./providers/aws/AwsCostProvider" +import { CloudflareCostProvider } from "./providers/cloudflare/CloudflareCostProvider" import { OpenRouterCostProvider } from "./providers/openrouter/OpenRouterCostProvider" import { advanceCostSync, @@ -165,6 +168,7 @@ export class CostSyncService extends Context.Service const dbExecute = makeDbExecute( database, @@ -204,9 +208,26 @@ export class CostSyncService extends Context.Service + Effect.forEach(COST_CONNECTION_PROVIDERS, (provider) => + reconcileCostConnectionSources(db, now, provider), + ), + ) + yield* Effect.annotateCurrentSpan({ + "maple.cost.sources_created": Num.sumAll(Arr.map(reconciled, (entry) => entry.created)), + "maple.cost.sources_removed": Num.sumAll(Arr.map(reconciled, (entry) => entry.removed)), + }) + }) + + /** One function per connection table; each owns the sources of its own. */ const reconcile = (now: number) => - Effect.all([reconcileAws(now), reconcileOpenRouter(now)], { discard: true }) + Effect.all([reconcileAws(now), reconcileOpenRouter(now), reconcileConnections(now)], { + discard: true, + }) /** Enabled sources with a pull due and no backoff, healthy ones first. */ const loadDue = (now: number) => { @@ -668,6 +689,7 @@ export class CostSyncService extends Context.Service +export type CostConnectionProvider = (typeof COST_CONNECTION_PROVIDERS)[number] + +/** Sources one run creates and removes, each. */ +const RECONCILE_LIMIT = 200 + +/** Every org's sources of `provider`, or only `orgId`'s. Joined by org, so a ref never crosses orgs. */ +export const reconcileCostConnectionSources = Effect.fnUntraced(function* ( + db: MapleDb, + now: number, + provider: CostConnectionProvider, + orgId?: OrgId, +) { + const missing = yield* db.run( + PG.from(CostConnections, "c") + .leftJoin(CostSources, "s", (connection, source) => + source.orgId + .eq(connection.orgId) + .and(source.provider.eq(provider)) + .and(source.connectionRef.eq(connection.id)), + ) + .select(($) => ({ id: $.id, orgId: $.orgId, accountId: $.accountId })) + .where(($) => [ + $.provider.eq(provider), + $.s.id.isNull(), + ...(orgId === undefined ? [] : [$.orgId.eq(orgId)]), + ]) + .orderBy(["id", "asc"]) + .limit(RECONCILE_LIMIT), + ) + const created = yield* Effect.forEach(missing, (connection) => + db.run( + PG.insertInto(CostSources) + .values({ + id: CostSourceId.make(randomUUID()), + orgId: connection.orgId, + provider, + connectionRef: connection.id, + accountId: connection.accountId, + createdAt: now, + updatedAt: now, + }) + // Another run made it first. + .onConflictDoNothing() + .returning(($) => ({ id: $.id })), + ), + ) + + const orphaned = yield* db.run( + PG.from(CostSources, "s") + .leftJoin(CostConnections, "c", (source, connection) => + source.orgId + .eq(connection.orgId) + .and(source.connectionRef.eq(connection.id)) + .and(connection.provider.eq(provider)), + ) + .select(($) => ({ id: $.id })) + .where(($) => [ + $.provider.eq(provider), + $.c.id.isNull(), + ...(orgId === undefined ? [] : [$.orgId.eq(orgId)]), + ]) + .orderBy(["id", "asc"]) + .limit(RECONCILE_LIMIT), + ) + const removed = Arr.isReadonlyArrayNonEmpty(orphaned) + ? yield* db.run( + PG.deleteFrom(CostSources) + .where(($) => [ + $.provider.eq(provider), + PG.inList( + $.id, + Arr.map(orphaned, (source) => source.id), + ), + ]) + .returning(($) => ({ id: $.id })), + ) + : [] + + return { created: Arr.flatten(created).length, removed: removed.length } +}) diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts new file mode 100644 index 0000000000..d2b370e6d0 --- /dev/null +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts @@ -0,0 +1,310 @@ +import { afterEach, assert, describe, it } from "@effect/vitest" +import { OrgId, UserId } from "@maple/domain/http" +import { DateTime, Effect, Layer, Schema, Array as Arr } from "effect" +import { TestClock } from "effect/testing" +import { cleanupTestDbs, createTestDb, queryFirstRow, type TestDb } from "@maple/backend/testing/test-pglite" +import { CloudflareCostConnectionService } from "../../CloudflareCostConnectionService" +import type { CostProviderError } from "../../CostProvider" +import { CloudflareCostProvider, piecesOf } from "./CloudflareCostProvider" +import { + FAKE_ACCOUNT, + type FakeCloudflare, + fakeCloudflareDeps, + makeFakeCloudflare, + usageRow, +} from "./fake-cloudflare" + +const trackedDbs: TestDb[] = [] +afterEach(() => cleanupTestDbs(trackedDbs)) + +const ORG = Schema.decodeSync(OrgId)("org_cost") +const OTHER_ORG = Schema.decodeSync(OrgId)("org_other") +const USER = Schema.decodeSync(UserId)("user_cost") +const TOKEN = "cfut_0123456789abcdefghijABCDEFGHIJ0123456789" +const ACCOUNT_PATH = `/client/v4/accounts/${FAKE_ACCOUNT.id}` +const WINDOW = { startDay: "2026-10-20", endDay: "2026-11-10" } +/** The pull runs the morning after the window's last day. */ +const NOW = DateTime.toEpochMillis(DateTime.makeUnsafe("2026-11-11T06:00:00Z")) + +const run = ( + testDb: TestDb, + world: FakeCloudflare, + effect: Effect.Effect, +) => + effect.pipe( + Effect.provide( + Layer.mergeAll( + Layer.effect(CloudflareCostProvider, CloudflareCostProvider.make), + Layer.effect(CloudflareCostConnectionService, CloudflareCostConnectionService.make), + ).pipe(Layer.provide(fakeCloudflareDeps(testDb, world))), + ), + ) + +/** Saves `TOKEN` for `ORG` and answers the connection's id, with the fake's call log emptied. */ +const saveToken = (testDb: TestDb, world: FakeCloudflare) => + Effect.gen(function* () { + yield* TestClock.setTime(NOW) + yield* CloudflareCostConnectionService.use((service) => service.set(ORG, USER, TOKEN)) + const row = yield* Effect.promise(() => + queryFirstRow<{ id: string }>(testDb, "SELECT id FROM cost_connections"), + ) + world.urls.length = 0 + world.authorizations.length = 0 + return row?.id ?? "" + }) + +const pull = (orgId: OrgId, connectionRef: string, window = WINDOW) => + CloudflareCostProvider.use((provider) => provider.pull({ orgId, connectionRef }, window)) + +const reasonOf = (error: CostProviderError) => [error.reason, error.requests, error.provider] + +describe("CloudflareCostProvider", () => { + it.effect( + "reads usage from before the window's billing period started, and keeps the window's days", + () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ + usage: [ + // October's period started on the 1st: a range starting on the 20th would miss it. + usageRow("2026-10-19", "R2", "R2 Storage", 9), + usageRow("2026-10-20", "R2", "R2 Storage", 1.5), + usageRow("2026-10-31", "Workers", "Workers Requests", 0), + usageRow("2026-11-09", "Workers", "Workers Requests", 2), + // The window's last day is only served when the range ends the day after. + usageRow("2026-11-10", "D1", "D1 Rows Read", 0.25), + ], + subscriptions: [ + { + price: 5, + currency: "USD", + frequency: "monthly", + current_period_start: "2026-11-01T00:00:00Z", + created_date: "2025-01-01T00:00:00Z", + rate_plan: { public_name: "Workers Paid" }, + }, + ], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const pulled = yield* pull(ORG, connectionRef) + assert.deepStrictEqual(world.urls, [ + `${ACCOUNT_PATH}/billable-usage/info`, + `${ACCOUNT_PATH}/billable-usage?from=2026-09-19&to=2026-11-11`, + `${ACCOUNT_PATH}/subscriptions?per_page=50&page=1`, + ]) + assert.deepStrictEqual(world.authorizations, Arr.replicate(`Bearer ${TOKEN}`, 3)) + // Cloudflare bills none of the requests. + assert.strictEqual(pulled.requests, 0) + assert.deepStrictEqual( + Arr.map(pulled.rows, (row) => [ + row.day, + row.cloudService, + row.chargeCategory, + row.billedCost, + row.accountId, + row.estimated, + ]), + [ + ["2026-10-20", "R2", "usage", 1.5, FAKE_ACCOUNT.id, false], + ["2026-11-09", "Workers", "usage", 2, FAKE_ACCOUNT.id, true], + ["2026-11-10", "D1", "usage", 0.25, FAKE_ACCOUNT.id, true], + ["2026-11-01", "Subscriptions", "purchase", 5, FAKE_ACCOUNT.id, false], + ], + ) + }), + ) + }, + ) + + it.effect("reads a billing period that starts mid-month, whose days span two months", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ + anchorDay: 21, + usage: [ + // Both days belong to the period that started on 21 September. + usageRow("2026-10-20", "R2", "R2 Storage", 1), + usageRow("2026-10-21", "R2", "R2 Storage", 2), + ], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const pulled = yield* pull(ORG, connectionRef) + assert.deepStrictEqual( + Arr.map(pulled.rows, (row) => [row.day, row.billedCost]), + [ + ["2026-10-20", 1], + ["2026-10-21", 2], + ], + ) + }), + ) + }) + + it.effect("reads a long window in pieces under the range cap, each day once", () => { + const testDb = createTestDb(trackedDbs) + const window = { startDay: "2025-12-01", endDay: "2026-03-31" } + const world = makeFakeCloudflare({ + usage: [ + usageRow("2025-12-01", "R2", "R2 Storage", 1), + // The last day of the first piece and the first of the second. + usageRow("2026-01-24", "R2", "R2 Storage", 2), + usageRow("2026-01-25", "R2", "R2 Storage", 3), + usageRow("2026-03-31", "R2", "R2 Storage", 4), + ], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const pulled = yield* pull(ORG, connectionRef, window) + assert.deepStrictEqual(piecesOf(window), [ + { startDay: "2025-12-01", endDay: "2026-01-24" }, + { startDay: "2026-01-25", endDay: "2026-03-20" }, + { startDay: "2026-03-21", endDay: "2026-03-31" }, + ]) + // The fake refuses a range over 90 days, as Cloudflare does. + assert.deepStrictEqual(world.urls.slice(1, 4), [ + `${ACCOUNT_PATH}/billable-usage?from=2025-10-31&to=2026-01-25`, + `${ACCOUNT_PATH}/billable-usage?from=2025-12-25&to=2026-03-21`, + `${ACCOUNT_PATH}/billable-usage?from=2026-02-18&to=2026-04-01`, + ]) + assert.deepStrictEqual( + Arr.map(pulled.rows, (row) => [row.day, row.billedCost]), + [ + ["2025-12-01", 1], + ["2026-01-24", 2], + ["2026-01-25", 3], + ["2026-03-31", 4], + ], + ) + }), + ) + }) + + it.effect("reads every page of the subscriptions", () => { + const testDb = createTestDb(trackedDbs) + const fee = (name: string) => ({ + price: 5, + frequency: "monthly", + current_period_start: "2026-11-01T00:00:00Z", + rate_plan: { public_name: name }, + }) + const world = makeFakeCloudflare({ + subscriptionsPageSize: 2, + subscriptions: [fee("Plan A"), fee("Plan B"), fee("Plan C")], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const pulled = yield* pull(ORG, connectionRef) + assert.deepStrictEqual( + Arr.map(pulled.rows, (row) => row.usageType), + ["Plan A", "Plan B", "Plan C"], + ) + }), + ) + }) + + it.effect("says what the sync should do about each way Cloudflare refuses", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const reasons = yield* Effect.forEach([401, 403, 400, 429, 502], (status) => + Effect.gen(function* () { + world.billingStatus = status + return (yield* Effect.flip(pull(ORG, connectionRef))).reason + }), + ) + assert.deepStrictEqual(reasons, ["auth", "not_ready", "upstream", "rate_limited", "upstream"]) + }), + ) + }) + + it.effect("holds an account Cloudflare has no usage-based billing for, before reading any usage", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + world.covered = false + const error = yield* Effect.flip(pull(ORG, connectionRef)) + assert.deepStrictEqual(reasonOf(error), ["not_ready", 0, "cloudflare"]) + assert.deepStrictEqual(world.urls, [`${ACCOUNT_PATH}/billable-usage/info`]) + }), + ) + }) + + it.effect("fails an answer it cannot read as invalid, with nothing to store", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + world.usage = "maintenance" + assert.deepStrictEqual(reasonOf(yield* Effect.flip(pull(ORG, connectionRef))), [ + "invalid_response", + 0, + "cloudflare", + ]) + // A row without its cost is not guessed at. + world.usage = + '{"result":[{"ServiceName":"R2 Storage","ChargePeriodStart":"2026-11-01T00:00:00Z"}]}' + assert.strictEqual((yield* Effect.flip(pull(ORG, connectionRef))).reason, "invalid_response") + }), + ) + }) + + it.effect("is built on a deployment whose secret key is unusable, and fails only its own pulls", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return Effect.gen(function* () { + yield* Effect.promise(() => + testDb.pglite.query( + `INSERT INTO cost_connections + (id, org_id, provider, secret_ciphertext, secret_iv, secret_tag, account_id, account_label, created_at, updated_at, updated_by) + VALUES ('conn_a', 'org_cost', 'cloudflare', 'sealed', 'iv', 'tag', 'account_a', 'Example Account', now(), now(), 'user_cost')`, + ), + ) + const error = yield* Effect.flip(pull(ORG, "conn_a")) + assert.deepStrictEqual([error.reason, world.urls], ["auth", []]) + }).pipe( + Effect.provide( + Layer.effect(CloudflareCostProvider, CloudflareCostProvider.make).pipe( + Layer.provide(fakeCloudflareDeps(testDb, world, "not-a-32-byte-key")), + ), + ), + ) + }) + + it.effect("never reads a token through another org's source, or a connection that is gone", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + assert.strictEqual((yield* Effect.flip(pull(OTHER_ORG, connectionRef))).reason, "auth") + assert.strictEqual((yield* Effect.flip(pull(ORG, "no-such-connection"))).reason, "auth") + assert.deepStrictEqual(world.urls, []) + }), + ) + }) +}) diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts new file mode 100644 index 0000000000..f1abb32111 --- /dev/null +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts @@ -0,0 +1,152 @@ +/** + * Cloudflare as a cost provider: an account's daily usage charges by product and priced metric, + * plus its subscriptions' fees, read with the API token the org saved. + */ +import { Clock, Context, Effect, Layer, Option, Array as Arr } from "effect" +import { FetchHttpClient, HttpClient } from "effect/http" +import { + type CostProviderAdapter, + CostProviderError, + type CostPull, + type CostPullSource, + type CostPullWindow, +} from "../../CostProvider" +import { CostConnectionStore, type CostConnectionStoreError } from "../../CostConnectionStore" +import { addDays, utcToday } from "../../sync-plan" +import * as CloudflareBilling from "./cloudflare-billing-api" +import { toSubscriptionRows, toUsageRows } from "./normalize" + +/** + * A billing period's rows come back only when the period starts inside the asked range, and a + * period is 31 days at most: asking from this many days before a day always includes its period. + */ +export const PERIOD_LEAD_DAYS = 31 +/** Days of a window one request reads: with the lead and the exclusive end, under the API's 90 day cap. */ +export const PIECE_DAYS = 55 +/** The newest days Cloudflare may still be counting. */ +const OPEN_DAYS = 2 + +type Reason = CostProviderError["reason"] + +// Cloudflare does not bill for API requests, so no failure counts any. +const failed = (reason: Reason, message: string) => + new CostProviderError({ message, provider: "cloudflare", reason, requests: 0 }) + +/** Cloudflare's failures, as what the sync does next and the Cost page shows. */ +const toCostProviderError = (error: CloudflareBilling.CloudflareBillingError): CostProviderError => { + switch (error.kind) { + case "unauthorized": + return failed( + "auth", + "Cloudflare rejected the saved token. Replace the token to resume the sync.", + ) + // The token passed this check when it was saved: its billing access was taken away since. + case "forbidden": + return failed( + "not_ready", + "Cloudflare no longer lets the saved token read the account's billing. Replace it with a token that has the Billing: Read permission.", + ) + case "invalid_response": + return failed("invalid_response", "Cloudflare's billing answer could not be read.") + case "rate_limited": + return failed("rate_limited", "Cloudflare is rate limiting billing requests.") + // A 400 is a request Maple wrote wrong, which a retry does not fix but a deploy does. + default: + return failed("upstream", "Cloudflare did not answer the billing request.") + } +} + +const toStoreFailure = (error: CostConnectionStoreError): CostProviderError => + error.reason === "secret" + ? failed("auth", error.message) + : failed("upstream", "Cloudflare connection database error") + +/** The window as consecutive runs of at most `PIECE_DAYS` days, oldest first. */ +export const piecesOf = (window: CostPullWindow): ReadonlyArray => + Arr.unfold(window.startDay, (startDay): Option.Option => { + if (startDay > window.endDay) return Option.none() + const last = addDays(startDay, PIECE_DAYS - 1) + const endDay = last < window.endDay ? last : window.endDay + return Option.some([{ startDay, endDay }, addDays(endDay, 1)]) + }) + +export class CloudflareCostProvider extends Context.Service()( + "@maple/backend/services/cost/CloudflareCostProvider", + { + make: Effect.gen(function* () { + const connections = yield* CostConnectionStore + const httpClient = yield* HttpClient.HttpClient + + const pull = Effect.fn("CloudflareCostProvider.pull")( + function* (source: CostPullSource, window: CostPullWindow) { + yield* Effect.annotateCurrentSpan({ + orgId: source.orgId, + "maple.cost.provider": "cloudflare", + "maple.cost.start_day": window.startDay, + "maple.cost.end_day": window.endDay, + }) + const credential = yield* connections + .load("cloudflare", source) + .pipe(Effect.mapError(toStoreFailure)) + if (Option.isNone(credential)) { + return yield* failed("auth", "No Cloudflare token is saved for this source.") + } + const { secret, accountId } = credential.value + const asked = (call: Effect.Effect) => + call.pipe( + // The page shows the mapped message; which failure it was stays on the span. + Effect.tapError((error) => + Effect.annotateCurrentSpan({ + "maple.cost.cloudflare.error": error.kind, + "maple.cost.cloudflare.status": error.status ?? 0, + }), + ), + Effect.mapError(toCostProviderError), + ) + + const info = yield* asked( + CloudflareBilling.getBillableUsageInfo(httpClient, secret, accountId), + ) + if (!info.covered) { + return yield* failed( + "not_ready", + "Cloudflare has no usage-based billing data for this account. Accounts on an enterprise contract are not supported yet.", + ) + } + const now = yield* Clock.currentTimeMillis + const openDay = addDays(utcToday(now), -OPEN_DAYS) + // One request at a time. Each answer is cut to its own piece: it also holds the days + // of the lead, which belong to the piece before. + const usage = yield* Effect.forEach(piecesOf(window), (piece) => + asked( + CloudflareBilling.getBillableUsage(httpClient, secret, accountId, { + from: addDays(piece.startDay, -PERIOD_LEAD_DAYS), + to: addDays(piece.endDay, 1), + }), + ).pipe(Effect.map((items) => toUsageRows(items, accountId, piece, openDay))), + ) + const subscriptions = yield* asked( + CloudflareBilling.listSubscriptions(httpClient, secret, accountId), + ) + const fees = toSubscriptionRows(subscriptions, accountId, window, openDay) + const rows = [...Arr.flatten(usage), ...fees] + yield* Effect.annotateCurrentSpan({ + "maple.cost.rows": rows.length, + "maple.cost.cloudflare.fee_rows": fees.length, + "maple.cost.cloudflare.usage_requests": usage.length, + }) + return { rows, requests: 0 } satisfies CostPull + }, + Effect.tapError((error) => + Effect.annotateCurrentSpan({ "maple.cost.error_reason": error.reason }), + ), + ) + + return { provider: "cloudflare", pull } satisfies CostProviderAdapter + }), + }, +) { + static readonly layer = Layer.effect(this, this.make).pipe( + Layer.provide(Layer.mergeAll(FetchHttpClient.layer, CostConnectionStore.layer)), + ) +} diff --git a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts new file mode 100644 index 0000000000..bae8ca1914 --- /dev/null +++ b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts @@ -0,0 +1,221 @@ +/** + * The slice of Cloudflare's REST API the cost source uses: the accounts a token reads, an + * account's daily billable usage and its subscriptions. Each call reads the status off a plain + * request and decodes only the fields Maple reads. Failures fold into one + * {@link CloudflareBillingError} whose `kind` says what the caller does next. + */ +import { Duration, Effect, Redacted, Schema, Array as Arr } from "effect" +import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http" + +const BASE_URL = "https://api.cloudflare.com/client/v4" +const REQUEST_TIMEOUT = Duration.seconds(30) +const PAGE_SIZE = 50 +const MAX_SUBSCRIPTION_PAGES = 10 + +export class CloudflareBillingError extends Schema.TaggedError()( + "@maple/backend/cost/CloudflareBillingError", + { + message: Schema.String, + kind: Schema.Literals([ + "unauthorized", + "forbidden", + "bad_request", + "rate_limited", + "invalid_response", + "upstream", + ]), + status: Schema.optionalKey(Schema.Number), + cause: Schema.optionalKey(Schema.Defect()), + }, +) {} + +const errorKind = (status: number): CloudflareBillingError["kind"] => + status === 401 + ? "unauthorized" + : status === 403 + ? "forbidden" + : status === 400 + ? "bad_request" + : status === 429 + ? "rate_limited" + : "upstream" + +/** Cloudflare's envelope: `result` is the answer, `result_info` the paging of a listing. */ +const envelope = (result: S) => + Schema.Struct({ + result, + result_info: Schema.optionalKey( + Schema.NullOr( + Schema.Struct({ + total_pages: Schema.optionalKey(Schema.NullOr(Schema.Number)), + total_count: Schema.optionalKey(Schema.NullOr(Schema.Number)), + }), + ), + ), + }) + +const call = ( + httpClient: HttpClient.HttpClient, + token: Redacted.Redacted, + request: HttpClientRequest.HttpClientRequest, + result: S, +) => { + const failed = (kind: CloudflareBillingError["kind"], what: string) => (cause: unknown) => + Effect.fail(new CloudflareBillingError({ message: `Cloudflare API ${what}`, kind, cause })) + return Effect.gen(function* () { + const response = yield* httpClient.execute(HttpClientRequest.bearerToken(request, token)) + if (response.status >= 300) { + return yield* new CloudflareBillingError({ + message: `Cloudflare API answered ${response.status}`, + kind: errorKind(response.status), + status: response.status, + }) + } + // A 2xx that is not JSON is an answer Maple cannot read, not a failed request. + const body = yield* response.json.pipe( + Effect.catchTag( + "HttpClientError", + failed("invalid_response", "answered with a body that is not JSON"), + ), + ) + return yield* Schema.decodeUnknownEffect(envelope(result))(body) + }).pipe( + Effect.timeout(REQUEST_TIMEOUT), + Effect.catchTags({ + HttpClientError: failed("upstream", "request failed"), + TimeoutError: failed("upstream", "request timed out"), + SchemaError: failed("invalid_response", "answered with a body Maple cannot read"), + }), + // A redirect is not Cloudflare answering, so the token is never followed to another host. + Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }), + Effect.annotateSpans("peer.service", "cloudflare"), + ) +} + +const Account = Schema.Struct({ id: Schema.String, name: Schema.String }) +export type CloudflareAccount = typeof Account.Type + +/** + * The accounts the token can see, and how many there are in all. A token limited to one account + * lists exactly that one. A token Cloudflare does not know answers 403 here, not 401. + */ +export const listAccounts = (httpClient: HttpClient.HttpClient, token: Redacted.Redacted) => + call( + httpClient, + token, + HttpClientRequest.setUrlParam( + HttpClientRequest.get(`${BASE_URL}/accounts`), + "per_page", + `${PAGE_SIZE}`, + ), + Schema.Array(Account), + ).pipe( + Effect.map((body) => ({ + accounts: body.result, + total: Math.max(body.result.length, body.result_info?.total_count ?? 0), + })), + ) + +const UsageInfo = Schema.Struct({ covered: Schema.Boolean }) + +/** Whether the account has usage-based billing data at all: not so on an enterprise contract. */ +export const getBillableUsageInfo = ( + httpClient: HttpClient.HttpClient, + token: Redacted.Redacted, + accountId: string, +) => + call( + httpClient, + token, + HttpClientRequest.get(`${BASE_URL}/accounts/${encodeURIComponent(accountId)}/billable-usage/info`), + UsageInfo, + ).pipe(Effect.map((body) => body.result)) + +const isoDay = Schema.String.check( + Schema.isPattern(/^\d{4}-\d{2}-\d{2}/, { description: "Expected to start with YYYY-MM-DD" }), +) + +/** One UTC day of one priced metric. Money is in `BillingCurrency`; most rows bill 0. */ +export const UsageRow = Schema.Struct({ + ServiceName: Schema.String, + ServiceFamilyName: Schema.optionalKey(Schema.NullOr(Schema.String)), + ChargePeriodStart: isoDay, + BillingCurrency: Schema.String, + BilledCost: Schema.Number, + EffectiveCost: Schema.Number, + ListCost: Schema.Number, + ConsumedQuantity: Schema.optionalKey(Schema.NullOr(Schema.Number)), + ConsumedUnit: Schema.optionalKey(Schema.NullOr(Schema.String)), +}) +export type UsageRow = typeof UsageRow.Type + +/** + * The days before `to` of every billing period that starts in `[from, to)`: `to` is exclusive, and + * a period that started before `from` returns nothing, whatever days of it the range covers. The + * range is 90 days at most. One answer, no paging. + */ +export const getBillableUsage = ( + httpClient: HttpClient.HttpClient, + token: Redacted.Redacted, + accountId: string, + range: { readonly from: string; readonly to: string }, +) => + call( + httpClient, + token, + HttpClientRequest.setUrlParams( + HttpClientRequest.get(`${BASE_URL}/accounts/${encodeURIComponent(accountId)}/billable-usage`), + range, + ), + Schema.Array(UsageRow), + ).pipe(Effect.map((body) => body.result)) + +/** A plan or add-on the account pays a fixed fee for. Free items carry no period. */ +export const Subscription = Schema.Struct({ + price: Schema.optionalKey(Schema.NullOr(Schema.Number)), + currency: Schema.optionalKey(Schema.NullOr(Schema.String)), + frequency: Schema.optionalKey(Schema.NullOr(Schema.String)), + current_period_start: Schema.optionalKey(Schema.NullOr(isoDay)), + created_date: Schema.optionalKey(Schema.NullOr(isoDay)), + rate_plan: Schema.optionalKey( + Schema.NullOr(Schema.Struct({ public_name: Schema.optionalKey(Schema.NullOr(Schema.String)) })), + ), + product: Schema.optionalKey( + Schema.NullOr(Schema.Struct({ name: Schema.optionalKey(Schema.NullOr(Schema.String)) })), + ), + zone: Schema.optionalKey( + Schema.NullOr(Schema.Struct({ name: Schema.optionalKey(Schema.NullOr(Schema.String)) })), + ), +}) +export type Subscription = typeof Subscription.Type + +const subscriptionsPage = ( + httpClient: HttpClient.HttpClient, + token: Redacted.Redacted, + accountId: string, + page: number, +) => + call( + httpClient, + token, + HttpClientRequest.setUrlParams( + HttpClientRequest.get(`${BASE_URL}/accounts/${encodeURIComponent(accountId)}/subscriptions`), + { per_page: `${PAGE_SIZE}`, page: `${page}` }, + ), + Schema.Array(Subscription), + ) + +/** Every subscription of the account, zone plans included, over as many pages as it takes. */ +export const listSubscriptions = ( + httpClient: HttpClient.HttpClient, + token: Redacted.Redacted, + accountId: string, +) => + Effect.gen(function* () { + const first = yield* subscriptionsPage(httpClient, token, accountId, 1) + const pages = Math.min(first.result_info?.total_pages ?? 1, MAX_SUBSCRIPTION_PAGES) + const rest = yield* Effect.forEach(pages < 2 ? [] : Arr.range(2, pages), (page) => + Effect.map(subscriptionsPage(httpClient, token, accountId, page), (body) => body.result), + ) + return [...first.result, ...Arr.flatten(rest)] + }) diff --git a/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts b/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts new file mode 100644 index 0000000000..a7872024c9 --- /dev/null +++ b/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts @@ -0,0 +1,169 @@ +// A fake Cloudflare for tests: the accounts, billable usage and subscription calls answered from +// a mutable world, with every call recorded. Usage is served the way the real API was seen to: +// a billing period's days before `to`, and only when the period starts inside `[from, to)`. +import { DISABLED_ORGANIZATION_FEATURE_FLAGS } from "@maple/domain/organization-feature-flags" +import { DateTime, Effect, Layer, Array as Arr, String as Str } from "effect" +import { FetchHttpClient } from "effect/http" +import { Env } from "@maple/backend/platform/Env" +import { OrganizationFeatureFlagsService } from "@maple/backend/services/org/OrganizationFeatureFlagsService" +import type { TestDb } from "@maple/backend/testing/test-pglite" +import { CostConnectionStore } from "../../CostConnectionStore" +import { fakeCostEnvConfig } from "../fake-cost-env" +import type { Subscription, UsageRow } from "./cloudflare-billing-api" + +export type FakeUsageRow = typeof UsageRow.Encoded +export type FakeSubscription = typeof Subscription.Encoded + +export interface FakeCloudflare { + /** Status of `GET /accounts`. */ + accountsStatus: number + /** The accounts the token can see. */ + accounts: ReadonlyArray<{ readonly id: string; readonly name: string }> + /** Status of the billable usage calls, `info` included. */ + billingStatus: number + /** Whether the account has usage-based billing data. */ + covered: boolean + /** Every usage row the account has, or raw text for a malformed answer. */ + usage: ReadonlyArray | string + /** The day of the month a billing period starts on. */ + anchorDay: number + subscriptions: ReadonlyArray + /** Subscriptions one page holds, to make a listing span pages. */ + subscriptionsPageSize: number + /** Whether the org is in the `cloudCost` rollout. */ + cloudCost: boolean + readonly urls: Array + readonly authorizations: Array +} + +export const FAKE_ACCOUNT = { id: "0123456789abcdef0123456789abcdef", name: "Example Account" } + +export const makeFakeCloudflare = (overrides: Partial = {}): FakeCloudflare => ({ + accountsStatus: 200, + accounts: [FAKE_ACCOUNT], + billingStatus: 200, + covered: true, + usage: [], + anchorDay: 1, + subscriptions: [], + subscriptionsPageSize: 50, + cloudCost: true, + urls: [], + authorizations: [], + ...overrides, +}) + +/** One usage row of `day`, with the fields a real row carries that Maple reads. */ +export const usageRow = ( + day: string, + family: string, + metric: string, + cost: number, + over: Partial = {}, +): FakeUsageRow => ({ + ServiceName: metric, + ServiceFamilyName: family, + ChargePeriodStart: `${day}T00:00:00Z`, + BillingCurrency: "USD", + BilledCost: cost, + EffectiveCost: cost, + ListCost: cost, + ConsumedQuantity: 1, + ConsumedUnit: "", + ...over, +}) + +const DAY_MS = 86_400_000 +const dayMs = (day: string) => DateTime.toEpochMillis(DateTime.makeUnsafe(`${day}T00:00:00Z`)) + +/** The day the billing period of `day` started: the newest anchor day at or before it. */ +const periodStart = (day: string, anchorDay: number): string => { + const anchor = Str.padStart(2, "0")(`${anchorDay}`) + const sameMonth = `${Str.slice(0, 8)(day)}${anchor}` + if (sameMonth <= day) return sameMonth + const previous = DateTime.formatIsoDateUtc( + DateTime.subtract(DateTime.makeUnsafe(`${Str.slice(0, 8)(day)}01T00:00:00Z`), { months: 1 }), + ) + return `${Str.slice(0, 8)(previous)}${anchor}` +} + +const envelope = (result: unknown, info: Record = {}) => + Response.json({ success: true, errors: [], messages: [], result, result_info: info }) + +const refused = (status: number) => + Response.json( + { success: false, errors: [{ code: 10000, message: "Authentication error" }], result: null }, + { status }, + ) + +const usageAnswer = (world: FakeCloudflare, params: URLSearchParams): Response => { + const from = params.get("from") ?? "" + const to = params.get("to") ?? "" + // The real API refuses an empty range and one over 90 days. + const days = (dayMs(to) - dayMs(from)) / DAY_MS + if (!(days >= 1 && days <= 90)) return refused(400) + if (typeof world.usage === "string") return new Response(world.usage, { status: 200 }) + return envelope( + Arr.filter(world.usage, (row) => { + const day = Str.slice(0, 10)(row.ChargePeriodStart) + const start = periodStart(day, world.anchorDay) + return start >= from && start < to && day < to + }), + ) +} + +const subscriptionsAnswer = (world: FakeCloudflare, params: URLSearchParams): Response => { + const page = Number(params.get("page") ?? "1") + const size = world.subscriptionsPageSize + return envelope(Arr.take(Arr.drop(world.subscriptions, (page - 1) * size), size), { + page, + total_pages: Math.max(1, Math.ceil(world.subscriptions.length / size)), + total_count: world.subscriptions.length, + }) +} + +const fetchStub = + (world: FakeCloudflare): typeof fetch => + async (input: string | URL | Request, init?: RequestInit) => { + const href = typeof input === "string" ? input : input instanceof URL ? input.href : input.url + const url = new URL(href) + world.urls.push(`${url.pathname}${url.search}`) + world.authorizations.push(new Headers(init?.headers).get("authorization")) + if (Str.endsWith("/accounts")(url.pathname)) { + return world.accountsStatus === 200 + ? envelope(world.accounts, { total_count: world.accounts.length, total_pages: 1 }) + : refused(world.accountsStatus) + } + if (world.billingStatus !== 200) return refused(world.billingStatus) + if (Str.endsWith("/billable-usage/info")(url.pathname)) { + return envelope({ covered: world.covered, subscriptions: [] }) + } + if (Str.endsWith("/billable-usage")(url.pathname)) return usageAnswer(world, url.searchParams) + return subscriptionsAnswer(world, url.searchParams) + } + +/** Everything the Cloudflare cost services need, over the fake and a test database. */ +export const fakeCloudflareDeps = ( + testDb: TestDb, + world: FakeCloudflare, + /** `MAPLE_INGEST_KEY_ENCRYPTION_KEY`, for a deployment whose secret key is unusable. */ + encryptionKey?: string, +) => + Layer.effect(CostConnectionStore, CostConnectionStore.make).pipe( + Layer.provideMerge( + Layer.mergeAll( + FetchHttpClient.layer, + Layer.succeed(OrganizationFeatureFlagsService, { + flags: () => + Effect.sync(() => ({ + ...DISABLED_ORGANIZATION_FEATURE_FLAGS, + cloudCost: world.cloudCost, + })), + }), + testDb.layer, + Env.layer, + ), + ), + Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchStub(world))), + Layer.provide(fakeCostEnvConfig(encryptionKey)), + ) diff --git a/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts b/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts new file mode 100644 index 0000000000..6f34c528a8 --- /dev/null +++ b/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts @@ -0,0 +1,266 @@ +import { Schema, Array as Arr } from "effect" +import { describe, expect, it } from "vitest" +import { Subscription, UsageRow } from "./cloudflare-billing-api" +import { FAKE_ACCOUNT, type FakeSubscription, usageRow } from "./fake-cloudflare" +import { toSubscriptionRows, toUsageRows } from "./normalize" + +const ACCOUNT = FAKE_ACCOUNT.id +const WINDOW = { startDay: "2026-10-01", endDay: "2026-11-10" } +/** The two newest days of a pull on 2026-11-11 can still change. */ +const OPEN_DAY = "2026-11-09" + +const usage = (rows: ReadonlyArray) => + toUsageRows(Schema.decodeSync(Schema.Array(UsageRow))(rows), ACCOUNT, WINDOW, OPEN_DAY) + +const fees = (subscriptions: ReadonlyArray, window = WINDOW) => + toSubscriptionRows( + Schema.decodeSync(Schema.Array(Subscription))(subscriptions), + ACCOUNT, + window, + OPEN_DAY, + ) + +/** A row as the API answers it, with the fields Maple does not read. */ +const recorded = { + ServiceName: "Durable Objects Compute Duration (First 400,000 GB-s are included)", + ServiceFamilyName: "Durable Objects", + ChargeCategory: "Usage", + ChargeClass: null, + ChargeDescription: "Durable Objects Compute Duration", + ChargePeriodStart: "2026-10-03T00:00:00Z", + ChargePeriodEnd: "2026-10-04T00:00:00Z", + BillingPeriodStart: "2026-10-01T00:00:00Z", + BillingCurrency: "USD", + ConsumedUnit: "GB-seconds", + ConsumedQuantity: 120000.5, + PricingQuantity: 120000.5, + PricingUnit: "GB-seconds", + CumulatedPricingQuantity: 610000, + ListCost: 1.5, + ContractedCost: 1.5, + EffectiveCost: 1.25, + BilledCost: 1.25, + CumulatedContractedCost: 2.6, + SubscriptionId: "sub_example", + BillingAccountId: ACCOUNT, + BillingAccountName: "Example Account", + ServiceProviderName: "Cloudflare, Inc.", + InvoiceIssuerName: "Cloudflare, Inc.", + HostProviderName: "Cloudflare, Inc.", +} + +describe("toUsageRows", () => { + it("maps a recorded row: the product is the service, the priced metric the usage type", () => { + expect(usage([recorded])).toEqual([ + { + day: "2026-10-03", + accountId: ACCOUNT, + cloudService: "Durable Objects", + serviceCategory: "Compute", + usageType: "Durable Objects Compute Duration (First 400,000 GB-s are included)", + region: "global", + resourceId: "", + resourceType: "", + serviceName: "", + chargeCategory: "usage", + basis: "billed", + currency: "USD", + billedCost: 1.25, + effectiveCost: 1.25, + listCost: 1.5, + consumedQuantity: 120000.5, + consumedUnit: "GB-seconds", + tags: {}, + estimated: false, + }, + ]) + }) + + it("leaves out rows inside the included allowance, and keeps one billed nothing at a list price", () => { + const rows = usage([ + usageRow("2026-10-03", "Workers", "Workers Requests (First 10M are included)", 0), + usageRow("2026-10-03", "R2", "R2 Storage", 0, { ListCost: 0.4 }), + usageRow("2026-10-03", "Workers KV", "KV Reads", 0, { ConsumedQuantity: 3 }), + ]) + expect(Arr.map(rows, (row) => [row.usageType, row.billedCost, row.listCost])).toEqual([ + ["R2 Storage", 0, 0.4], + ]) + }) + + it("keeps only the window's days, and marks the newest two as open to change", () => { + const rows = usage([ + usageRow("2026-09-30", "R2", "R2 Storage", 1), + usageRow("2026-10-01", "R2", "R2 Storage", 2), + usageRow("2026-11-08", "R2", "R2 Storage", 3), + usageRow("2026-11-09", "R2", "R2 Storage", 4), + usageRow("2026-11-10", "R2", "R2 Storage", 5), + usageRow("2026-11-11", "R2", "R2 Storage", 6), + ]) + expect(Arr.map(rows, (row) => [row.day, row.billedCost, row.estimated])).toEqual([ + ["2026-10-01", 2, false], + ["2026-11-08", 3, false], + ["2026-11-09", 4, true], + ["2026-11-10", 5, true], + ]) + }) + + it("puts each product in its category, and one it does not know in Other", () => { + const families = ["Workers", "Queues", "Containers", "R2", "D1", "Workers KV", "Workers AI", "Email"] + const rows = usage([ + ...Arr.map(families, (family) => usageRow("2026-10-05", family, `${family} metric`, 1)), + usageRow("2026-10-05", "Something New", "New metric", 1), + usageRow("2026-10-05", "Workers", "No family", 1, { ServiceFamilyName: null }), + ]) + expect(Arr.map(rows, (row) => [row.cloudService, row.serviceCategory])).toEqual([ + ["Workers", "Compute"], + ["Queues", "Compute"], + ["Containers", "Compute"], + ["R2", "Storage"], + ["D1", "Databases"], + ["Workers KV", "Databases"], + ["Workers AI", "AI and Machine Learning"], + ["Email", "Other"], + ["Something New", "Other"], + ["", "Other"], + ]) + }) + + it("adds up two rows of one day and metric, which the warehouse would read as one", () => { + const rows = usage([ + usageRow("2026-10-05", "R2", "R2 Storage", 1.5, { + ConsumedQuantity: 10, + ConsumedUnit: "GB-months", + }), + usageRow("2026-10-05", "R2", "R2 Storage", 2.25, { + ConsumedQuantity: 5, + ConsumedUnit: "GB-months", + }), + ]) + expect(Arr.map(rows, (row) => [row.billedCost, row.listCost, row.consumedQuantity])).toEqual([ + [3.75, 3.75, 15], + ]) + }) + + it("keeps the row's own currency", () => { + expect( + usage([usageRow("2026-10-05", "R2", "R2 Storage", 1, { BillingCurrency: "EUR" })]), + ).toMatchObject([{ currency: "EUR" }]) + }) +}) + +const plan = (over: FakeSubscription = {}): FakeSubscription => ({ + price: 5, + currency: "USD", + frequency: "monthly", + current_period_start: "2026-11-04T09:12:44Z", + created_date: "2025-03-04T09:12:44Z", + rate_plan: { public_name: "Workers Paid" }, + product: { name: "workers" }, + ...over, +}) + +describe("toSubscriptionRows", () => { + it("puts a monthly fee on each period start in the window, stepped back from the current one", () => { + const rows = fees([plan()]) + expect(Arr.map(rows, (row) => [row.day, row.billedCost, row.estimated])).toEqual([ + ["2026-11-04", 5, false], + ["2026-10-04", 5, false], + ]) + expect(rows[0]).toEqual({ + day: "2026-11-04", + accountId: ACCOUNT, + cloudService: "Subscriptions", + serviceCategory: "Other", + usageType: "Workers Paid", + region: "global", + resourceId: "", + resourceType: "", + serviceName: "", + chargeCategory: "purchase", + basis: "billed", + currency: "USD", + billedCost: 5, + effectiveCost: 5, + listCost: 5, + consumedQuantity: 0, + consumedUnit: "", + tags: {}, + estimated: false, + }) + }) + + it("names the zone a zone's plan belongs to", () => { + const [row] = fees([ + plan({ + price: 10, + rate_plan: { public_name: "Advanced Certificate Manager" }, + zone: { name: "example.com" }, + }), + ]) + expect([row?.usageType, row?.resourceId, row?.resourceType]).toEqual([ + "Advanced Certificate Manager", + "example.com", + "zone", + ]) + }) + + it("never charges a period from before the subscription was created", () => { + const rows = fees([ + plan({ created_date: "2026-10-20T00:00:00Z", current_period_start: "2026-11-01T00:00:00Z" }), + ]) + expect(Arr.map(rows, (row) => row.day)).toEqual(["2026-11-01"]) + }) + + it("walks a year of monthly periods back, and keeps to the month's last day where it is shorter", () => { + const rows = fees( + [plan({ current_period_start: "2026-10-31T00:00:00Z", created_date: "2020-01-01T00:00:00Z" })], + { + startDay: "2025-12-01", + endDay: "2026-11-10", + }, + ) + expect(Arr.map(rows, (row) => row.day)).toEqual([ + "2026-10-31", + "2026-09-30", + "2026-08-31", + "2026-07-31", + "2026-06-30", + "2026-05-31", + "2026-04-30", + "2026-03-31", + "2026-02-28", + "2026-01-31", + "2025-12-31", + ]) + }) + + it("charges a yearly fee once, on its current period's start", () => { + const yearly = plan({ price: 120, frequency: "yearly", current_period_start: "2026-10-15T00:00:00Z" }) + expect(Arr.map(fees([yearly]), (row) => [row.day, row.billedCost])).toEqual([["2026-10-15", 120]]) + // A period that started before the window is not in it. + expect(fees([{ ...yearly, current_period_start: "2026-03-15T00:00:00Z" }])).toEqual([]) + }) + + it("leaves out free items, items with no period and frequencies it does not bill by", () => { + expect( + fees([ + plan({ price: 0 }), + { price: 0, frequency: "not-applicable", created_date: "2025-01-01T00:00:00Z" }, + plan({ frequency: "not-applicable" }), + plan({ frequency: "quarterly" }), + plan({ current_period_start: null }), + ]), + ).toEqual([]) + }) + + it("adds up two subscriptions to one plan on one day, and marks an open day", () => { + const rows = fees([ + plan({ current_period_start: "2026-11-09T00:00:00Z" }), + plan({ current_period_start: "2026-11-09T12:00:00Z", price: 7 }), + ]) + expect(Arr.map(rows, (row) => [row.day, row.billedCost, row.estimated])).toEqual([ + ["2026-11-09", 12, true], + ["2026-10-09", 12, false], + ]) + }) +}) diff --git a/packages/backend/src/services/cost/providers/cloudflare/normalize.ts b/packages/backend/src/services/cost/providers/cloudflare/normalize.ts new file mode 100644 index 0000000000..3ddd824223 --- /dev/null +++ b/packages/backend/src/services/cost/providers/cloudflare/normalize.ts @@ -0,0 +1,160 @@ +// Cloudflare billing as `cost_daily` rows: usage charges per day and priced metric, and each paid +// subscription's fee on the day its billing period starts. +import { DateTime, Array as Arr, Record as Rec, String as Str } from "effect" +import type { CostPullRow, CostPullWindow } from "../../CostProvider" +import type { Subscription, UsageRow } from "./cloudflare-billing-api" + +/** FOCUS `ServiceCategory` of each Cloudflare product family; one not listed is "Other". */ +const FAMILY_CATEGORIES = new Map([ + ["Workers", "Compute"], + ["Durable Objects", "Compute"], + ["Containers", "Compute"], + ["Queues", "Compute"], + ["R2", "Storage"], + ["D1", "Databases"], + ["Workers KV", "Databases"], + ["Workers AI", "AI and Machine Learning"], +]) +const OTHER_CATEGORY = "Other" + +/** `CloudService` of every subscription fee: the plans are not products with usage of their own. */ +export const SUBSCRIPTIONS_SERVICE = "Subscriptions" + +const dayOf = (timestamp: string): string => Str.slice(0, 10)(timestamp) + +const inWindow = (day: string, window: CostPullWindow): boolean => + day >= window.startDay && day <= window.endDay + +// NUL cannot be in a day, a name or a currency, so the key keeps the parts apart. +const keyOf = (parts: ReadonlyArray): string => Arr.join(parts, "\u0000") + +const sum = (items: ReadonlyArray, of: (item: A) => number): number => + Arr.reduce(items, 0, (total, item) => total + of(item)) + +/** + * The window's usage charges, one row per day, product and priced metric. Cloudflare answers a + * row for every metric every day, and most are inside the included allowance: a row with no + * billed and no list cost is left out. Days from `openDay` on can still change. + */ +export const toUsageRows = ( + items: ReadonlyArray, + accountId: string, + window: CostPullWindow, + openDay: string, +): ReadonlyArray => { + const charged = Arr.filter( + items, + (item) => + inWindow(dayOf(item.ChargePeriodStart), window) && (item.BilledCost !== 0 || item.ListCost !== 0), + ) + // A day's rows are replaced whole and read per key, so two rows of one key would count once. + const groups = Arr.groupBy(charged, (item) => + keyOf([ + dayOf(item.ChargePeriodStart), + item.ServiceFamilyName ?? "", + item.ServiceName, + item.BillingCurrency, + ]), + ) + return Arr.map(Rec.values(groups), (group): CostPullRow => { + const first = Arr.headNonEmpty(group) + const day = dayOf(first.ChargePeriodStart) + const family = first.ServiceFamilyName ?? "" + return { + day, + accountId, + cloudService: family, + serviceCategory: FAMILY_CATEGORIES.get(family) ?? OTHER_CATEGORY, + usageType: first.ServiceName, + region: "global", + resourceId: "", + resourceType: "", + serviceName: "", + chargeCategory: "usage", + basis: "billed", + currency: first.BillingCurrency, + billedCost: sum(group, (item) => item.BilledCost), + effectiveCost: sum(group, (item) => item.EffectiveCost), + listCost: sum(group, (item) => item.ListCost), + consumedQuantity: sum(group, (item) => item.ConsumedQuantity ?? 0), + consumedUnit: first.ConsumedUnit ?? "", + tags: {}, + estimated: day >= openDay, + } + }) +} + +/** Monthly periods a fee is stepped back over: the sync reads a year, and a period starts within the last month. */ +const MONTHLY_PERIODS = 14 + +/** `day` moved back by `months`, on the same day of the month or the month's last. */ +const monthsBefore = (day: string, months: number): string => + DateTime.formatIsoDateUtc(DateTime.subtract(DateTime.makeUnsafe(`${day}T00:00:00Z`), { months })) + +/** The days in the window a subscription's billing period started on, by how often it bills. */ +const periodStarts = (subscription: Subscription, window: CostPullWindow): ReadonlyArray => { + const start = subscription.current_period_start + if (start === undefined || start === null) return [] + const current = dayOf(start) + const created = dayOf(subscription.created_date ?? "") + const starts = + subscription.frequency === "monthly" + ? Arr.makeBy(MONTHLY_PERIODS, (months) => monthsBefore(current, months)) + : subscription.frequency === "yearly" + ? [current] + : [] + return Arr.filter(starts, (day) => inWindow(day, window) && day >= created) +} + +/** + * Each paid subscription's fee, as a purchase on the day a billing period started inside the + * window. Cloudflare reports the current period only: earlier months are stepped back from it at + * today's price, which assumes the price did not change. A yearly fee is on its current period's + * start alone. + */ +export const toSubscriptionRows = ( + subscriptions: ReadonlyArray, + accountId: string, + window: CostPullWindow, + openDay: string, +): ReadonlyArray => { + const fees = Arr.flatMap(subscriptions, (subscription) => { + const price = subscription.price ?? 0 + return price > 0 + ? Arr.map(periodStarts(subscription, window), (day) => ({ + day, + price, + plan: subscription.rate_plan?.public_name ?? subscription.product?.name ?? "", + zone: subscription.zone?.name ?? "", + currency: subscription.currency ?? "USD", + })) + : [] + }) + // Two subscriptions to one plan (and zone) on one day are one key, so their fees add up. + const groups = Arr.groupBy(fees, (fee) => keyOf([fee.day, fee.plan, fee.zone, fee.currency])) + return Arr.map(Rec.values(groups), (group): CostPullRow => { + const first = Arr.headNonEmpty(group) + const price = sum(group, (fee) => fee.price) + return { + day: first.day, + accountId, + cloudService: SUBSCRIPTIONS_SERVICE, + serviceCategory: OTHER_CATEGORY, + usageType: first.plan, + region: "global", + resourceId: first.zone, + resourceType: first.zone === "" ? "" : "zone", + serviceName: "", + chargeCategory: "purchase", + basis: "billed", + currency: first.currency, + billedCost: price, + effectiveCost: price, + listCost: price, + consumedQuantity: 0, + consumedUnit: "", + tags: {}, + estimated: first.day >= openDay, + } + }) +} diff --git a/packages/backend/src/services/cost/providers/fake-cost-env.ts b/packages/backend/src/services/cost/providers/fake-cost-env.ts new file mode 100644 index 0000000000..af462ef031 --- /dev/null +++ b/packages/backend/src/services/cost/providers/fake-cost-env.ts @@ -0,0 +1,22 @@ +// The deployment config the cost provider fakes run their services on. +import { ConfigProvider } from "effect" + +export const FAKE_ENCRYPTION_KEY = Buffer.alloc(32, 9).toString("base64") + +/** `encryptionKey` is `MAPLE_INGEST_KEY_ENCRYPTION_KEY`, for a deployment whose secret key is unusable. */ +export const fakeCostEnvConfig = (encryptionKey = FAKE_ENCRYPTION_KEY) => + ConfigProvider.layer( + ConfigProvider.fromUnknown({ + PORT: "3472", + TINYBIRD_HOST: "https://api.tinybird.co", + TINYBIRD_TOKEN: "test-token", + MAPLE_AUTH_MODE: "self_hosted", + MAPLE_ROOT_PASSWORD: "test-root-password", + MAPLE_DEFAULT_ORG_ID: "default", + MAPLE_INGEST_KEY_ENCRYPTION_KEY: encryptionKey, + MAPLE_INGEST_KEY_LOOKUP_HMAC_KEY: "maple-test-lookup-secret", + MAPLE_INGEST_PUBLIC_URL: "https://ingest.test", + // OpenRouter's management host is the global one on every instance. + MAPLE_REGION: "eu", + }), + ) diff --git a/packages/backend/src/services/cost/providers/openrouter/fake-openrouter.ts b/packages/backend/src/services/cost/providers/openrouter/fake-openrouter.ts index bf5a23ee6d..e8ebc126c8 100644 --- a/packages/backend/src/services/cost/providers/openrouter/fake-openrouter.ts +++ b/packages/backend/src/services/cost/providers/openrouter/fake-openrouter.ts @@ -1,12 +1,13 @@ // A fake OpenRouter for tests: `GET /key` and `GET /activity` answered from a mutable world, with // every call recorded. import { DISABLED_ORGANIZATION_FEATURE_FLAGS } from "@maple/domain/organization-feature-flags" -import { ConfigProvider, Effect, Layer, String as Str } from "effect" +import { Effect, Layer, String as Str } from "effect" import { FetchHttpClient } from "effect/http" import { Env } from "@maple/backend/platform/Env" import { OrganizationFeatureFlagsService } from "@maple/backend/services/org/OrganizationFeatureFlagsService" import type { TestDb } from "@maple/backend/testing/test-pglite" import type { ActivityItem } from "../../../org/openrouter-api" +import { fakeCostEnvConfig } from "../fake-cost-env" export interface FakeOpenRouter { /** Status of `GET /key`. */ @@ -74,31 +75,12 @@ const fetchStub = : Response.json({ data: world.activity }) } -const config = (encryptionKey: string) => - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - PORT: "3472", - TINYBIRD_HOST: "https://api.tinybird.co", - TINYBIRD_TOKEN: "test-token", - MAPLE_AUTH_MODE: "self_hosted", - MAPLE_ROOT_PASSWORD: "test-root-password", - MAPLE_DEFAULT_ORG_ID: "default", - MAPLE_INGEST_KEY_ENCRYPTION_KEY: encryptionKey, - MAPLE_INGEST_KEY_LOOKUP_HMAC_KEY: "maple-test-lookup-secret", - MAPLE_INGEST_PUBLIC_URL: "https://ingest.test", - // The management host is the global one on every instance. - MAPLE_REGION: "eu", - }), - ) - -const ENCRYPTION_KEY = Buffer.alloc(32, 9).toString("base64") - /** Everything the OpenRouter cost services need, over the fake and a test database. */ export const fakeOpenRouterDeps = ( testDb: TestDb, world: FakeOpenRouter, /** `MAPLE_INGEST_KEY_ENCRYPTION_KEY`, for a deployment whose secret key is unusable. */ - encryptionKey = ENCRYPTION_KEY, + encryptionKey?: string, ) => Layer.mergeAll( FetchHttpClient.layer, @@ -110,5 +92,5 @@ export const fakeOpenRouterDeps = ( Env.layer, ).pipe( Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchStub(world))), - Layer.provide(config(encryptionKey)), + Layer.provide(fakeCostEnvConfig(encryptionKey)), ) diff --git a/packages/backend/src/services/cost/sync-plan.test.ts b/packages/backend/src/services/cost/sync-plan.test.ts index b56cd2bb0f..0a5ebbdb4c 100644 --- a/packages/backend/src/services/cost/sync-plan.test.ts +++ b/packages/backend/src/services/cost/sync-plan.test.ts @@ -359,6 +359,70 @@ describe("planCostSync for a provider that serves 30 completed days", () => { }) }) +describe("planCostSync for a provider that serves a year of completed days", () => { + const CLOUDFLARE_NEW: CostSyncState = { ...NEW, provider: "cloudflare" } + const cloudflarePulled = ( + now: number, + backfillDate: string, + ago: { readonly head: number; readonly settle: number }, + ): CostSyncState => ({ + ...pulled(now, backfillDate, ago), + provider: "cloudflare", + // The newest day it serves is the day before the pull. + watermarkDate: utcToday(now - ago.head - DAY_MS), + }) + + it("starts at the previous month and ends yesterday", () => { + const now = at("2026-11-20T12:00:00Z") + assert.deepStrictEqual(planCostSync(CLOUDFLARE_NEW, now), { + mode: "settle", + startDay: "2026-10-01", + endDay: "2026-11-19", + }) + }) + + it("takes the four newest served days as its head", () => { + const now = at("2026-11-20T12:00:00Z") + const state = cloudflarePulled(now, "2026-10-01", { head: DAY_MS, settle: 2 * DAY_MS }) + assert.deepStrictEqual(planCostSync(state, now), { + mode: "head", + startDay: "2026-11-16", + endDay: "2026-11-19", + }) + }) + + it("walks history a month per step down to the eleventh month back, and no further", () => { + const now = at("2026-11-20T12:00:00Z") + const current = { head: HOUR_MS, settle: HOUR_MS } + assert.strictEqual(historyFloor("cloudflare", "2026-11-20"), "2025-12-01") + assert.deepStrictEqual(planCostSync(cloudflarePulled(now, "2026-01-01", current), now), { + mode: "backfill", + startDay: "2025-12-01", + endDay: "2025-12-31", + }) + assert.deepStrictEqual(planCostSync(cloudflarePulled(now, "2025-12-01", current), now), { + mode: "idle", + }) + }) + + it("keeps its floor inside the year Cloudflare serves on every day of a month", () => { + // One year back from the pull is the oldest day served: the floor is never before it. + assert.isTrue(historyFloor("cloudflare", "2026-11-01") >= "2025-11-01") + assert.isTrue(historyFloor("cloudflare", "2026-11-30") >= "2025-11-30") + assert.strictEqual(historyFloor("cloudflare", "2027-01-31"), "2026-02-01") + }) + + it("resumes at the floor after a hold longer than what the provider serves", () => { + const now = at("2026-11-20T12:00:00Z") + const state = cloudflarePulled(now, "2025-01-01", { head: 500 * DAY_MS, settle: 500 * DAY_MS }) + assert.deepStrictEqual(planCostSync(state, now), { + mode: "settle", + startDay: "2025-12-01", + endDay: "2026-11-19", + }) + }) +}) + describe("planCostSync request budgets", () => { const now = at("2026-11-11T12:00:00Z") const current = { head: HOUR_MS, settle: HOUR_MS } diff --git a/packages/backend/src/services/cost/sync-plan.ts b/packages/backend/src/services/cost/sync-plan.ts index b178bb1ec7..89c73089a5 100644 --- a/packages/backend/src/services/cost/sync-plan.ts +++ b/packages/backend/src/services/cost/sync-plan.ts @@ -31,6 +31,11 @@ export const HISTORY_MONTHS = 13 * dropped comes back empty, and an empty day replaces the stored one. */ const OPENROUTER_HISTORY_DAYS = 28 +/** + * Cloudflare serves the last year of days. History stops at the first day of the month this many + * months before the current one, which is always inside that year. + */ +const CLOUDFLARE_HISTORY_MONTHS = 11 // Through this day of the month the previous month's bill is still open; the first settle after // it takes the previous month once more, so its last pull is never from before it closed. export const PREVIOUS_MONTH_OPEN_DAYS = 10 @@ -66,6 +71,7 @@ interface CostSyncProfile { const COST_SYNC_PROFILES = { aws: { floor: (today) => monthStart(today, -HISTORY_MONTHS), lagDays: 0 }, openrouter: { floor: (today) => addDays(today, -OPENROUTER_HISTORY_DAYS), lagDays: 1 }, + cloudflare: { floor: (today) => monthStart(today, -CLOUDFLARE_HISTORY_MONTHS), lagDays: 1 }, } as const satisfies Record /** The oldest day the sync pulls from `provider`. */ diff --git a/packages/backend/src/services/org/OrganizationService.ts b/packages/backend/src/services/org/OrganizationService.ts index 0cb34c0f9e..154b3b4988 100644 --- a/packages/backend/src/services/org/OrganizationService.ts +++ b/packages/backend/src/services/org/OrganizationService.ts @@ -174,6 +174,8 @@ const PURGE_ORDER: ReadonlyArray = [ "OrgOpenRouterKeys", // A surviving key would keep Maple reading the deleted org's OpenRouter spend. "OpenRouterCostConnections", + // The same for every provider read with a pasted credential. + "CostConnections", // A deleted member org stops being billed to anyone. A billing org with members is refused. "OrgBillingLinks", // Drop rules and the hash salt: a recreated org must not silently lose data to old rules. diff --git a/packages/backend/src/services/secrets/reseal-targets.ts b/packages/backend/src/services/secrets/reseal-targets.ts index cf6302d0a0..329ec67713 100644 --- a/packages/backend/src/services/secrets/reseal-targets.ts +++ b/packages/backend/src/services/secrets/reseal-targets.ts @@ -10,6 +10,7 @@ import { AwsConnections, ChatWorkspaces, CliDeviceAuthorizations, + CostConnections, DashboardShares, ExternalDataSources, GcpConnectors, @@ -274,6 +275,14 @@ export const RESEAL_TARGETS = { keys: One, ref: org, }), + costConnectionSecretSlot: tripleTarget({ + slot: slots.costConnectionSecretSlot, + table: CostConnections, + columns: { ciphertext: "secretCiphertext", iv: "secretIv", tag: "secretTag" }, + identity: ["orgId", "provider"], + keys: Two, + ref: ([orgId, provider]) => ({ orgId, provider }), + }), planetScaleWebhookSecretSlot: tripleTarget({ slot: slots.planetScaleWebhookSecretSlot, table: PlanetscaleConnections, diff --git a/packages/db/drizzle/20261112221939_cost_connections/migration.sql b/packages/db/drizzle/20261112221939_cost_connections/migration.sql new file mode 100644 index 0000000000..38b900c6d8 --- /dev/null +++ b/packages/db/drizzle/20261112221939_cost_connections/migration.sql @@ -0,0 +1,18 @@ +-- metadata +CREATE TABLE IF NOT EXISTS "cost_connections" ( + "id" text NOT NULL, + "org_id" text NOT NULL, + "provider" text NOT NULL, + "secret_ciphertext" text NOT NULL, + "secret_iv" text NOT NULL, + "secret_tag" text NOT NULL, + "account_id" text NOT NULL, + "account_label" text NOT NULL, + "created_at" timestamp with time zone NOT NULL, + "updated_at" timestamp with time zone NOT NULL, + "updated_by" text NOT NULL, + CONSTRAINT "cost_connections_pkey" PRIMARY KEY ("id") +); +--> statement-breakpoint +-- metadata +CREATE UNIQUE INDEX IF NOT EXISTS "cost_connections_org_provider_idx" ON "cost_connections" USING btree ("org_id", "provider"); diff --git a/packages/db/drizzle/20261112221939_cost_connections/snapshot.json b/packages/db/drizzle/20261112221939_cost_connections/snapshot.json new file mode 100644 index 0000000000..0b95e41202 --- /dev/null +++ b/packages/db/drizzle/20261112221939_cost_connections/snapshot.json @@ -0,0 +1,22723 @@ +{ + "version": "1", + "dialect": "postgres", + "id": "e24847153356524297eba7b70f21b8155780037b367cbe68be5c37437fe1d03e", + "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_connections", + "primaryKey": { + "name": "cost_connections_pkey", + "columns": [ + "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": "tags_json", + "position": 11, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "signal_type", + "position": 12, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "comparator", + "position": 13, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold", + "position": 14, + "type": "double precision", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "threshold_upper", + "position": 15, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "thresholds_json", + "position": 16, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "window_minutes", + "position": 17, + "type": "integer", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "evaluation_interval_minutes", + "position": 18, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "slowed_interval_minutes", + "position": 19, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "slowed_at", + "position": 20, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "minimum_sample_count", + "position": 21, + "type": "integer", + "notNull": true, + "default": "0", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_breaches_required", + "position": 22, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "breaches_out_of", + "position": 23, + "type": "integer", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "consecutive_healthy_required", + "position": 24, + "type": "integer", + "notNull": true, + "default": "2", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "renotify_interval_minutes", + "position": 25, + "type": "integer", + "notNull": true, + "default": "30", + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "apdex_threshold_ms", + "position": 26, + "type": "double precision", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_builder_draft_json", + "position": 27, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "raw_query_sql", + "position": 28, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "group_by", + "position": 29, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "destination_ids_json", + "position": 30, + "type": "jsonb", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "query_spec_json", + "position": 31, + "type": "jsonb", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "reducer", + "position": 32, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "sample_count_strategy", + "position": 33, + "type": "text", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "no_data_behavior", + "position": 34, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "last_scheduled_at", + "position": 35, + "type": "timestamp with time zone", + "notNull": false, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_at", + "position": 36, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_at", + "position": 37, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "created_by", + "position": 38, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "alert_rules", + "name": "updated_by", + "position": 39, + "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_connections", + "name": "id", + "position": 0, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "org_id", + "position": 1, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "provider", + "position": 2, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "secret_ciphertext", + "position": 3, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "secret_iv", + "position": 4, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "secret_tag", + "position": 5, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "account_id", + "position": 6, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "account_label", + "position": 7, + "type": "text", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "created_at", + "position": 8, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "updated_at", + "position": 9, + "type": "timestamp with time zone", + "notNull": true, + "default": null, + "identity": null + }, + { + "kind": "column", + "table": "cost_connections", + "name": "updated_by", + "position": 10, + "type": "text", + "notNull": true, + "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_connections", + "name": "cost_connections_org_provider_idx", + "unique": true, + "method": "btree", + "columns": [ + "\"org_id\"", + "\"provider\"" + ], + "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/cost-connections.ts b/packages/db/src/tables/cost-connections.ts new file mode 100644 index 0000000000..543270f250 --- /dev/null +++ b/packages/db/src/tables/cost-connections.ts @@ -0,0 +1,34 @@ +import * as PG from "@maple-dev/effect-orm/postgres" +import { CostProvider } from "@maple/domain/cost" +import { OrgId, UserId } from "@maple/primitives" + +/** + * A credential an org pasted so Maple can read one provider's spend (AES-GCM, bound to the org and + * the provider), one per provider and org. `cost_sources.connection_ref` holds `id`, which a + * replaced credential keeps. OpenRouter predates this table and keeps its own. + */ +export const CostConnections = PG.table("cost_connections", { + columns: { + id: PG.text, + orgId: PG.column(PG.brand(PG.text, OrgId), { name: "org_id" }), + provider: PG.brand(PG.text, CostProvider), + secretCiphertext: PG.column(PG.text, { name: "secret_ciphertext" }), + secretIv: PG.column(PG.text, { name: "secret_iv" }), + secretTag: PG.column(PG.text, { name: "secret_tag" }), + /** + * The provider's account the credential reads. A replacement must name the same one, so one + * source never mixes two accounts. + */ + accountId: PG.column(PG.text, { name: "account_id" }), + /** The account as the provider names it, for people. */ + accountLabel: PG.column(PG.text, { name: "account_label" }), + createdAt: PG.column(PG.timestamptzMillis, { name: "created_at" }), + updatedAt: PG.column(PG.timestamptzMillis, { name: "updated_at" }), + updatedBy: PG.column(PG.brand(PG.text, UserId), { name: "updated_by" }), + }, + primaryKey: ["id"], + indexes: [PG.uniqueIndex("cost_connections_org_provider_idx", ["orgId", "provider"])], + tenantColumn: "orgId", +}) + +export type CostConnectionRow = PG.SelectRowOf diff --git a/packages/db/src/tables/index.ts b/packages/db/src/tables/index.ts index 553d7db627..6c31e0bdc1 100644 --- a/packages/db/src/tables/index.ts +++ b/packages/db/src/tables/index.ts @@ -71,6 +71,7 @@ import { PlanetscaleEvents, PlanetscaleIssueReceipts, } from "./planetscale-inventory" +import { CostConnections } from "./cost-connections" import { OpenRouterCostConnections } from "./openrouter-cost-connections" import { OrgOpenRouterKeys } from "./openrouter-keys" import { RailwayConnections, RailwayEnvironments } from "./railway" @@ -159,6 +160,7 @@ export * from "./org-ingest-sampling-policies" export * from "./org-recommendation-issues" export * from "./planetscale-connections" export * from "./planetscale-inventory" +export * from "./cost-connections" export * from "./openrouter-cost-connections" export * from "./openrouter-keys" export * from "./railway" @@ -248,6 +250,7 @@ export const allTables: ReadonlyArray> = [ OrgIngestKeys, OrgIngestSamplingPolicies, OpenRouterCostConnections, + CostConnections, OrgOpenRouterKeys, OrgBillingLinks, Sources, diff --git a/packages/domain/src/cost.ts b/packages/domain/src/cost.ts index b18d84f62e..96af48a814 100644 --- a/packages/domain/src/cost.ts +++ b/packages/domain/src/cost.ts @@ -24,7 +24,7 @@ export const CostDimension = Schema.Literals([ export type CostDimension = typeof CostDimension.Type /** Providers Maple reads cost from. One literal per built adapter. */ -export const CostProvider = Schema.Literals(["aws", "openrouter"]) +export const CostProvider = Schema.Literals(["aws", "openrouter", "cloudflare"]) export type CostProvider = typeof CostProvider.Type /** `billed`: the provider reported the money. `estimated`: usage times list price. */ diff --git a/packages/domain/src/generated/anticipated-error-identifiers.ts b/packages/domain/src/generated/anticipated-error-identifiers.ts index 9099172dd0..fb5356e9ca 100644 --- a/packages/domain/src/generated/anticipated-error-identifiers.ts +++ b/packages/domain/src/generated/anticipated-error-identifiers.ts @@ -11,6 +11,8 @@ export const ANTICIPATED_ERROR_IDENTIFIER_LIST: ReadonlyArray = [ "@maple/http/anomalies/AnomalyForbiddenError", "@maple/http/anomalies/AnomalyIncidentNotFoundError", "@maple/http/anomalies/AnomalyLinkedIssueNotFoundError", + "@maple/http/cost-connections/CostConnectionForbiddenError", + "@maple/http/cost-connections/CostConnectionValidationError", "@maple/http/data-sources/CatalogEntryNotFoundError", "@maple/http/data-sources/DataSourceInvalidError", "@maple/http/data-sources/DataSourceNotFoundError", diff --git a/packages/domain/src/http/cloudflare-cost.ts b/packages/domain/src/http/cloudflare-cost.ts new file mode 100644 index 0000000000..2811fe6f9b --- /dev/null +++ b/packages/domain/src/http/cloudflare-cost.ts @@ -0,0 +1,43 @@ +import { HttpApiEndpoint, HttpApiGroup } from "effect/http-api" +import { Schema } from "effect" +import { + CostConnectionForbiddenError, + CostConnectionPersistenceError, + CostConnectionRemoved, + CostConnectionSaved, + CostConnectionValidationError, +} from "./cost-connections" +import { SessionAuthorization } from "./current-tenant" +import { CloudCostDisabledError } from "./query-engine" + +/** Cloudflare tokens are 40 to about 60 characters; anything longer is not one. */ +export const CLOUDFLARE_COST_TOKEN_MAX_LENGTH = 256 + +export class CloudflareCostTokenRequest extends Schema.Class( + "CloudflareCostTokenRequest", +)({ + apiToken: Schema.String.check(Schema.isMaxLength(CLOUDFLARE_COST_TOKEN_MAX_LENGTH)), +}) {} + +/** Saving a token makes the org's Cloudflare cost source, replacing one keeps it, removing deletes it. */ +export class CloudflareCostApiGroup extends HttpApiGroup.make("cloudflareCost") + .add( + HttpApiEndpoint.put("set", "/", { + payload: CloudflareCostTokenRequest, + success: CostConnectionSaved, + error: [ + CostConnectionPersistenceError, + CostConnectionForbiddenError, + CostConnectionValidationError, + CloudCostDisabledError, + ], + }), + ) + .add( + HttpApiEndpoint.delete("remove", "/", { + success: CostConnectionRemoved, + error: [CostConnectionPersistenceError, CostConnectionForbiddenError], + }), + ) + .prefix("/internal/cost/cloudflare") + .middleware(SessionAuthorization) {} diff --git a/packages/domain/src/http/cost-connections.ts b/packages/domain/src/http/cost-connections.ts new file mode 100644 index 0000000000..91e24bf23d --- /dev/null +++ b/packages/domain/src/http/cost-connections.ts @@ -0,0 +1,57 @@ +import { Schema } from "effect" +import { HttpTaggedError } from "./error-policy" + +// What every provider read with a pasted credential answers when one is saved or removed. The +// Cost page shows these to the person saving it, so each carries a public body. + +/** The saved credential never comes back: only the account it reads. */ +export class CostConnectionSaved extends Schema.Class("CostConnectionSaved")({ + accountId: Schema.String, + accountLabel: Schema.String, +}) {} + +export class CostConnectionRemoved extends Schema.Class("CostConnectionRemoved")({ + removed: Schema.Boolean, +}) {} + +/** The database or the provider could not be reached; its own text can carry internals, so it stays server-side. */ +export class CostConnectionPersistenceError extends HttpTaggedError()( + "@maple/http/cost-connections/CostConnectionPersistenceError", + { message: Schema.String, cause: Schema.optionalKey(Schema.String) }, + { + status: 503, + code: "cost_connection_unavailable", + title: "Cost sources are temporarily unavailable", + message: "The credential could not be checked or saved right now. Try again in a few seconds.", + retry: "backoff", + recovery: "retry", + exposure: "redacted", + }, +) {} + +export class CostConnectionForbiddenError extends HttpTaggedError()( + "@maple/http/cost-connections/CostConnectionForbiddenError", + { message: Schema.String }, + { + status: 403, + code: "cost_connection_forbidden", + title: "Permission required", + retry: "never", + recovery: "request_access", + exposure: "public_message", + }, +) {} + +/** The credential is malformed, or the provider refused it. The message says which, and what to use instead. */ +export class CostConnectionValidationError extends HttpTaggedError()( + "@maple/http/cost-connections/CostConnectionValidationError", + { message: Schema.String }, + { + status: 400, + code: "cost_connection_invalid", + title: "Credential not accepted", + retry: "never", + recovery: "fix_request", + exposure: "public_message", + }, +) {} diff --git a/packages/domain/src/http/index.ts b/packages/domain/src/http/index.ts index e777872091..2311b78dd1 100644 --- a/packages/domain/src/http/index.ts +++ b/packages/domain/src/http/index.ts @@ -9,6 +9,8 @@ export * from "./dependency-scan" export * from "./ai-triage" export * from "./openrouter-key" export * from "./openrouter-cost" +export * from "./cost-connections" +export * from "./cloudflare-cost" export * from "./service-map-world" export * from "./incident-triage" export * from "./cancellation-review" diff --git a/packages/domain/src/http/internal-api.ts b/packages/domain/src/http/internal-api.ts index 6613fd5b0d..32958e59e6 100644 --- a/packages/domain/src/http/internal-api.ts +++ b/packages/domain/src/http/internal-api.ts @@ -6,6 +6,7 @@ import { AiTriageApiGroup } from "./ai-triage" import { BillingApiGroup } from "./billing" import { CodeReviewApiGroup } from "./code-review" import { DemoApiGroup } from "./demo" +import { CloudflareCostApiGroup } from "./cloudflare-cost" import { DependencyScanApiGroup } from "./dependency-scan" import { DigestApiGroup } from "./digest" import { ErrorsApiGroup } from "./errors" @@ -61,6 +62,7 @@ export class MapleInternalApi extends HttpApi.make("MapleInternalApi") .add(McpInternalApiGroup) .add(OpenRouterKeyApiGroup) .add(OpenRouterCostApiGroup) + .add(CloudflareCostApiGroup) .add(OrgClickHouseSettingsApiGroup) .add(OrganizationsApiGroup) .add(OrganizationCreationApiGroup) From b0a391b51602e227a7ee00768966052d46015c12 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Thu, 12 Nov 2026 23:46:16 +0100 Subject: [PATCH 2/9] feat(cost): add, replace and remove a Cloudflare source on the Cost page "Add source" lists Cloudflare. Picking it asks for an API token with the Billing: Read permission on one account, with a link to where tokens are made. A refused token stays in the dialog with the reason under the field. The source's row offers Replace token, which keeps the source and its history, and Remove. The pasted-credential step is one form described by data. Each provider added this way is an entry in credential-sources.ts: what its list row, form, row action, confirm and toasts say, and the commands that save and remove it. OpenRouter moved onto it with its wording and behaviour unchanged, and the page's messages name the provider they are about. The first-sync wait names Cloudflare beside AWS as bringing in months, the empty state mentions it, and the cost lab has Cloudflare cost, a source and a state for each dialog. --- apps/web/src/lab/cost-fixture.test.ts | 12 +- apps/web/src/lab/cost-fixture.ts | 98 +++++--- apps/web/src/lab/cost-lab.ts | 115 +++++++--- apps/web/src/page/cost/command.ts | 38 ++- apps/web/src/page/cost/cost.scene.test.ts | 229 +++++++++++++++++-- apps/web/src/page/cost/credential-sources.ts | 124 ++++++++++ apps/web/src/page/cost/message.ts | 16 +- apps/web/src/page/cost/model.ts | 11 +- apps/web/src/page/cost/providers.ts | 20 ++ apps/web/src/page/cost/update.ts | 69 +++--- apps/web/src/page/cost/view-add-source.ts | 205 +++++++++-------- apps/web/src/page/cost/view-sources.ts | 21 +- apps/web/src/page/cost/view.ts | 29 ++- docs/cloud-cost-plan.md | 87 +++++-- 14 files changed, 797 insertions(+), 277 deletions(-) create mode 100644 apps/web/src/page/cost/credential-sources.ts diff --git a/apps/web/src/lab/cost-fixture.test.ts b/apps/web/src/lab/cost-fixture.test.ts index 884a79b808..9e2c811994 100644 --- a/apps/web/src/lab/cost-fixture.test.ts +++ b/apps/web/src/lab/cost-fixture.test.ts @@ -8,30 +8,30 @@ const openRouterOnly = { days: 30, filters: { provider: ["openrouter"] } } describe("cost lab fixture", () => { test("a removed OpenRouter source leaves every read, also under a filter naming it", () => { const before = labTotals(window).cost - const after = labTotals({ ...window, withoutOpenRouter: true }).cost + const after = labTotals({ ...window, removed: ["openrouter"] }).cost expect(after).toBeGreaterThan(0) expect(after).toBeLessThan(before) expect( Arr.map( - labBreakdown({ ...window, withoutOpenRouter: true, groupBy: "provider", sortBy: "cost" }) + labBreakdown({ ...window, removed: ["openrouter"], groupBy: "provider", sortBy: "cost" }) .rows, (row) => row.key, ), - ).toEqual(["aws"]) + ).toEqual(["aws", "cloudflare"]) // Filtered to OpenRouter there is nothing left: AWS cost must not show under that filter. expect(labTotals(openRouterOnly).cost).toBeGreaterThan(0) - expect(labTotals({ ...openRouterOnly, withoutOpenRouter: true }).cost).toBe(0) + expect(labTotals({ ...openRouterOnly, removed: ["openrouter"] }).cost).toBe(0) expect( labBreakdown({ ...openRouterOnly, - withoutOpenRouter: true, + removed: ["openrouter"], groupBy: "cloud_service", sortBy: "cost", }).rows, ).toEqual([]) expect( - labTimeseries({ ...openRouterOnly, withoutOpenRouter: true, groupBy: "provider", limit: 8 }) + labTimeseries({ ...openRouterOnly, removed: ["openrouter"], groupBy: "provider", limit: 8 }) .series, ).toEqual([]) }) diff --git a/apps/web/src/lab/cost-fixture.ts b/apps/web/src/lab/cost-fixture.ts index fcf71aed1f..392f6f0b95 100644 --- a/apps/web/src/lab/cost-fixture.ts +++ b/apps/web/src/lab/cost-fixture.ts @@ -8,6 +8,7 @@ import { } from "@maple/domain/http" import { Array as Arr, Number as Num, Order, Record as Rec, Schema, String as Str } from "effect" import type { CostGroupBy } from "../page/cost/derive" +import type { CredentialProvider } from "../page/cost/providers" // Synthetic cost for `/lab/cost`: invented services, regions and amounts on one made-up account. // Nothing here is read by the product. @@ -40,8 +41,6 @@ const line = ( drift, }) -/** A saved OpenRouter key, as OpenRouter masks it. */ -export const LAB_OPENROUTER_KEY = "sk-or-v1-4f2...c81" /** The OpenRouter account the lab's source reads: a personal account's user id. */ const LAB_OPENROUTER_ACCOUNT = "user_2labExampleAccount0000000000" @@ -56,6 +55,26 @@ const modelLine = (model: string, servedBy: string, daily: number, drift: number drift, }) +/** The Cloudflare account the lab's source reads. */ +const LAB_CLOUDFLARE_ACCOUNT = "1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d" + +const productLine = ( + product: string, + category: string, + metric: string, + daily: number, + drift: number, +): Line => ({ + cloud_service: product, + provider: "cloudflare", + service_category: category, + region: "global", + usage_type: metric, + account: LAB_CLOUDFLARE_ACCOUNT, + daily, + drift, +}) + /** `daily` is the charge per day today; `drift` how much it grew or shrank over the last 30 days. */ const LINES: ReadonlyArray = [ line("Example Compute", "Compute", "north-1", "north-1:instance-hours", 61.2, 0.18), @@ -79,6 +98,10 @@ const LINES: ReadonlyArray = [ modelLine("example/large-model", "Example Cloud", 6.2, 0.1), modelLine("example/fast-model", "Example Inference", 12.9, -0.15), modelLine("example/embedding-model", "Example Labs", 1.8, 0.0), + productLine("Workers", "Compute", "Workers Requests", 9.4, 0.25), + productLine("Durable Objects", "Compute", "Durable Objects Compute Duration", 5.1, 0.12), + productLine("R2", "Storage", "R2 Storage", 3.6, 0.05), + productLine("D1", "Databases", "D1 Rows Read", 1.2, -0.08), ] /** The day `offset` days before the lab's today, as `YYYY-MM-DD`. */ @@ -104,15 +127,14 @@ const matches = (entry: Line, filters: CostFilters | undefined): boolean => type Request = Readonly<{ days: number filters: CostFilters | undefined - /** The lab's reader removed OpenRouter: its lines are in no read, whatever the filters say. */ - withoutOpenRouter?: boolean + /** Providers the lab's reader removed: their lines are in no read, whatever the filters say. */ + removed?: ReadonlyArray }> /** Each matching line with its charge on every day of the window (oldest first) and of the period before. */ const windowOf = (request: Request) => Arr.flatMap(LINES, (entry, index) => - matches(entry, request.filters) && - !(request.withoutOpenRouter === true && entry.provider === "openrouter") + matches(entry, request.filters) && !Arr.contains(request.removed ?? [], entry.provider) ? [ { entry, @@ -248,36 +270,52 @@ const openRouterSource = source({ requestsThisMonth: 0, }) +/** The Cloudflare source: history back to the eleventh month before this one, and no billed requests. */ +const cloudflareSource = source({ + id: "0190f2b3-0000-7000-8000-00000000c061", + provider: "cloudflare", + accountId: LAB_CLOUDFLARE_ACCOUNT, + backfillDate: "2025-12-01", + lastSyncedAt: LAB_NOW_MS - 2 * 3_600_000, + requestsThisMonth: 0, +}) + +const CREDENTIAL_SOURCES = { openrouter: openRouterSource, cloudflare: cloudflareSource } as const + +/** What the lab's reader did to each pasted-credential source since the state loaded; null for nothing. */ +export type LabSourceChanges = Readonly> + /** - * `sources` after the lab's reader added OpenRouter (true: it waits for its first sync) or removed - * it (false); null leaves them as the state has them. + * `sources` after the lab's reader added a provider (true: it waits for its first sync) or removed + * it (false); null leaves it as the state has it. */ export const labSourcesWith = ( sources: CostSourcesResponse, - openRouter: boolean | null, -): CostSourcesResponse => { - if (openRouter === null) return sources - const others = Arr.filter(sources.sources, (entry) => entry.provider !== "openrouter") - if (!openRouter) return new CostSourcesResponse({ sources: others }) - return others.length === sources.sources.length - ? decodeSources({ - sources: [ - ...Schema.encodeSync(CostSourcesResponse)(sources).sources, - { - ...openRouterSource, - status: "pending", - watermarkDate: null, - backfillDate: null, - historyComplete: false, - lastSyncedAt: null, - }, - ], - }) - : sources -} + changes: LabSourceChanges, +): CostSourcesResponse => + Arr.reduce(Rec.toEntries(changes), sources, (current, [provider, added]) => { + if (added === null) return current + const others = Arr.filter(current.sources, (entry) => entry.provider !== provider) + if (!added) return new CostSourcesResponse({ sources: others }) + return others.length === current.sources.length + ? decodeSources({ + sources: [ + ...Schema.encodeSync(CostSourcesResponse)(current).sources, + { + ...CREDENTIAL_SOURCES[provider], + status: "pending", + watermarkDate: null, + backfillDate: null, + historyComplete: false, + lastSyncedAt: null, + }, + ], + }) + : current + }) export const LAB_SOURCES = { - synced: decodeSources({ sources: [source({}), openRouterSource] }), + synced: decodeSources({ sources: [source({}), openRouterSource, cloudflareSource] }), none: decodeSources({ sources: [] }), pending: decodeSources({ sources: [ diff --git a/apps/web/src/lab/cost-lab.ts b/apps/web/src/lab/cost-lab.ts index 8023445b12..88350369e2 100644 --- a/apps/web/src/lab/cost-lab.ts +++ b/apps/web/src/lab/cost-lab.ts @@ -1,8 +1,7 @@ -import { OpenRouterCostKeyRemoved, OpenRouterCostKeySaved } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" import { toLoadFailure } from "@maple/views/data/load-failure" import * as Field from "@maple/views/ui/field" -import { Array as Arr, Effect, Option, Result, Schema } from "effect" +import { Array as Arr, Effect, Option, Record as Rec, Result, Schema } from "effect" import { Command, type Update } from "foldkit" import type { Html, HtmlBuilder } from "foldkit/html" import { defineMessageUnion } from "foldkit/message" @@ -14,23 +13,26 @@ import { FetchCostTimeseries, FetchCostTotals, ReadCostGrants, + RemoveCloudflareCostToken, RemoveOpenRouterCostKey, ResolveWindow, + SaveCloudflareCostToken, SaveOpenRouterCostKey, TimeseriesInput, WindowInput, } from "../page/cost/command" import { Message as CostMessage } from "../page/cost/message" import { Model as CostModel, type Search } from "../page/cost/model" +import type { CredentialProvider } from "../page/cost/providers" import * as Cost from "../page/cost/update" import { view as costView } from "../page/cost/view" import type { Resources } from "../platform/resources" import { urlToAppRoute } from "../route" import { LAB_NOW_MS, - LAB_OPENROUTER_KEY, LAB_SOURCES, LAB_TODAY, + type LabSourceChanges, labBreakdown, labSourcesWith, labTimeseries, @@ -57,6 +59,10 @@ export const LabState = Schema.Literals([ "key-refused", "replace-key", "remove-source", + "add-cloudflare", + "token-refused", + "replace-token", + "remove-cloudflare", "member", "member-onramp", ]) @@ -74,8 +80,11 @@ export const flags: Effect.Effect = Effect.sync(() => ({ export const Model = Schema.Struct({ state: LabState, - /** OpenRouter added (true) or removed (false) since the state loaded; null before either. */ - openRouter: Schema.NullOr(Schema.Boolean), + /** Each pasted-credential source added (true) or removed (false) since the state loaded; null before either. */ + changes: Schema.Struct({ + openrouter: Schema.NullOr(Schema.Boolean), + cloudflare: Schema.NullOr(Schema.Boolean), + }), cost: CostModel, }) export type Model = typeof Model.Type @@ -84,8 +93,10 @@ export const Message = defineMessageUnion({ GotCostMessage: { message: CostMessa export type Message = typeof Message.Type type Return = Update.Return -/** What the lab answers from: the state it loaded in, and what the reader did to OpenRouter since. */ -type World = Readonly<{ state: LabState; openRouter: boolean | null }> +/** What the lab answers from: the state it loaded in, and what the reader did to its sources since. */ +type World = Readonly<{ state: LabState; changes: LabSourceChanges }> + +const NO_CHANGES: LabSourceChanges = { openrouter: null, cloudflare: null } type CostCommand = Command.Command const searchFor = (state: LabState): Search => @@ -101,7 +112,9 @@ const startsEmpty = (state: LabState): boolean => state === "member-onramp" || state === "add-source" || state === "add-openrouter" || - state === "key-refused" + state === "key-refused" || + state === "add-cloudflare" || + state === "token-refused" /** What the reader did before the state's screenshot: the clicks that open its dialog. */ const openingOf = (state: LabState): ReadonlyArray => { @@ -109,20 +122,35 @@ const openingOf = (state: LabState): ReadonlyArray => { case "add-source": return [CostMessage.ClickedAddSource()] case "add-openrouter": - return [CostMessage.ClickedAddSource(), CostMessage.ClickedAddOpenRouter()] + return [CostMessage.ClickedAddSource(), CostMessage.PickedProvider({ provider: "openrouter" })] case "key-refused": return [ CostMessage.ClickedAddSource(), - CostMessage.ClickedAddOpenRouter(), + CostMessage.PickedProvider({ provider: "openrouter" }), CostMessage.GotKeyDraftMessage({ message: Field.Message.ChangedValue({ value: "sk-or-v1-0123456789abcdef0123456789" }), }), CostMessage.SubmittedKey(), ] case "replace-key": - return [CostMessage.ClickedReplaceKey()] + return [CostMessage.ClickedReplaceKey({ provider: "openrouter" })] case "remove-source": - return [CostMessage.ClickedRemoveSource()] + return [CostMessage.ClickedRemoveSource({ provider: "openrouter" })] + case "add-cloudflare": + return [CostMessage.ClickedAddSource(), CostMessage.PickedProvider({ provider: "cloudflare" })] + case "token-refused": + return [ + CostMessage.ClickedAddSource(), + CostMessage.PickedProvider({ provider: "cloudflare" }), + CostMessage.GotKeyDraftMessage({ + message: Field.Message.ChangedValue({ value: "cfut_0123456789abcdefghij0123456789" }), + }), + CostMessage.SubmittedKey(), + ] + case "replace-token": + return [CostMessage.ClickedReplaceKey({ provider: "cloudflare" })] + case "remove-cloudflare": + return [CostMessage.ClickedRemoveSource({ provider: "cloudflare" })] // Without Clerk the page starts with every permission: the member states take them away. case "member": case "member-onramp": @@ -138,6 +166,11 @@ const refusedKey = { "This is a regular API key. Reading spend needs a management key, which you create under Settings, Management keys in OpenRouter.", } +const refusedToken = { + ...toLoadFailure("refused"), + message: "This token can read 3 accounts. Create one limited to a single account.", +} + /** The days a preset spans, for the fixture's window; a custom range reads as the default. */ const DAYS = new Map([ ["1w", 7], @@ -167,6 +200,9 @@ const windowArgs = argsOf(WindowInput) const timeseriesArgs = argsOf(TimeseriesInput) const breakdownArgs = argsOf(BreakdownInput) +const removedAnswer = (provider: CredentialProvider) => + CostMessage.CompletedRemoveKey({ provider, result: Result.succeed(undefined) }) + /** A read's answer: the fixture's, or the outage in the `error` state. */ const settled = (state: LabState, value: () => A): Result.Result => state === "error" ? Result.fail(failure) : Result.succeed(value()) @@ -188,13 +224,15 @@ const answerOf = (world: World, search: Search, command: CostCommand): Option.Op const filtersOf = (filters: WindowInput["filters"]) => state === "empty-window" ? { account: ["000000000000"] } : filters // A removed source's cost is not read any more, under any filter. - const withoutOpenRouter = world.openRouter === false + const removed = Arr.flatMap(Rec.toEntries(world.changes), ([provider, added]) => + added === false ? [provider] : [], + ) switch (command.name) { case FetchCostSources.name: return Option.map(keyArgs(command.args), ({ key }) => CostMessage.CompletedLoadSources({ key, - result: Result.succeed(labSourcesWith(sourcesFor(state), world.openRouter)), + result: Result.succeed(labSourcesWith(sourcesFor(state), world.changes)), }), ) case FetchCostTotals.name: @@ -202,7 +240,7 @@ const answerOf = (world: World, search: Search, command: CostCommand): Option.Op CostMessage.CompletedLoadTotals({ key, result: settled(state, () => - labTotals({ days, filters: filtersOf(input.filters), withoutOpenRouter }), + labTotals({ days, filters: filtersOf(input.filters), removed }), ), }), ) @@ -214,7 +252,7 @@ const answerOf = (world: World, search: Search, command: CostCommand): Option.Op labTimeseries({ days, filters: filtersOf(input.filters), - withoutOpenRouter, + removed, groupBy: input.groupBy, limit: input.limit, }), @@ -229,7 +267,7 @@ const answerOf = (world: World, search: Search, command: CostCommand): Option.Op labBreakdown({ days, filters: filtersOf(input.filters), - withoutOpenRouter, + removed, groupBy: input.groupBy, sortBy: input.sortBy, }), @@ -245,33 +283,36 @@ const answerOf = (world: World, search: Search, command: CostCommand): Option.Op case SaveOpenRouterCostKey.name: return Option.some( CostMessage.CompletedSaveKey({ - result: - state === "key-refused" - ? Result.fail(refusedKey) - : Result.succeed(new OpenRouterCostKeySaved({ keyLabel: LAB_OPENROUTER_KEY })), + provider: "openrouter", + result: state === "key-refused" ? Result.fail(refusedKey) : Result.succeed(undefined), }), ) - case RemoveOpenRouterCostKey.name: + case SaveCloudflareCostToken.name: return Option.some( - CostMessage.CompletedRemoveKey({ - result: Result.succeed(new OpenRouterCostKeyRemoved({ removed: true })), + CostMessage.CompletedSaveKey({ + provider: "cloudflare", + result: state === "token-refused" ? Result.fail(refusedToken) : Result.succeed(undefined), }), ) + case RemoveOpenRouterCostKey.name: + return Option.some(removedAnswer("openrouter")) + case RemoveCloudflareCostToken.name: + return Option.some(removedAnswer("cloudflare")) default: return Option.none() } } type Step = Readonly<{ model: CostModel; commands: ReadonlyArray }> -type Settled = Step & Pick +type Settled = Step & Pick -/** A saved key adds OpenRouter to the sources the lab answers with next, a removal takes it out. */ -const openRouterAfter = (openRouter: boolean | null, answers: ReadonlyArray) => - Arr.reduce(answers, openRouter, (current, answer) => +/** A saved credential adds its provider to the sources the lab answers with next, a removal takes it out. */ +const changesAfter = (changes: LabSourceChanges, answers: ReadonlyArray): LabSourceChanges => + Arr.reduce(answers, changes, (current, answer) => answer._tag === "CompletedSaveKey" && Result.isSuccess(answer.result) - ? true + ? { ...current, [answer.provider]: true } : answer._tag === "CompletedRemoveKey" && Result.isSuccess(answer.result) - ? false + ? { ...current, [answer.provider]: false } : current, ) @@ -284,7 +325,7 @@ const settle = (world: World, step: Step): Settled => { Option.toArray(answerOf(world, step.model.search, command)), ) const kept = keepLocal(step.commands) - if (answers.length === 0) return { model: step.model, commands: kept, openRouter: world.openRouter } + if (answers.length === 0) return { model: step.model, commands: kept, changes: world.changes } const next = Arr.reduce( answers, { model: step.model, commands: Arr.empty() }, @@ -293,7 +334,7 @@ const settle = (world: World, step: Step): Settled => { return { model: result.model, commands: [...acc.commands, ...(result.commands ?? [])] } }, ) - const rest = settle({ ...world, openRouter: openRouterAfter(world.openRouter, answers) }, next) + const rest = settle({ ...world, changes: changesAfter(world.changes, answers) }, next) return { ...rest, commands: [...kept, ...rest.commands] } } @@ -312,18 +353,18 @@ const searchOfUrl = (url: string): Search => export const init = (flags: Flags): Return => { const entered = Cost.init({ search: searchFor(flags.state) }, LAB_CONTEXT) - const world: World = { state: flags.state, openRouter: null } + const world: World = { state: flags.state, changes: NO_CHANGES } const loaded = settle(world, { model: entered.model, commands: entered.commands ?? [] }) const ready = Arr.reduce(openingOf(flags.state), loaded, (step, message) => { const next = Cost.update(step.model, message, LAB_CONTEXT) const settled = settle( - { ...world, openRouter: step.openRouter }, + { ...world, changes: step.changes }, { model: next.model, commands: next.commands ?? [] }, ) return { ...settled, commands: [...step.commands, ...settled.commands] } }) return { - model: { state: flags.state, openRouter: ready.openRouter, cost: ready.model }, + model: { state: flags.state, changes: ready.changes, cost: ready.model }, commands: lift(ready.commands), } } @@ -338,11 +379,11 @@ export const update = (model: Model, message: Message): Return => ? Cost.routeChanged(step.model, { search: searchOfUrl(out.url) }, LAB_CONTEXT) : { model: step.model } const ready = settle( - { state: model.state, openRouter: model.openRouter }, + { state: model.state, changes: model.changes }, { model: moved.model, commands: [...(step.commands ?? []), ...(moved.commands ?? [])] }, ) return { - model: { ...model, openRouter: ready.openRouter, cost: ready.model }, + model: { ...model, changes: ready.changes, cost: ready.model }, commands: lift(ready.commands), } }, diff --git a/apps/web/src/page/cost/command.ts b/apps/web/src/page/cost/command.ts index f3c9c32d96..2db25b2cfb 100644 --- a/apps/web/src/page/cost/command.ts +++ b/apps/web/src/page/cost/command.ts @@ -1,4 +1,9 @@ -import { CostBreakdownSortBy, CostFilters, OpenRouterKeyUpdateRequest } from "@maple/domain/http" +import { + CloudflareCostTokenRequest, + CostBreakdownSortBy, + CostFilters, + OpenRouterKeyUpdateRequest, +} from "@maple/domain/http" import { reloadSpin } from "@maple/views/data/timing" import { settleTo } from "@maple/views/data/load-failure" import { @@ -91,6 +96,8 @@ export const ReadCostGrants = Command.define("ReadCostGrants", { execute: Effect.map(readGrants, (grants) => Message.CompletedReadGrants({ grants })), }) +// A credential's save and removal answer with their provider: the page reads only whether they worked. + /** Saves the org's OpenRouter management key, which makes (or keeps) its cost source. */ export const SaveOpenRouterCostKey = Command.define("SaveOpenRouterCostKey", { args: { apiKey: Schema.String }, @@ -98,12 +105,37 @@ export const SaveOpenRouterCostKey = Command.define("SaveOpenRouterCostKey", { execute: ({ apiKey }) => Effect.flatMap(MapleInternalApiClient, (client) => client.openRouterCost.set({ payload: new OpenRouterKeyUpdateRequest({ apiKey }) }), - ).pipe(settleTo((result) => Message.CompletedSaveKey({ result }))), + ).pipe( + Effect.asVoid, + settleTo((result) => Message.CompletedSaveKey({ provider: "openrouter", result })), + ), }) export const RemoveOpenRouterCostKey = Command.define("RemoveOpenRouterCostKey", { messages: [Message.CompletedRemoveKey], execute: Effect.flatMap(MapleInternalApiClient, (client) => client.openRouterCost.remove()).pipe( - settleTo((result) => Message.CompletedRemoveKey({ result })), + Effect.asVoid, + settleTo((result) => Message.CompletedRemoveKey({ provider: "openrouter", result })), + ), +}) + +/** Saves the org's Cloudflare API token, which makes (or keeps) its cost source. */ +export const SaveCloudflareCostToken = Command.define("SaveCloudflareCostToken", { + args: { apiToken: Schema.String }, + messages: [Message.CompletedSaveKey], + execute: ({ apiToken }) => + Effect.flatMap(MapleInternalApiClient, (client) => + client.cloudflareCost.set({ payload: new CloudflareCostTokenRequest({ apiToken }) }), + ).pipe( + Effect.asVoid, + settleTo((result) => Message.CompletedSaveKey({ provider: "cloudflare", result })), + ), +}) + +export const RemoveCloudflareCostToken = Command.define("RemoveCloudflareCostToken", { + messages: [Message.CompletedRemoveKey], + execute: Effect.flatMap(MapleInternalApiClient, (client) => client.cloudflareCost.remove()).pipe( + Effect.asVoid, + settleTo((result) => Message.CompletedRemoveKey({ provider: "cloudflare", result })), ), }) diff --git a/apps/web/src/page/cost/cost.scene.test.ts b/apps/web/src/page/cost/cost.scene.test.ts index 61c9640f8a..f0ec3cc96b 100644 --- a/apps/web/src/page/cost/cost.scene.test.ts +++ b/apps/web/src/page/cost/cost.scene.test.ts @@ -5,8 +5,6 @@ import { CostSourcesResponse, CostTimeseriesResponse, CostTotalsResponse, - OpenRouterCostKeyRemoved, - OpenRouterCostKeySaved, } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" import { Option, Record as Rec, Result, Schema, Array as Arr, String as Str } from "effect" @@ -183,7 +181,7 @@ describe("cost page", () => { expectView(text("Add a cost source")).toExist(), expectView(role("link", { name: /^AWS/ })).toHaveAttr("href", "/sources/new?integration=aws"), expectView(text(/AWS bills USD.0\.01 for each request Maple makes/)).toExist(), - expectView(text(/OpenRouter requests are free/)).toExist(), + expectView(text(/OpenRouter and Cloudflare requests are free/)).toExist(), // An account already connected turns its Cost module on from the AWS integration. expectView( role("link", { name: "Turn on Cost for an AWS account that is connected" }), @@ -193,7 +191,7 @@ describe("cost page", () => { ) scene( { update: pageUpdate, view }, - given(pageUpdate(opened, Message.ClickedAddOpenRouter()).model), + given(pageUpdate(opened, Message.PickedProvider({ provider: "openrouter" })).model), resolveTheme, dialogMounted, expectView(text("Add OpenRouter")).toExist(), @@ -211,7 +209,7 @@ describe("cost page", () => { test("a saved key is sent trimmed; a refusal stays under the field until the key changes", () => { const form = pageUpdate( pageUpdate(entered({}, []).model, Message.ClickedAddSource()).model, - Message.ClickedAddOpenRouter(), + Message.PickedProvider({ provider: "openrouter" }), ).model const typed = (model: Model, value: string) => pageUpdate(model, Message.GotKeyDraftMessage({ message: Field.Message.ChangedValue({ value }) })) @@ -236,6 +234,7 @@ describe("cost page", () => { const refused = pageUpdate( saving.model, Message.CompletedSaveKey({ + provider: "openrouter", result: Result.fail({ ...toLoadFailure("refused"), message: "This is a regular API key. Reading spend needs a management key.", @@ -258,7 +257,7 @@ describe("cost page", () => { test("a saved key closes the dialog, says when the sync runs and reads the sources again", () => { const form = pageUpdate( pageUpdate(entered({}, []).model, Message.ClickedAddSource()).model, - Message.ClickedAddOpenRouter(), + Message.PickedProvider({ provider: "openrouter" }), ).model const saving = pageUpdate( pageUpdate( @@ -271,9 +270,7 @@ describe("cost page", () => { ).model const saved = pageUpdate( saving, - Message.CompletedSaveKey({ - result: Result.succeed(new OpenRouterCostKeySaved({ keyLabel: "sk-or-v1-man...789" })), - }), + Message.CompletedSaveKey({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(saved.model.addDialog.isOpen).toBe(false) // The key does not outlive the dialog. @@ -315,7 +312,7 @@ describe("cost page", () => { ) // Replace opens the key form with no providers to go back to. - const replacing = pageUpdate(model, Message.ClickedReplaceKey()).model + const replacing = pageUpdate(model, Message.ClickedReplaceKey({ provider: "openrouter" })).model scene( { update: pageUpdate, view }, given(replacing), @@ -334,15 +331,13 @@ describe("cost page", () => { expectView(role("button", { name: /^OpenRouter/ })).not.toExist(), ) - const confirming = pageUpdate(model, Message.ClickedRemoveSource()).model + const confirming = pageUpdate(model, Message.ClickedRemoveSource({ provider: "openrouter" })).model expect(confirming.removeDialog.isOpen).toBe(true) const removing = pageUpdate(confirming, Message.ClickedConfirmRemove()) expect(names(removing.commands)).toEqual(["RemoveOpenRouterCostKey"]) const removed = pageUpdate( removing.model, - Message.CompletedRemoveKey({ - result: Result.succeed(new OpenRouterCostKeyRemoved({ removed: true })), - }), + Message.CompletedRemoveKey({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(removed.model.removeDialog.isOpen).toBe(false) // The reads name the org's sources: without reading them again the removed source's cost stays. @@ -387,9 +382,7 @@ describe("cost page", () => { const staleKey = reloading.sources.key ?? "" const afterWrite = pageUpdate( reloading, - Message.CompletedRemoveKey({ - result: Result.succeed(new OpenRouterCostKeyRemoved({ removed: true })), - }), + Message.CompletedRemoveKey({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(names(afterWrite.commands)).toContain("FetchCostSources") const stale = pageUpdate( @@ -413,17 +406,192 @@ describe("cost page", () => { ).model, Message.SubmittedKey(), ).model, + Message.CompletedSaveKey({ provider: "openrouter", result: Result.succeed(undefined) }), + ) + expect(replaced.outMessage).toEqual( + PageOutMessage.RequestedToast({ + type: "success", + title: "OpenRouter key replaced", + description: "The next sync runs within a few minutes.", + }), + ) + }) + + test("Cloudflare is added by pasting an API token, and a refused one says why under the field", () => { + const opened = pageUpdate(entered({}, []).model, Message.ClickedAddSource()).model + scene( + { update: pageUpdate, view }, + given(opened), + resolveTheme, + dialogMounted, + expectView(role("button", { name: /^Cloudflare/ })).toExist(), + expectView(text("Daily charges by product, plus plan fees")).toExist(), + ) + const form = pageUpdate(opened, Message.PickedProvider({ provider: "cloudflare" })).model + scene( + { update: pageUpdate, view }, + given(form), + resolveTheme, + dialogMounted, + expectView(text("Add Cloudflare")).toExist(), + expectView(text("API token")).toExist(), + expectView(role("link", { name: "Create a custom token" })).toHaveAttr( + "href", + "https://dash.cloudflare.com/profile/api-tokens", + ), + expectView(text(/with the Billing: Read permission on one account/)).toExist(), + // The other provider's form is not this one. + expectView(text("Management key")).not.toExist(), + expectView(role("button", { name: "Add source" })).toBeDisabled(), + expectView(role("button", { name: "Back" })).toExist(), + ) + const typed = (model: Model, value: string) => + pageUpdate(model, Message.GotKeyDraftMessage({ message: Field.Message.ChangedValue({ value }) })) + .model + + const saving = pageUpdate( + typed(form, " cfut_0123456789abcdefghij0123456789 "), + Message.SubmittedKey(), + ) + expect(saving.commands).toMatchObject([ + { name: "SaveCloudflareCostToken", args: { apiToken: "cfut_0123456789abcdefghij0123456789" } }, + ]) + const oversized = pageUpdate(typed(form, `cfut_${Str.repeat(300)("a")}`), Message.SubmittedKey()) + expect(names(oversized.commands)).toEqual([]) + expect(oversized.model.keyError).toBe("That is too long to be a Cloudflare API token.") + + const refused = pageUpdate( + saving.model, Message.CompletedSaveKey({ - result: Result.succeed(new OpenRouterCostKeySaved({ keyLabel: "sk-or-v1-man...789" })), + provider: "cloudflare", + result: Result.fail({ + ...toLoadFailure("refused"), + message: "This token can read 3 accounts. Create one limited to a single account.", + }), + }), + ).model + expect(refused.addDialog.isOpen).toBe(true) + scene( + { update: pageUpdate, view }, + given(refused), + resolveTheme, + dialogMounted, + expectView(role("alert")).toHaveText( + "This token can read 3 accounts. Create one limited to a single account.", + ), + ) + + const saved = pageUpdate( + saving.model, + Message.CompletedSaveKey({ provider: "cloudflare", result: Result.succeed(undefined) }), + ) + expect(saved.model.addDialog.isOpen).toBe(false) + expect(saved.model.keyDraft.value).toBe("") + expect(names(saved.commands)).toContain("FetchCostSources") + expect(saved.outMessage).toEqual( + PageOutMessage.RequestedToast({ + type: "success", + title: "Cloudflare added", + description: "The first sync runs within a few minutes.", }), ) + }) + + test("a Cloudflare source has its token replaced or is removed, and the OpenRouter source beside it stays", () => { + const openRouter = source({ + id: "0190f2b3-0000-7000-8000-00000000c060", + provider: "openrouter", + accountId: "user_2openrouterAccount", + requestsThisMonth: 0, + }) + const cloudflare = source({ + id: "0190f2b3-0000-7000-8000-00000000c061", + provider: "cloudflare", + accountId: "1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d", + backfillDate: "2025-12-01", + requestsThisMonth: 0, + }) + const model = answered(entered({}, [openRouter, cloudflare]).model) + scene( + { update: pageUpdate, view }, + given(model), + resolveTheme, + expectView(text("Cloudflare")).toExist(), + expectView(text("1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d")).toExist(), + // Neither provider bills for requests. + expectView(text(/provider requests/)).not.toExist(), + expectView(role("button", { name: "Replace token" })).toExist(), + expectView(role("button", { name: "Replace key" })).toExist(), + ) + + const replacing = pageUpdate(model, Message.ClickedReplaceKey({ provider: "cloudflare" })).model + scene( + { update: pageUpdate, view }, + given(replacing), + resolveTheme, + dialogMounted, + expectView(text("Replace the Cloudflare token")).toExist(), + expectView(text(/Use a token for the same Cloudflare account/)).toExist(), + expectView(role("button", { name: "Back" })).not.toExist(), + ) + const replaced = pageUpdate( + pageUpdate( + pageUpdate( + replacing, + Message.GotKeyDraftMessage({ + message: Field.Message.ChangedValue({ value: "cfut_0123456789abcdefghij0123456789" }), + }), + ).model, + Message.SubmittedKey(), + ).model, + Message.CompletedSaveKey({ provider: "cloudflare", result: Result.succeed(undefined) }), + ) expect(replaced.outMessage).toEqual( PageOutMessage.RequestedToast({ type: "success", - title: "OpenRouter key replaced", + title: "Cloudflare token replaced", description: "The next sync runs within a few minutes.", }), ) + + // Both exist: the list offers neither again. + scene( + { update: pageUpdate, view }, + given(pageUpdate(model, Message.ClickedAddSource()).model), + resolveTheme, + dialogMounted, + expectView(role("button", { name: /^Cloudflare/ })).not.toExist(), + expectView(role("button", { name: /^OpenRouter/ })).not.toExist(), + expectView(text("Replace its token in the Sources panel")).toExist(), + ) + + const confirming = pageUpdate(model, Message.ClickedRemoveSource({ provider: "cloudflare" })).model + scene( + { update: pageUpdate, view }, + given(confirming), + resolveTheme, + dialogMounted, + expectView(text("Remove the Cloudflare source?")).toExist(), + ) + const removing = pageUpdate(confirming, Message.ClickedConfirmRemove()) + expect(names(removing.commands)).toEqual(["RemoveCloudflareCostToken"]) + const removed = pageUpdate( + removing.model, + Message.CompletedRemoveKey({ provider: "cloudflare", result: Result.succeed(undefined) }), + ) + expect(removed.outMessage).toEqual( + PageOutMessage.RequestedToast({ type: "success", title: "Cloudflare removed", description: "" }), + ) + scene( + { update: pageUpdate, view }, + given(removed.model), + resolveTheme, + dialogMounted, + expectView(text("1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d")).not.toExist(), + expectView(role("button", { name: "Replace token" })).not.toExist(), + // The other provider's source is still listed. + expectView(text("user_2openrouterAccount")).toExist(), + ) }) test("a view filtered to a provider compares with the period before when that provider's history reaches it", () => { @@ -499,6 +667,7 @@ describe("cost page", () => { test("grouped by provider, a row reads the provider's name and filters by its id", () => { expect(keyLabel("provider", "openrouter")).toBe("OpenRouter") expect(keyLabel("provider", "aws")).toBe("AWS") + expect(keyLabel("provider", "cloudflare")).toBe("Cloudflare") // A provider this build does not know still shows, as the rows name it. expect(keyLabel("provider", "somecloud")).toBe("somecloud") expect(requestFilters({ provider: ["openrouter"] })).toEqual({ provider: ["openrouter"] }) @@ -572,6 +741,28 @@ describe("cost page", () => { expectView(text(/AWS brings in this month and the last/)).toExist(), expectView(text(/OpenRouter brings in its last four weeks/)).toExist(), ) + // Cloudflare keeps a year: alone it reads like AWS, and beside AWS both are named. + const cloudflare = source({ + ...unsynced, + id: "0190f2b3-0000-7000-8000-00000000c061", + provider: "cloudflare", + status: "pending", + }) + scene( + { update: pageUpdate, view }, + given(entered({}, [cloudflare]).model), + resolveTheme, + expectView( + text(/It brings in this month and the last, and older months fill in after that/), + ).toExist(), + ) + scene( + { update: pageUpdate, view }, + given(entered({}, [failingAws, cloudflare, openRouter]).model), + resolveTheme, + expectView(text(/AWS and Cloudflare bring in this month and the last/)).toExist(), + expectView(text(/OpenRouter brings in its last four weeks/)).toExist(), + ) }) test("a synced source asks for the totals, the daily series and the breakdown of the window", () => { diff --git a/apps/web/src/page/cost/credential-sources.ts b/apps/web/src/page/cost/credential-sources.ts new file mode 100644 index 0000000000..7a5ce35132 --- /dev/null +++ b/apps/web/src/page/cost/credential-sources.ts @@ -0,0 +1,124 @@ +import { CLOUDFLARE_COST_TOKEN_MAX_LENGTH, OPENROUTER_KEY_MAX_LENGTH } from "@maple/domain/http" +import type { Command } from "foldkit" +import type { Resources } from "../../platform/resources" +import { + RemoveCloudflareCostToken, + RemoveOpenRouterCostKey, + SaveCloudflareCostToken, + SaveOpenRouterCostKey, +} from "./command" +import type { Message } from "./message" +import type { CredentialProvider } from "./providers" + +type Cmd = Command.Command + +/** + * A cost source added by pasting a credential: everything its dialog steps, its Sources row and + * its toasts say, and the commands that save and remove it. A new provider is one entry here. + */ +export type CredentialSource = Readonly<{ + /** The line under the provider's name in the list, and once its source exists. */ + pick: Readonly<{ meta: string; addedMeta: string }> + field: Readonly<{ + label: string + placeholder: string + maxLength: number + /** Shown when the text is longer than `maxLength`, which the request would refuse to carry. */ + tooLong: string + /** The help under the field: `before`, a link to where the credential is made, then `after`. */ + help: Readonly<{ before: string; link: string; href: string; after: string }> + }> + add: Readonly<{ title: string; description: string; toast: string }> + replace: Readonly<{ + title: string + description: string + /** The Sources row's action and the form's submit. */ + label: string + toast: string + }> + remove: Readonly<{ title: string; description: string; toast: string }> + save: (secret: string) => Cmd + delete: () => Cmd +}> + +export const CREDENTIAL_SOURCES = { + openrouter: { + pick: { + meta: "Daily spend by model and serving provider", + addedMeta: "Replace its key in the Sources panel", + }, + field: { + label: "Management key", + placeholder: "sk-or-v1-...", + maxLength: OPENROUTER_KEY_MAX_LENGTH, + tooLong: "That is too long to be an OpenRouter key.", + help: { + before: "Create one under ", + link: "Management keys", + href: "https://openrouter.ai/settings/management-keys", + after: " in OpenRouter. A regular API key cannot read spend. Maple checks the key with OpenRouter, then stores it encrypted.", + }, + }, + add: { + title: "Add OpenRouter", + description: "Maple reads the last four weeks at once, then every day from here on.", + toast: "OpenRouter added", + }, + replace: { + title: "Replace the OpenRouter key", + description: "The source and its history stay. Use a key from the same OpenRouter account.", + label: "Replace key", + toast: "OpenRouter key replaced", + }, + remove: { + title: "Remove the OpenRouter source?", + description: + "Maple deletes the key and stops reading the account's spend, and its cost leaves this page. Adding OpenRouter again brings back the last four weeks only.", + toast: "OpenRouter removed", + }, + save: (apiKey) => SaveOpenRouterCostKey({ apiKey }), + delete: () => RemoveOpenRouterCostKey(), + }, + cloudflare: { + pick: { + meta: "Daily charges by product, plus plan fees", + addedMeta: "Replace its token in the Sources panel", + }, + field: { + label: "API token", + placeholder: "Cloudflare API token", + maxLength: CLOUDFLARE_COST_TOKEN_MAX_LENGTH, + tooLong: "That is too long to be a Cloudflare API token.", + help: { + before: "", + link: "Create a custom token", + href: "https://dash.cloudflare.com/profile/api-tokens", + after: " with the Billing: Read permission on one account. Maple checks the token with Cloudflare, then stores it encrypted.", + }, + }, + add: { + title: "Add Cloudflare", + description: + "Maple reads this month and the last at once, then older months back to a year, and every day from here on.", + toast: "Cloudflare added", + }, + replace: { + title: "Replace the Cloudflare token", + description: "The source and its history stay. Use a token for the same Cloudflare account.", + label: "Replace token", + toast: "Cloudflare token replaced", + }, + remove: { + title: "Remove the Cloudflare source?", + description: + "Maple deletes the token and stops reading the account's charges, and its cost leaves this page. Adding Cloudflare again reads the last year again.", + toast: "Cloudflare removed", + }, + save: (apiToken) => SaveCloudflareCostToken({ apiToken }), + delete: () => RemoveCloudflareCostToken(), + }, +} as const satisfies Record + +/** `provider`'s entry, as the shape every entry shares. */ +export const credentialSource = (provider: CredentialProvider): CredentialSource => + CREDENTIAL_SOURCES[provider] diff --git a/apps/web/src/page/cost/message.ts b/apps/web/src/page/cost/message.ts index ce3cb4292a..2f39af7483 100644 --- a/apps/web/src/page/cost/message.ts +++ b/apps/web/src/page/cost/message.ts @@ -4,8 +4,6 @@ import { CostSourcesResponse, CostTimeseriesResponse, CostTotalsResponse, - OpenRouterCostKeyRemoved, - OpenRouterCostKeySaved, } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" import { Schema } from "effect" @@ -19,6 +17,7 @@ import * as Field from "@maple/views/ui/field" import { TimeWindow } from "../../data/time-range" import { COST_GROUP_BYS } from "../../route/search" import * as TimeRangePicker from "../kit/time-range-picker" +import { CredentialProvider } from "./providers" /** A read's answer; `key` is what it was requested under, so a stale one is dropped. */ const result = (schema: Schema.Codec) => ({ @@ -48,18 +47,19 @@ export const Message = defineMessageUnion({ CompletedReadGrants: { grants: Schema.Array(Policy.PermissionName) }, /** Opens the add-source dialog on the providers. */ ClickedAddSource: {}, - ClickedAddOpenRouter: {}, + /** Picks a provider added by pasting a credential: the dialog moves to its form. */ + PickedProvider: { provider: CredentialProvider }, ClickedBackToProviders: {}, - /** Opens the dialog on the key form, for the source that exists. */ - ClickedReplaceKey: {}, + /** Opens the dialog on the provider's form, for the source that exists. */ + ClickedReplaceKey: { provider: CredentialProvider }, GotAddDialogMessage: { message: Dialog.Message }, GotKeyDraftMessage: { message: Field.Message }, SubmittedKey: {}, - CompletedSaveKey: { result: Schema.Result(OpenRouterCostKeySaved, LoadFailure) }, - ClickedRemoveSource: {}, + CompletedSaveKey: { provider: CredentialProvider, result: Schema.Result(Schema.Void, LoadFailure) }, + ClickedRemoveSource: { provider: CredentialProvider }, GotRemoveDialogMessage: { message: Dialog.Message }, ClickedConfirmRemove: {}, - CompletedRemoveKey: { result: Schema.Result(OpenRouterCostKeyRemoved, LoadFailure) }, + CompletedRemoveKey: { provider: CredentialProvider, result: Schema.Result(Schema.Void, LoadFailure) }, ...ChartEnvMessages, }) export type Message = typeof Message.Type diff --git a/apps/web/src/page/cost/model.ts b/apps/web/src/page/cost/model.ts index 07c172eac8..ab8a0a5e91 100644 --- a/apps/web/src/page/cost/model.ts +++ b/apps/web/src/page/cost/model.ts @@ -15,11 +15,12 @@ import * as Field from "@maple/views/ui/field" import { TimeWindow } from "../../data/time-range" import { CostSearch } from "../../route/search" import * as TimeRangePicker from "../kit/time-range-picker" +import { CredentialProvider } from "./providers" export type Search = typeof CostSearch.Type -/** The add-source dialog's two faces: the providers, then the chosen one's form. */ -export const AddStep = Schema.Literals(["pick", "openrouter"]) +/** The add-source dialog's two faces: the providers, then the chosen one's credential form. */ +export const AddStep = Schema.Union([Schema.Literal("pick"), CredentialProvider]) export type AddStep = typeof AddStep.Type export const Model = Schema.Struct({ @@ -40,11 +41,13 @@ export const Model = Schema.Struct({ grants: Schema.NullOr(Schema.Array(Policy.PermissionName)), addDialog: Dialog.Model, addStep: AddStep, - /** The OpenRouter management key being typed; cleared whichever way the dialog closes. */ + /** The credential being typed (a key or a token); cleared whichever way the dialog closes. */ keyDraft: Field.Model, - /** Why the last save was refused, shown under the field until the key changes. */ + /** Why the last save was refused, shown under the field until the credential changes. */ keyError: Schema.NullOr(Schema.String), removeDialog: Dialog.Model, + /** The provider whose source the remove dialog asks about. */ + removeProvider: CredentialProvider, /** `Settling`: the write answered and the sources are being read again; the list on show is the old one. */ sourceWrite: Schema.Literals(["Idle", "Saving", "Removing", "Settling"]), }) diff --git a/apps/web/src/page/cost/providers.ts b/apps/web/src/page/cost/providers.ts index b6a90e9696..5808f9b6b1 100644 --- a/apps/web/src/page/cost/providers.ts +++ b/apps/web/src/page/cost/providers.ts @@ -1,5 +1,6 @@ import type { CostProvider } from "@maple/domain/cost" import type { IconName } from "@maple/views/ui/icon" +import { Option, Schema } from "effect" import { PROVIDERS } from "../kit/providers" /** @@ -11,25 +12,44 @@ export type CostProviderEntry = Readonly<{ icon: IconName iconClassName?: string requestUsd: number | null + /** What its first sync brings in: this month and the last with older months after, or all it keeps. */ + firstSync: "months" | "four_weeks" }> // Not every cost provider is an integration: OpenRouter supplies cost only, so it is named here. +// Cloudflare is one, but its cost source is a token of its own, not the integration's connection. export const COST_PROVIDERS = { aws: { name: PROVIDERS.aws.name, icon: PROVIDERS.aws.icon, iconClassName: PROVIDERS.aws.iconClassName, requestUsd: 0.01, + firstSync: "months", }, openrouter: { name: "OpenRouter", icon: "OpenRouterIcon", iconClassName: "text-foreground", requestUsd: null, + firstSync: "four_weeks", }, cloudflare: { name: PROVIDERS.cloudflare.name, icon: PROVIDERS.cloudflare.icon, requestUsd: null, + firstSync: "months", }, } as const satisfies Record + +/** + * The providers added by pasting a credential on the Cost page, which can also replace it and + * remove the source. Each one's form and wording are in `credential-sources.ts`. + */ +export const CredentialProvider = Schema.Literals(["openrouter", "cloudflare"]) +export type CredentialProvider = typeof CredentialProvider.Type + +const decodeCredentialProvider = Schema.decodeUnknownOption(CredentialProvider) + +/** `provider` as one whose source is its pasted credential, or null for a connection's source. */ +export const credentialProviderOf = (provider: CostProvider): CredentialProvider | null => + Option.getOrNull(decodeCredentialProvider(provider)) diff --git a/apps/web/src/page/cost/update.ts b/apps/web/src/page/cost/update.ts index bf16c34f56..497dffbc99 100644 --- a/apps/web/src/page/cost/update.ts +++ b/apps/web/src/page/cost/update.ts @@ -1,4 +1,4 @@ -import { CostSourcesResponse, OPENROUTER_KEY_MAX_LENGTH } from "@maple/domain/http" +import { CostSourcesResponse } from "@maple/domain/http" import * as Policy from "@maple/domain/policy" import { Array as Arr, Option, Result, String as Str } from "effect" import { AsyncData, Command } from "foldkit" @@ -27,9 +27,7 @@ import { FetchCostTimeseries, FetchCostTotals, ReadCostGrants, - RemoveOpenRouterCostKey, ResolveWindow, - SaveOpenRouterCostKey, type TimeseriesInput, type WindowInput, } from "./command" @@ -44,8 +42,10 @@ import { withoutFilter, withoutFilters, } from "./derive" +import { credentialSource } from "./credential-sources" import { Message } from "./message" import type { AddStep, Model, Search } from "./model" +import type { CredentialProvider } from "./providers" type Return = PageReturn type Cmd = Command.Command @@ -150,6 +150,7 @@ export const init = (route: { readonly search: Search }, context: PageContext): keyDraft: Field.init(), keyError: null, removeDialog: Dialog.init("cost-remove-source", "alert-dialog"), + removeProvider: "openrouter", sourceWrite: "Idle", } return chain( @@ -202,10 +203,10 @@ const withReread = (step: Return, context: PageContext): Return => ) /** - * The list on show without the OpenRouter source, which a removal just deleted: its row is gone + * The list on show without `provider`'s source, which a removal just deleted: its row is gone * at once, so no refresh, late or failed, leaves actions on a source that no longer exists. */ -const withoutOpenRouterSource = (model: Model): Model => ({ +const withoutSourceOf = (model: Model, provider: CredentialProvider): Model => ({ ...model, sources: { ...model.sources, @@ -213,16 +214,16 @@ const withoutOpenRouterSource = (model: Model): Model => ({ model.sources.data, (response) => new CostSourcesResponse({ - sources: Arr.filter(response.sources, (source) => source.provider !== "openrouter"), + sources: Arr.filter(response.sources, (source) => source.provider !== provider), }), ), }, }) -/** Whether the org has an OpenRouter source, which a saved key then replaces the key of. */ -const hasOpenRouterSource = (model: Model): boolean => +/** Whether the org has `provider`'s source, which a saved credential then replaces the credential of. */ +const hasSourceOf = (model: Model, provider: CredentialProvider): boolean => Option.exists(AsyncData.getData(model.sources.data), ({ sources }) => - Arr.some(sources, (source) => source.provider === "openrouter"), + Arr.some(sources, (source) => source.provider === provider), ) export const routeChanged = ( @@ -343,10 +344,10 @@ export const update = (model: Model, message: Message, context: PageContext): Re }, CompletedReadGrants: ({ grants }) => ({ model: { ...model, grants } }), ClickedAddSource: () => openAdd(model, "pick"), - ClickedAddOpenRouter: () => ({ model: { ...clearedKey(model), addStep: "openrouter" } }), + PickedProvider: ({ provider }) => ({ model: { ...clearedKey(model), addStep: provider } }), ClickedBackToProviders: () => model.sourceWrite === "Idle" ? { model: { ...clearedKey(model), addStep: "pick" } } : { model }, - ClickedReplaceKey: () => openAdd(model, "openrouter"), + ClickedReplaceKey: ({ provider }) => openAdd(model, provider), // A save in flight keeps the dialog: its answer decides whether it closes. GotAddDialogMessage: ({ message: inner }) => { if (model.sourceWrite === "Saving") return { model } @@ -368,19 +369,20 @@ export const update = (model: Model, message: Message, context: PageContext): Re }, }, SubmittedKey: () => { - const apiKey = Str.trim(model.keyDraft.value) - if (apiKey === "" || model.sourceWrite !== "Idle") return { model } - // The request refuses a longer key before it is sent, which would leave the save unanswered. - if (apiKey.length > OPENROUTER_KEY_MAX_LENGTH) { - return { model: { ...model, keyError: "That is too long to be an OpenRouter key." } } + const secret = Str.trim(model.keyDraft.value) + if (secret === "" || model.sourceWrite !== "Idle" || model.addStep === "pick") return { model } + const source = credentialSource(model.addStep) + // The request refuses a longer one before it is sent, which would leave the save unanswered. + if (secret.length > source.field.maxLength) { + return { model: { ...model, keyError: source.field.tooLong } } } return { model: { ...model, sourceWrite: "Saving", keyError: null }, - commands: [SaveOpenRouterCostKey({ apiKey })], + commands: [source.save(secret)], } }, - // A refused key keeps the dialog open with the reason under the field. - CompletedSaveKey: ({ result }) => + // A refused credential keeps the dialog open with the reason under the field. + CompletedSaveKey: ({ provider, result }) => Result.match(result, { onFailure: (failure): Return => ({ model: { ...model, sourceWrite: "Idle", keyError: failureMessage(failure) }, @@ -390,20 +392,17 @@ export const update = (model: Model, message: Message, context: PageContext): Re clearedKey({ ...model, sourceWrite: "Settling" }), Dialog.close(model.addDialog), ) - const outMessage = hasOpenRouterSource(model) - ? toast( - "success", - "OpenRouter key replaced", - "The next sync runs within a few minutes.", - ) - : toast("success", "OpenRouter added", "The first sync runs within a few minutes.") + const source = credentialSource(provider) + const outMessage = hasSourceOf(model, provider) + ? toast("success", source.replace.toast, "The next sync runs within a few minutes.") + : toast("success", source.add.toast, "The first sync runs within a few minutes.") return withReread({ ...closed, outMessage }, context) }, }), - ClickedRemoveSource: () => + ClickedRemoveSource: ({ provider }) => model.sourceWrite !== "Idle" ? { model } - : liftRemoveDialog(model, Dialog.open(model.removeDialog)), + : liftRemoveDialog({ ...model, removeProvider: provider }, Dialog.open(model.removeDialog)), GotRemoveDialogMessage: ({ message: inner }) => model.sourceWrite === "Removing" ? { model } @@ -411,11 +410,14 @@ export const update = (model: Model, message: Message, context: PageContext): Re ClickedConfirmRemove: () => model.sourceWrite !== "Idle" ? { model } - : { model: { ...model, sourceWrite: "Removing" }, commands: [RemoveOpenRouterCostKey()] }, - CompletedRemoveKey: ({ result }) => { + : { + model: { ...model, sourceWrite: "Removing" }, + commands: [credentialSource(model.removeProvider).delete()], + }, + CompletedRemoveKey: ({ provider, result }) => { const closed = liftRemoveDialog( Result.isSuccess(result) - ? withoutOpenRouterSource({ ...model, sourceWrite: "Settling" }) + ? withoutSourceOf({ ...model, sourceWrite: "Settling" }, provider) : { ...model, sourceWrite: "Idle" }, Dialog.close(model.removeDialog), ) @@ -425,7 +427,10 @@ export const update = (model: Model, message: Message, context: PageContext): Re outMessage: toast("error", "Couldn't remove the source", failureMessage(failure)), }), onSuccess: (): Return => - withReread({ ...closed, outMessage: toast("success", "OpenRouter removed") }, context), + withReread( + { ...closed, outMessage: toast("success", credentialSource(provider).remove.toast) }, + context, + ), }) }, ...chartEnvHandlers(model), diff --git a/apps/web/src/page/cost/view-add-source.ts b/apps/web/src/page/cost/view-add-source.ts index 42b233f638..9644d37620 100644 --- a/apps/web/src/page/cost/view-add-source.ts +++ b/apps/web/src/page/cost/view-add-source.ts @@ -15,9 +15,10 @@ import { vstack } from "@maple/views/ui/stack" import { textLink } from "@maple/views/ui/text-link" import { typography } from "@maple/views/ui/typography" import { AppRoute, hrefOf } from "../../route" +import { type CredentialSource, credentialSource } from "./credential-sources" import { Message } from "./message" import type { Model } from "./model" -import { COST_PROVIDERS, type CostProviderEntry } from "./providers" +import { COST_PROVIDERS, type CostProviderEntry, CredentialProvider } from "./providers" type H = HtmlBuilder @@ -26,17 +27,42 @@ const CONNECT_AWS_HREF = hrefOf(AppRoute.SourcesNew({ search: { integration: "aw /** Where an AWS connection and its Cost module are changed. */ export const AWS_INTEGRATION_HREF = hrefOf(AppRoute.Integrations({ search: { integration: "aws" } })) -const OPENROUTER_KEYS_URL = "https://openrouter.ai/settings/management-keys" - -/** Whether the org has its OpenRouter source, which there is at most one of. */ -const hasOpenRouterSource = (sources: ReadonlyArray): boolean => - Arr.some(sources, (source) => source.provider === "openrouter") +/** Whether the org has `provider`'s source, which there is at most one of. */ +const hasSourceOf = (sources: ReadonlyArray, provider: CredentialProvider): boolean => + Arr.some(sources, (source) => source.provider === provider) const mark = (h: H, provider: CostProviderEntry): Html => icon(h, provider.icon, { size: 18, className: cn("shrink-0", provider.iconClassName) }) -/** The providers to add: AWS goes to its connect flow, OpenRouter to the key form. */ -const providers = (h: H, hasOpenRouter: boolean): Html => +/** A provider added by pasting a credential: to its form, or named as added once its source exists. */ +const credentialRow = ( + h: H, + provider: CredentialProvider, + sources: ReadonlyArray, + isLast: boolean, +): Html => { + const entry: CostProviderEntry = COST_PROVIDERS[provider] + const { pick } = credentialSource(provider) + return hasSourceOf(sources, provider) + ? listRow(h, { + divided: !isLast, + leading: mark(h, entry), + title: entry.name, + meta: pick.addedMeta, + trailing: badge(h, { variant: "muted", size: "sm" }, ["Already added"]), + }) + : listRow(h, { + divided: !isLast, + onClick: Message.PickedProvider({ provider }), + leading: mark(h, entry), + title: entry.name, + meta: pick.meta, + trailing: icon(h, "ChevronRightIcon", { size: 14 }), + }) +} + +/** The providers to add: AWS goes to its connect flow, the others to their credential form. */ +const providers = (h: H, sources: ReadonlyArray): Html => vstack(h, { gap: "3" }, [ vstack(h, { gap: "0" }, [ listRow(h, { @@ -47,23 +73,12 @@ const providers = (h: H, hasOpenRouter: boolean): Html => meta: "Daily cost by service, region and usage type", trailing: icon(h, "ChevronRightIcon", { size: 14 }), }), - hasOpenRouter - ? listRow(h, { - leading: mark(h, COST_PROVIDERS.openrouter), - title: COST_PROVIDERS.openrouter.name, - meta: "Replace its key in the Sources panel", - trailing: badge(h, { variant: "muted", size: "sm" }, ["Already added"]), - }) - : listRow(h, { - onClick: Message.ClickedAddOpenRouter(), - leading: mark(h, COST_PROVIDERS.openrouter), - title: COST_PROVIDERS.openrouter.name, - meta: "Daily spend by model and serving provider", - trailing: icon(h, "ChevronRightIcon", { size: 14 }), - }), + ...Arr.map(CredentialProvider.literals, (provider, index) => + credentialRow(h, provider, sources, index === CredentialProvider.literals.length - 1), + ), ]), typography(h, { size: "xs", tone: "muted", as: "p", className: "font-prose" }, [ - "AWS bills USD\u00a00.01 for each request Maple makes, a few dollars a month per account. OpenRouter requests are free. ", + "AWS bills USD\u00a00.01 for each request Maple makes, a few dollars a month per account. OpenRouter and Cloudflare requests are free. ", textLink(h, { href: AWS_INTEGRATION_HREF, tone: "inherit", className: "underline" }, [ "Turn on Cost for an AWS account that is connected", ]), @@ -72,23 +87,23 @@ const providers = (h: H, hasOpenRouter: boolean): Html => ]) const KEY_IDS: FieldIds = { - control: "cost-openrouter-key", - label: "cost-openrouter-key-label", - error: "cost-openrouter-key-error", - description: "cost-openrouter-key-description", + control: "cost-source-credential", + label: "cost-source-credential-label", + error: "cost-source-credential-error", + description: "cost-source-credential-description", } -const keyField = (h: H, model: Model): Html => { +const keyField = (h: H, model: Model, { field: text }: CredentialSource): Html => { const state = Field.stateOf(model.keyDraft, model.keyError !== null) return field(h, { state }, [ - fieldLabel(h, { ids: KEY_IDS, state }, "Management key"), + fieldLabel(h, { ids: KEY_IDS, state }, text.label), input(h, { ids: KEY_IDS, state, attributes: [ h.Type("password"), h.Attribute("autocomplete", "off"), - h.Placeholder("sk-or-v1-..."), + h.Placeholder(text.placeholder), ...(model.sourceWrite === "Saving" ? [h.Disabled(true)] : []), ...Field.controlAttributes(h, model.keyDraft, (message) => Message.GotKeyDraftMessage({ message }), @@ -99,32 +114,73 @@ const keyField = (h: H, model: Model): Html => { ? [] : [fieldError(h, { ids: KEY_IDS, state, attributes: [h.Role("alert")] }, [model.keyError])]), fieldDescription(h, { ids: KEY_IDS, state }, [ - "Create one under ", + text.help.before, textLink( h, { - href: OPENROUTER_KEYS_URL, + href: text.help.href, tone: "inherit", className: "underline", attributes: [h.Target("_blank"), h.Rel("noreferrer")], }, - ["Management keys"], + [text.help.link], ), - " in OpenRouter. A regular API key cannot read spend. Maple checks the key with OpenRouter, then stores it encrypted.", + text.help.after, + ]), + ]) +} + +/** The chosen provider's credential form: adding its source, or replacing the credential of the one that exists. */ +const credentialForm = ( + h: H, + model: Model, + provider: CredentialProvider, + sources: ReadonlyArray, + close: Parameters[0], +): Html => { + const id = model.addDialog.id + const source = credentialSource(provider) + const isReplacing = hasSourceOf(sources, provider) + const isSaving = model.sourceWrite === "Saving" + const canSubmit = !isSaving && Str.trim(model.keyDraft.value) !== "" + const text = isReplacing ? source.replace : source.add + return formDialogForm(h, canSubmit ? Message.SubmittedKey() : undefined, [ + DialogView.dialogHeader(h, "dialog", [ + DialogView.dialogTitle(h, "dialog", id, text.title), + DialogView.dialogDescription(h, "dialog", id, text.description), + ]), + DialogView.dialogPanel(h, "dialog", id, [keyField(h, model, source)]), + DialogView.dialogFooter(h, "dialog", [ + // A replace has no providers to go back to. + isReplacing + ? button( + h, + { variant: "outline", isDisabled: isSaving, attributes: isSaving ? [] : [...close] }, + ["Cancel"], + ) + : button( + h, + { + variant: "outline", + isDisabled: isSaving, + attributes: [h.OnClick(Message.ClickedBackToProviders())], + }, + ["Back"], + ), + button(h, { type: "submit", isLoading: isSaving, isDisabled: !canSubmit }, [ + isReplacing ? source.replace.label : "Add source", + ]), ]), ]) } /** - * Add a source, or replace the OpenRouter key: the providers first, then OpenRouter's key form. - * Opened on the form for a replace, which has no providers to go back to. + * Add a source, or replace a source's credential: the providers first, then the chosen one's + * form. Opened on the form for a replace. */ export const addSourceDialog = (h: H, model: Model, sources: ReadonlyArray): Html => { const id = model.addDialog.id - const isReplacing = hasOpenRouterSource(sources) - const isSaving = model.sourceWrite === "Saving" - const canSubmit = !isSaving && Str.trim(model.keyDraft.value) !== "" - const onPick = model.addStep === "pick" + const step = model.addStep return h.submodel({ slotId: "cost-add-source", model: model.addDialog, @@ -135,7 +191,7 @@ export const addSourceDialog = (h: H, model: Model, sources: ReadonlyArray portal ?? h.empty, toContent: (close) => - onPick + step === "pick" ? [ DialogView.dialogHeader(h, "dialog", [ DialogView.dialogTitle(h, "dialog", id, "Add a cost source"), @@ -146,76 +202,27 @@ export const addSourceDialog = (h: H, model: Model, sources: ReadonlyArray - confirmDialog(h, { +/** Removing a credential's source: what leaves the page, and what adding it again brings back. */ +export const removeSourceDialog = (h: H, model: Model): Html => { + const { remove } = credentialSource(model.removeProvider) + return confirmDialog(h, { slotId: "cost-remove-source", model: model.removeDialog, toParentMessage: (message) => Message.GotRemoveDialogMessage({ message }), - title: "Remove the OpenRouter source?", - description: - "Maple deletes the key and stops reading the account's spend, and its cost leaves this page. Adding OpenRouter again brings back the last four weeks only.", + title: remove.title, + description: remove.description, confirmLabel: "Remove source", onConfirm: Message.ClickedConfirmRemove(), isPending: model.sourceWrite === "Removing", }) +} diff --git a/apps/web/src/page/cost/view-sources.ts b/apps/web/src/page/cost/view-sources.ts index 06f3ed57d6..e2ea3b4863 100644 --- a/apps/web/src/page/cost/view-sources.ts +++ b/apps/web/src/page/cost/view-sources.ts @@ -11,9 +11,10 @@ import { stripClass } from "@maple/views/ui/rule" import { hstack, vstack } from "@maple/views/ui/stack" import { typography } from "@maple/views/ui/typography" import { relativeTime } from "../kit/relative-time" +import { credentialSource } from "./credential-sources" import { formatDay, requestsCost } from "./derive" import { Message } from "./message" -import { COST_PROVIDERS, type CostProviderEntry } from "./providers" +import { COST_PROVIDERS, type CostProviderEntry, credentialProviderOf } from "./providers" import { AWS_INTEGRATION_HREF } from "./view-add-source" type H = HtmlBuilder @@ -30,19 +31,21 @@ const STATUS = { Readonly<{ label: string; variant: "ok" | "info" | "muted" | "crit" }> > -/** What a row offers: an AWS source is its connection's, an OpenRouter one is its key. */ -const actions = (h: H, source: CostSourceSummary, isBusy: boolean): ReadonlyArray => - source.provider === "openrouter" - ? [ +/** What a row offers: a connection's source is managed there, a pasted credential's here. */ +const actions = (h: H, source: CostSourceSummary, isBusy: boolean): ReadonlyArray => { + const provider = credentialProviderOf(source.provider) + return provider === null + ? [linkButton(h, { variant: "ghost", size: "sm", href: AWS_INTEGRATION_HREF }, ["Manage connection"])] + : [ button( h, { variant: "ghost", size: "sm", isDisabled: isBusy, - attributes: [h.OnClick(Message.ClickedReplaceKey())], + attributes: [h.OnClick(Message.ClickedReplaceKey({ provider }))], }, - ["Replace key"], + [credentialSource(provider).replace.label], ), button( h, @@ -50,12 +53,12 @@ const actions = (h: H, source: CostSourceSummary, isBusy: boolean): ReadonlyArra variant: "ghost", size: "sm", isDisabled: isBusy, - attributes: [h.OnClick(Message.ClickedRemoveSource())], + attributes: [h.OnClick(Message.ClickedRemoveSource({ provider }))], }, ["Remove"], ), ] - : [linkButton(h, { variant: "ghost", size: "sm", href: AWS_INTEGRATION_HREF }, ["Manage connection"])] +} /** What the provider billed the account for Maple's requests this month; nothing where requests are free. */ const requestsFact = (source: CostSourceSummary): ReadonlyArray => { diff --git a/apps/web/src/page/cost/view.ts b/apps/web/src/page/cost/view.ts index 4479537619..da02910567 100644 --- a/apps/web/src/page/cost/view.ts +++ b/apps/web/src/page/cost/view.ts @@ -38,6 +38,7 @@ import { activeFilters, DIMENSIONS, groupByOf, hasSynced, keyLabel } from "./der import { Message } from "./message" import { LONG_RANGE_MAX_SECONDS } from "../../route/session-time-range" import { canManageSources, cannotManageSources, type Model } from "./model" +import { COST_PROVIDERS } from "./providers" import { DEFAULT_PRESET } from "./update" import { addSourceDialog, removeSourceDialog } from "./view-add-source" import { dailyChart } from "./view-chart" @@ -63,7 +64,7 @@ const onRamp = ( emptyMedia(h, { variant: "icon" }, [icon(h, "CircleDollarIcon")]), emptyTitle(h, ["No cost source yet"]), emptyDescription(h, [ - "Add a source and Maple reads what it bills each day: an AWS account by service, region and usage type, an OpenRouter account by model.", + "Add a source and Maple reads what it bills each day: an AWS account by service, region and usage type, an OpenRouter account by model, a Cloudflare account by product.", ]), // A member's save would be refused: say who can, and offer no form. ...(!options.cannotManage @@ -104,19 +105,27 @@ const onRamp = ( emptyFrameClass("dashed"), ) +const MONTHS_SCOPE = "this month and the last, and older months fill in after that" + /** * What the first syncs bring in, by the providers that have no cost yet (waiting or failing - * alike): AWS keeps over a year, OpenRouter four weeks. + * alike): AWS and Cloudflare keep a year or more, OpenRouter four weeks. */ const firstSyncScope = (sources: ReadonlyArray): string => { - const unsynced = Arr.filter(sources, (source) => source.watermarkDate === null) - const hasOpenRouter = Arr.some(unsynced, (source) => source.provider === "openrouter") - const hasOther = Arr.some(unsynced, (source) => source.provider !== "openrouter") - return hasOpenRouter && hasOther - ? "AWS brings in this month and the last, and older months fill in after that. OpenRouter brings in its last four weeks." - : hasOpenRouter - ? "It brings in the last four weeks, which is all OpenRouter keeps." - : "It brings in this month and the last, and older months fill in after that." + const unsynced = Arr.dedupe( + Arr.map( + Arr.filter(sources, (source) => source.watermarkDate === null), + (source) => source.provider, + ), + ) + const months = Arr.filter(unsynced, (provider) => COST_PROVIDERS[provider].firstSync === "months") + if (months.length === unsynced.length) return `It brings in ${MONTHS_SCOPE}.` + if (months.length === 0) return "It brings in the last four weeks, which is all OpenRouter keeps." + const names = Arr.join( + Arr.map(months, (provider) => COST_PROVIDERS[provider].name), + " and ", + ) + return `${names} ${months.length === 1 ? "brings" : "bring"} in ${MONTHS_SCOPE}. OpenRouter brings in its last four weeks.` } /** Sources exist and none has cost yet: what the reader is waiting for. */ diff --git a/docs/cloud-cost-plan.md b/docs/cloud-cost-plan.md index 79b3525cc8..6a32b88031 100644 --- a/docs/cloud-cost-plan.md +++ b/docs/cloud-cost-plan.md @@ -1,7 +1,7 @@ # Cloud cost: plan Status: **phase 1 built, 2026-11-11, behind the `cloudCost` org flag.** AWS spend is synced daily -into `cost_daily` and shown on `/cost`. OpenRouter followed on 2026-11-12 as the second source. +into `cost_daily` and shown on `/cost`. OpenRouter and Cloudflare followed on 2026-11-12. Phase 0 checked the provider APIs against docs, public schemas and one real AWS account. Recommendations (phase 2) and the other providers are not built. @@ -44,8 +44,14 @@ warehouse are not. One adapter per provider, in `packages/backend/src/services/cost/providers/`, behind one port. The registry covers cost only. The integrations themselves are not refactored. A provider that is -not an integration (OpenRouter) keeps its credential in a table of its own, and -`cost_sources.connection_ref` points at that row. +not read through an integration's connection keeps a pasted credential, and +`cost_sources.connection_ref` points at its row. OpenRouter has a table of its own +(`openrouter_cost_connections`). Every later provider shares `cost_connections`: one row per org +and provider, with the sealed credential, the vendor's account id and a label. `CostConnectionStore` +seals, upserts (refusing a credential of another account), removes and loads it, and +`reconcileCostConnectionSources` keeps `cost_sources` in step for each provider named in +`COST_CONNECTION_PROVIDERS`. A new provider of this kind adds its literal there and needs no +migration. ```ts interface CostProvider { @@ -113,7 +119,8 @@ The windows above are the AWS ones. Each provider has a profile (`COST_SYNC_PROF `sync-plan.ts`): the oldest day it serves and how far its newest day trails today. No window reaches outside them, because a day the provider no longer serves comes back empty, and an empty day replaces the stored one. OpenRouter serves 30 completed days: its sync asks for 28 at most and -never for today. +never for today. Cloudflare serves a year of completed days: its history stops at the first day of +the eleventh month back, and it never asks for today either. Each step writes its days whole under one `IngestedAt`. A day a pull returned nothing for gets a marker row (`ChargeCategory = 'none'`), so it supersedes the older pull. Reads leave it out. @@ -140,12 +147,13 @@ currency the figures are in and lists the others. Nothing is added across curren - Web: `/cost` behind the `cloudCost` flag. One page: the window against the period before, a daily chart, a breakdown by service, provider, category, region, usage type or account with filters, and each source's sync state. Sources are added from the page: AWS through its - connection (which has a Cost switch), OpenRouter by pasting a management key, which the page can - also replace or remove. Recommendations (list and detail with the evidence chart) come with + connection (which has a Cost switch), OpenRouter by pasting a management key and Cloudflare by + pasting an API token, which the page can also replace or remove. The pasted-credential step is + one form described by data (`credential-sources.ts`). Recommendations (list and detail with the evidence chart) come with phase 2. - API: four internal endpoints in the query-engine group (`cost-totals`, `cost-timeseries`, - `cost-breakdown`, `cost-sources`), and `PUT` / `DELETE /internal/cost/openrouter` for the - OpenRouter key. A public `/v2/cost/*` is not built. + `cost-breakdown`, `cost-sources`), and `PUT` / `DELETE` on `/internal/cost/openrouter` and + `/internal/cost/cloudflare` for the pasted credentials. A public `/v2/cost/*` is not built. - MCP: `cost_overview`, `cost_breakdown`, `compare_cost_periods`, `list_cost_recommendations`, `inspect_cost_recommendation`. @@ -229,14 +237,51 @@ currency the figures are in and lists the others. Nothing is added across curren overprovisioned, Cloud Run cost, commitments. Negative cost is the saving. - Until the export exists, Maple can still show the recommender's savings and its own rules. -### Cloudflare (billed for pay-as-you-go, else estimated) - -- Schema: `GET /accounts/{id}/billable-usage` returns FOCUS-style daily rows with `BilledCost`. It - is marked alpha, covers usage overages (not plan fees), and is not available to enterprise - accounts. Plan fees come from the subscription endpoints. -- All of it needs the "Billing Read" permission. Open: whether the OAuth app can be granted a - billing scope. If not, this source needs an API token. -- Fallback: usage from the GraphQL analytics datasets (already authorized) times list price. +### Cloudflare (billed, pay-as-you-go accounts) + +"Live" here means checked against a real pay-as-you-go account on 2026-11-12. + +- It needs an API token with the "Billing: Read" permission on one account. No billing scope is + known for the OAuth app, so the integration's connection is not used: the org pastes a token on + the Cost page, which Maple checks and stores sealed in `cost_connections`. +- Live: `GET /accounts` lists the accounts the token reads. A token for one account lists one, + and its id is the source's account. A token that lists none or several is refused. A + replacement token must read the same account, since another account's charges would overwrite + the source's days. Live: a token Cloudflare does not know answers 403 on this call, and 401 on + the billing calls. +- Live: `GET /accounts/{id}/billable-usage/info` answers `covered`. Schema: usage-based billing + data is not available to enterprise accounts. A save is refused and a pull reports "not ready" + while it is false. +- Live: `GET /accounts/{id}/billable-usage?from=&to=` (marked "Version 1, Alpha") answers + FOCUS-style rows, one per UTC day and priced metric, in one answer without paging (about 1,100 + rows for six weeks). Most rows bill nothing, being inside the included allowance. +- Live: `to` is exclusive. `from=2026-10-01&to=2026-10-02` answers 1 October only, and equal + dates are refused. +- Live: a billing period's rows come back only when the period starts inside `[from, to)`. A + range that starts a day after the period did answers nothing for it. So every request asks from + 31 days before its first day to the day after its last, and keeps only its own days. +- Live: a range over 90 days is refused (`max_date_range_days`), so a window is read in pieces of + 55 days. Requests are free, so the source counts none. +- Live: yesterday is served, today is not, and the oldest day served is one year before today. + The profile asks from the first day of the eleventh month back through yesterday. The two newest + days are stored as not final. +- Live: a wrong or revoked token answers 401 (sync reason `auth`), a token without billing access + 403 (`not_ready`). A 400 is a request Maple wrote wrong (`upstream`). +- Rows: the product family (`Workers`, `R2`, `D1` ...) is `CloudService`, the priced metric is + `UsageType`, the region is `global`, and the category comes from a small map of the families. + Rows with no billed and no list cost are left out. +- Live: usage rows do not include plan fees. `GET /accounts/{id}/subscriptions` (paged) lists them + with `price`, `frequency` and `current_period_start`. Each paid monthly subscription becomes a + purchase row (`CloudService` "Subscriptions", the rate plan as `UsageType`, the zone as the + resource) on the day each billing period started, stepped back month by month from the current + period and never before the subscription was created. A yearly one gets one row on its current + period's start. +- Assumed: Cloudflare reports the current period only, so earlier months use today's price. A plan + whose price changed shows the new price for its past months too. +- Open: whether a subscription in trial is charged its listed price, and whether a completed day + is ever restated. The four newest days are pulled again daily. +- Not built: enterprise contracts, zone-level analytics, the restricted "Version 2" endpoints, + invoices and credits. ### Railway (estimated per service, billed per workspace) @@ -272,9 +317,10 @@ Each phase is one PR stack. Plan imports. First Maple rules, each backed by a metric already ingested: load balancers with no requests, databases with idle connections and low CPU, container services reserving far more than they use. -3. **More providers.** OpenRouter is built. Railway, GCP, PlanetScale, Supabase, Cloudflare, one - PR each. Each provider adds a literal to `CostProvider`, an adapter, a reconcile function and a - sync profile. +3. **More providers.** OpenRouter and Cloudflare are built. Railway, GCP, PlanetScale, Supabase, + one PR each. Each provider adds a literal to `CostProvider`, an adapter and a sync profile. One + read with a pasted credential also adds itself to `COST_CONNECTION_PROVIDERS` and to the Cost + page's `CREDENTIAL_SOURCES`; one read through a connection adds a reconcile function. 4. **Closing the loop.** Budgets and cost anomalies as managed alert signals, a cost block in the weekly digest, cost per request on service pages, realized savings once an accepted recommendation stops firing. @@ -291,7 +337,8 @@ Each phase is one PR stack. ## Open questions -- Cloudflare: is there an OAuth billing scope. +- Cloudflare: is there an OAuth billing scope. With one, the integration's connection could + supply cost and no token would be pasted. - Railway: can a workspace token read `customer`. - PlanetScale: the time unit of SKU `rate`, and whether the open invoice is listed. - AWS: the request rate limit of Cost Explorer is not published. The adapter backs off on From 9d9cdb6de6c5bda2efed92811c58bc6da4facf52 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 00:00:42 +0100 Subject: [PATCH 3/9] fix(cost): review pass on the Cloudflare source Backend. A subscription in trial, failed or expired gets no fee row, since its listed price was not charged. A subscription list longer than the pages Maple reads fails the pull instead of under-counting. The usage read takes a window and adds the billing period's lead and the exclusive end itself, so the save check and the pull share one rule, and the failure's kind is put on the span in one place. The store maps its own load failures for an adapter. The test fake serves a year like the real API: the range before the oldest month reaches past it and is answered with the older days left out. Web. The credential messages and fields are named for a credential, not a key. The first-sync sentence and the free-requests note take the provider names from the provider table, so a second four-week provider reads right. The remove dialog has no provider until it opens. Docs say what a new pasted-credential provider adds beyond its literal, and name the limit on fees of a subscription that leaves Cloudflare's list. --- apps/web/src/lab/cost-fixture.ts | 4 +- apps/web/src/lab/cost-lab.ts | 22 ++--- apps/web/src/page/cost/command.ts | 16 ++-- apps/web/src/page/cost/cost.scene.test.ts | 83 ++++++++++++------- apps/web/src/page/cost/credential-sources.ts | 8 +- apps/web/src/page/cost/derive.ts | 4 + apps/web/src/page/cost/message.ts | 16 ++-- apps/web/src/page/cost/model.ts | 8 +- apps/web/src/page/cost/update.ts | 70 +++++++++------- apps/web/src/page/cost/view-add-source.ts | 55 +++++++----- apps/web/src/page/cost/view-sources.ts | 6 +- apps/web/src/page/cost/view.ts | 25 ++++-- docs/cloud-cost-plan.md | 24 ++++-- .../cost/CloudflareCostConnectionService.ts | 34 ++------ .../src/services/cost/CostConnectionStore.ts | 29 ++++--- .../services/cost/cost-connection-sources.ts | 12 +-- .../cloudflare/CloudflareCostProvider.test.ts | 76 +++++++++++++++-- .../cloudflare/CloudflareCostProvider.ts | 49 +++-------- .../cloudflare/cloudflare-billing-api.ts | 48 +++++++---- .../providers/cloudflare/fake-cloudflare.ts | 5 +- .../providers/cloudflare/normalize.test.ts | 6 ++ .../cost/providers/cloudflare/normalize.ts | 13 +-- .../services/cost/providers/fake-cost-env.ts | 2 +- packages/domain/src/http/cloudflare-cost.ts | 2 +- packages/domain/src/http/cost-connections.ts | 2 +- 25 files changed, 367 insertions(+), 252 deletions(-) diff --git a/apps/web/src/lab/cost-fixture.ts b/apps/web/src/lab/cost-fixture.ts index 392f6f0b95..f7e253425b 100644 --- a/apps/web/src/lab/cost-fixture.ts +++ b/apps/web/src/lab/cost-fixture.ts @@ -280,7 +280,7 @@ const cloudflareSource = source({ requestsThisMonth: 0, }) -const CREDENTIAL_SOURCES = { openrouter: openRouterSource, cloudflare: cloudflareSource } as const +const LAB_CREDENTIAL_SOURCES = { openrouter: openRouterSource, cloudflare: cloudflareSource } as const /** What the lab's reader did to each pasted-credential source since the state loaded; null for nothing. */ export type LabSourceChanges = Readonly> @@ -302,7 +302,7 @@ export const labSourcesWith = ( sources: [ ...Schema.encodeSync(CostSourcesResponse)(current).sources, { - ...CREDENTIAL_SOURCES[provider], + ...LAB_CREDENTIAL_SOURCES[provider], status: "pending", watermarkDate: null, backfillDate: null, diff --git a/apps/web/src/lab/cost-lab.ts b/apps/web/src/lab/cost-lab.ts index 88350369e2..1d3de5a077 100644 --- a/apps/web/src/lab/cost-lab.ts +++ b/apps/web/src/lab/cost-lab.ts @@ -127,13 +127,13 @@ const openingOf = (state: LabState): ReadonlyArray => { return [ CostMessage.ClickedAddSource(), CostMessage.PickedProvider({ provider: "openrouter" }), - CostMessage.GotKeyDraftMessage({ + CostMessage.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value: "sk-or-v1-0123456789abcdef0123456789" }), }), - CostMessage.SubmittedKey(), + CostMessage.SubmittedCredential(), ] case "replace-key": - return [CostMessage.ClickedReplaceKey({ provider: "openrouter" })] + return [CostMessage.ClickedReplaceCredential({ provider: "openrouter" })] case "remove-source": return [CostMessage.ClickedRemoveSource({ provider: "openrouter" })] case "add-cloudflare": @@ -142,13 +142,13 @@ const openingOf = (state: LabState): ReadonlyArray => { return [ CostMessage.ClickedAddSource(), CostMessage.PickedProvider({ provider: "cloudflare" }), - CostMessage.GotKeyDraftMessage({ + CostMessage.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value: "cfut_0123456789abcdefghij0123456789" }), }), - CostMessage.SubmittedKey(), + CostMessage.SubmittedCredential(), ] case "replace-token": - return [CostMessage.ClickedReplaceKey({ provider: "cloudflare" })] + return [CostMessage.ClickedReplaceCredential({ provider: "cloudflare" })] case "remove-cloudflare": return [CostMessage.ClickedRemoveSource({ provider: "cloudflare" })] // Without Clerk the page starts with every permission: the member states take them away. @@ -201,7 +201,7 @@ const timeseriesArgs = argsOf(TimeseriesInput) const breakdownArgs = argsOf(BreakdownInput) const removedAnswer = (provider: CredentialProvider) => - CostMessage.CompletedRemoveKey({ provider, result: Result.succeed(undefined) }) + CostMessage.CompletedRemoveCredential({ provider, result: Result.succeed(undefined) }) /** A read's answer: the fixture's, or the outage in the `error` state. */ const settled = (state: LabState, value: () => A): Result.Result => @@ -282,14 +282,14 @@ const answerOf = (world: World, search: Search, command: CostCommand): Option.Op ) case SaveOpenRouterCostKey.name: return Option.some( - CostMessage.CompletedSaveKey({ + CostMessage.CompletedSaveCredential({ provider: "openrouter", result: state === "key-refused" ? Result.fail(refusedKey) : Result.succeed(undefined), }), ) case SaveCloudflareCostToken.name: return Option.some( - CostMessage.CompletedSaveKey({ + CostMessage.CompletedSaveCredential({ provider: "cloudflare", result: state === "token-refused" ? Result.fail(refusedToken) : Result.succeed(undefined), }), @@ -309,9 +309,9 @@ type Settled = Step & Pick /** A saved credential adds its provider to the sources the lab answers with next, a removal takes it out. */ const changesAfter = (changes: LabSourceChanges, answers: ReadonlyArray): LabSourceChanges => Arr.reduce(answers, changes, (current, answer) => - answer._tag === "CompletedSaveKey" && Result.isSuccess(answer.result) + answer._tag === "CompletedSaveCredential" && Result.isSuccess(answer.result) ? { ...current, [answer.provider]: true } - : answer._tag === "CompletedRemoveKey" && Result.isSuccess(answer.result) + : answer._tag === "CompletedRemoveCredential" && Result.isSuccess(answer.result) ? { ...current, [answer.provider]: false } : current, ) diff --git a/apps/web/src/page/cost/command.ts b/apps/web/src/page/cost/command.ts index 2db25b2cfb..9f2caad33f 100644 --- a/apps/web/src/page/cost/command.ts +++ b/apps/web/src/page/cost/command.ts @@ -101,41 +101,41 @@ export const ReadCostGrants = Command.define("ReadCostGrants", { /** Saves the org's OpenRouter management key, which makes (or keeps) its cost source. */ export const SaveOpenRouterCostKey = Command.define("SaveOpenRouterCostKey", { args: { apiKey: Schema.String }, - messages: [Message.CompletedSaveKey], + messages: [Message.CompletedSaveCredential], execute: ({ apiKey }) => Effect.flatMap(MapleInternalApiClient, (client) => client.openRouterCost.set({ payload: new OpenRouterKeyUpdateRequest({ apiKey }) }), ).pipe( Effect.asVoid, - settleTo((result) => Message.CompletedSaveKey({ provider: "openrouter", result })), + settleTo((result) => Message.CompletedSaveCredential({ provider: "openrouter", result })), ), }) export const RemoveOpenRouterCostKey = Command.define("RemoveOpenRouterCostKey", { - messages: [Message.CompletedRemoveKey], + messages: [Message.CompletedRemoveCredential], execute: Effect.flatMap(MapleInternalApiClient, (client) => client.openRouterCost.remove()).pipe( Effect.asVoid, - settleTo((result) => Message.CompletedRemoveKey({ provider: "openrouter", result })), + settleTo((result) => Message.CompletedRemoveCredential({ provider: "openrouter", result })), ), }) /** Saves the org's Cloudflare API token, which makes (or keeps) its cost source. */ export const SaveCloudflareCostToken = Command.define("SaveCloudflareCostToken", { args: { apiToken: Schema.String }, - messages: [Message.CompletedSaveKey], + messages: [Message.CompletedSaveCredential], execute: ({ apiToken }) => Effect.flatMap(MapleInternalApiClient, (client) => client.cloudflareCost.set({ payload: new CloudflareCostTokenRequest({ apiToken }) }), ).pipe( Effect.asVoid, - settleTo((result) => Message.CompletedSaveKey({ provider: "cloudflare", result })), + settleTo((result) => Message.CompletedSaveCredential({ provider: "cloudflare", result })), ), }) export const RemoveCloudflareCostToken = Command.define("RemoveCloudflareCostToken", { - messages: [Message.CompletedRemoveKey], + messages: [Message.CompletedRemoveCredential], execute: Effect.flatMap(MapleInternalApiClient, (client) => client.cloudflareCost.remove()).pipe( Effect.asVoid, - settleTo((result) => Message.CompletedRemoveKey({ provider: "cloudflare", result })), + settleTo((result) => Message.CompletedRemoveCredential({ provider: "cloudflare", result })), ), }) diff --git a/apps/web/src/page/cost/cost.scene.test.ts b/apps/web/src/page/cost/cost.scene.test.ts index f0ec3cc96b..6ae8418855 100644 --- a/apps/web/src/page/cost/cost.scene.test.ts +++ b/apps/web/src/page/cost/cost.scene.test.ts @@ -212,28 +212,36 @@ describe("cost page", () => { Message.PickedProvider({ provider: "openrouter" }), ).model const typed = (model: Model, value: string) => - pageUpdate(model, Message.GotKeyDraftMessage({ message: Field.Message.ChangedValue({ value }) })) - .model + pageUpdate( + model, + Message.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value }) }), + ).model // Nothing typed sends nothing. - expect(names(pageUpdate(form, Message.SubmittedKey()).commands)).toEqual([]) + expect(names(pageUpdate(form, Message.SubmittedCredential()).commands)).toEqual([]) - const saving = pageUpdate(typed(form, " sk-or-v1-regular-key-0123456789 "), Message.SubmittedKey()) + const saving = pageUpdate( + typed(form, " sk-or-v1-regular-key-0123456789 "), + Message.SubmittedCredential(), + ) expect(saving.commands).toMatchObject([ { name: "SaveOpenRouterCostKey", args: { apiKey: "sk-or-v1-regular-key-0123456789" } }, ]) // A key the request would refuse to carry is never sent, and the dialog says why. - const oversized = pageUpdate(typed(form, `sk-or-v1-${Str.repeat(300)("a")}`), Message.SubmittedKey()) + const oversized = pageUpdate( + typed(form, `sk-or-v1-${Str.repeat(300)("a")}`), + Message.SubmittedCredential(), + ) expect(names(oversized.commands)).toEqual([]) - expect([oversized.model.sourceWrite, oversized.model.keyError]).toEqual([ + expect([oversized.model.sourceWrite, oversized.model.credentialError]).toEqual([ "Idle", "That is too long to be an OpenRouter key.", ]) // A second submit while the first is in flight sends nothing more. - expect(names(pageUpdate(saving.model, Message.SubmittedKey()).commands)).toEqual([]) + expect(names(pageUpdate(saving.model, Message.SubmittedCredential()).commands)).toEqual([]) const refused = pageUpdate( saving.model, - Message.CompletedSaveKey({ + Message.CompletedSaveCredential({ provider: "openrouter", result: Result.fail({ ...toLoadFailure("refused"), @@ -251,7 +259,7 @@ describe("cost page", () => { "This is a regular API key. Reading spend needs a management key.", ), ) - expect(typed(refused, "sk-or-v1-management-key-0123456789").keyError).toBeNull() + expect(typed(refused, "sk-or-v1-management-key-0123456789").credentialError).toBeNull() }) test("a saved key closes the dialog, says when the sync runs and reads the sources again", () => { @@ -262,19 +270,19 @@ describe("cost page", () => { const saving = pageUpdate( pageUpdate( form, - Message.GotKeyDraftMessage({ + Message.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value: "sk-or-v1-management-key-0123456789" }), }), ).model, - Message.SubmittedKey(), + Message.SubmittedCredential(), ).model const saved = pageUpdate( saving, - Message.CompletedSaveKey({ provider: "openrouter", result: Result.succeed(undefined) }), + Message.CompletedSaveCredential({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(saved.model.addDialog.isOpen).toBe(false) // The key does not outlive the dialog. - expect(saved.model.keyDraft.value).toBe("") + expect(saved.model.credentialDraft.value).toBe("") expect(names(saved.commands)).toContain("FetchCostSources") expect(saved.outMessage).toEqual( PageOutMessage.RequestedToast({ @@ -312,7 +320,10 @@ describe("cost page", () => { ) // Replace opens the key form with no providers to go back to. - const replacing = pageUpdate(model, Message.ClickedReplaceKey({ provider: "openrouter" })).model + const replacing = pageUpdate( + model, + Message.ClickedReplaceCredential({ provider: "openrouter" }), + ).model scene( { update: pageUpdate, view }, given(replacing), @@ -337,7 +348,7 @@ describe("cost page", () => { expect(names(removing.commands)).toEqual(["RemoveOpenRouterCostKey"]) const removed = pageUpdate( removing.model, - Message.CompletedRemoveKey({ provider: "openrouter", result: Result.succeed(undefined) }), + Message.CompletedRemoveCredential({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(removed.model.removeDialog.isOpen).toBe(false) // The reads name the org's sources: without reading them again the removed source's cost stays. @@ -382,7 +393,7 @@ describe("cost page", () => { const staleKey = reloading.sources.key ?? "" const afterWrite = pageUpdate( reloading, - Message.CompletedRemoveKey({ provider: "openrouter", result: Result.succeed(undefined) }), + Message.CompletedRemoveCredential({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(names(afterWrite.commands)).toContain("FetchCostSources") const stale = pageUpdate( @@ -400,13 +411,13 @@ describe("cost page", () => { pageUpdate( pageUpdate( replacing, - Message.GotKeyDraftMessage({ + Message.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value: "sk-or-v1-management-key-0123456789" }), }), ).model, - Message.SubmittedKey(), + Message.SubmittedCredential(), ).model, - Message.CompletedSaveKey({ provider: "openrouter", result: Result.succeed(undefined) }), + Message.CompletedSaveCredential({ provider: "openrouter", result: Result.succeed(undefined) }), ) expect(replaced.outMessage).toEqual( PageOutMessage.RequestedToast({ @@ -446,23 +457,28 @@ describe("cost page", () => { expectView(role("button", { name: "Back" })).toExist(), ) const typed = (model: Model, value: string) => - pageUpdate(model, Message.GotKeyDraftMessage({ message: Field.Message.ChangedValue({ value }) })) - .model + pageUpdate( + model, + Message.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value }) }), + ).model const saving = pageUpdate( typed(form, " cfut_0123456789abcdefghij0123456789 "), - Message.SubmittedKey(), + Message.SubmittedCredential(), ) expect(saving.commands).toMatchObject([ { name: "SaveCloudflareCostToken", args: { apiToken: "cfut_0123456789abcdefghij0123456789" } }, ]) - const oversized = pageUpdate(typed(form, `cfut_${Str.repeat(300)("a")}`), Message.SubmittedKey()) + const oversized = pageUpdate( + typed(form, `cfut_${Str.repeat(300)("a")}`), + Message.SubmittedCredential(), + ) expect(names(oversized.commands)).toEqual([]) - expect(oversized.model.keyError).toBe("That is too long to be a Cloudflare API token.") + expect(oversized.model.credentialError).toBe("That is too long to be a Cloudflare API token.") const refused = pageUpdate( saving.model, - Message.CompletedSaveKey({ + Message.CompletedSaveCredential({ provider: "cloudflare", result: Result.fail({ ...toLoadFailure("refused"), @@ -483,10 +499,10 @@ describe("cost page", () => { const saved = pageUpdate( saving.model, - Message.CompletedSaveKey({ provider: "cloudflare", result: Result.succeed(undefined) }), + Message.CompletedSaveCredential({ provider: "cloudflare", result: Result.succeed(undefined) }), ) expect(saved.model.addDialog.isOpen).toBe(false) - expect(saved.model.keyDraft.value).toBe("") + expect(saved.model.credentialDraft.value).toBe("") expect(names(saved.commands)).toContain("FetchCostSources") expect(saved.outMessage).toEqual( PageOutMessage.RequestedToast({ @@ -524,7 +540,10 @@ describe("cost page", () => { expectView(role("button", { name: "Replace key" })).toExist(), ) - const replacing = pageUpdate(model, Message.ClickedReplaceKey({ provider: "cloudflare" })).model + const replacing = pageUpdate( + model, + Message.ClickedReplaceCredential({ provider: "cloudflare" }), + ).model scene( { update: pageUpdate, view }, given(replacing), @@ -538,13 +557,13 @@ describe("cost page", () => { pageUpdate( pageUpdate( replacing, - Message.GotKeyDraftMessage({ + Message.GotCredentialDraftMessage({ message: Field.Message.ChangedValue({ value: "cfut_0123456789abcdefghij0123456789" }), }), ).model, - Message.SubmittedKey(), + Message.SubmittedCredential(), ).model, - Message.CompletedSaveKey({ provider: "cloudflare", result: Result.succeed(undefined) }), + Message.CompletedSaveCredential({ provider: "cloudflare", result: Result.succeed(undefined) }), ) expect(replaced.outMessage).toEqual( PageOutMessage.RequestedToast({ @@ -577,7 +596,7 @@ describe("cost page", () => { expect(names(removing.commands)).toEqual(["RemoveCloudflareCostToken"]) const removed = pageUpdate( removing.model, - Message.CompletedRemoveKey({ provider: "cloudflare", result: Result.succeed(undefined) }), + Message.CompletedRemoveCredential({ provider: "cloudflare", result: Result.succeed(undefined) }), ) expect(removed.outMessage).toEqual( PageOutMessage.RequestedToast({ type: "success", title: "Cloudflare removed", description: "" }), diff --git a/apps/web/src/page/cost/credential-sources.ts b/apps/web/src/page/cost/credential-sources.ts index 7a5ce35132..d1bd448072 100644 --- a/apps/web/src/page/cost/credential-sources.ts +++ b/apps/web/src/page/cost/credential-sources.ts @@ -14,7 +14,7 @@ type Cmd = Command.Command /** * A cost source added by pasting a credential: everything its dialog steps, its Sources row and - * its toasts say, and the commands that save and remove it. A new provider is one entry here. + * its toasts say, and the commands that save and remove it. The form itself is shared. */ export type CredentialSource = Readonly<{ /** The line under the provider's name in the list, and once its source exists. */ @@ -117,8 +117,4 @@ export const CREDENTIAL_SOURCES = { save: (apiToken) => SaveCloudflareCostToken({ apiToken }), delete: () => RemoveCloudflareCostToken(), }, -} as const satisfies Record - -/** `provider`'s entry, as the shape every entry shares. */ -export const credentialSource = (provider: CredentialProvider): CredentialSource => - CREDENTIAL_SOURCES[provider] +} satisfies Record diff --git a/apps/web/src/page/cost/derive.ts b/apps/web/src/page/cost/derive.ts index 7a4c82c6e5..2e1ed9ea10 100644 --- a/apps/web/src/page/cost/derive.ts +++ b/apps/web/src/page/cost/derive.ts @@ -296,3 +296,7 @@ export const requestsCost = (provider: CostProvider, requests: number): string | const { requestUsd } = COST_PROVIDERS[provider] return requestUsd === null ? null : `USD ${(requests * requestUsd).toFixed(2)}` } + +/** Whether the org has a source of `provider`: a pasted-credential provider has one at most. */ +export const hasSourceOf = (sources: ReadonlyArray, provider: CostProvider): boolean => + Arr.some(sources, (source) => source.provider === provider) diff --git a/apps/web/src/page/cost/message.ts b/apps/web/src/page/cost/message.ts index 2f39af7483..cb10105665 100644 --- a/apps/web/src/page/cost/message.ts +++ b/apps/web/src/page/cost/message.ts @@ -51,15 +51,21 @@ export const Message = defineMessageUnion({ PickedProvider: { provider: CredentialProvider }, ClickedBackToProviders: {}, /** Opens the dialog on the provider's form, for the source that exists. */ - ClickedReplaceKey: { provider: CredentialProvider }, + ClickedReplaceCredential: { provider: CredentialProvider }, GotAddDialogMessage: { message: Dialog.Message }, - GotKeyDraftMessage: { message: Field.Message }, - SubmittedKey: {}, - CompletedSaveKey: { provider: CredentialProvider, result: Schema.Result(Schema.Void, LoadFailure) }, + GotCredentialDraftMessage: { message: Field.Message }, + SubmittedCredential: {}, + CompletedSaveCredential: { + provider: CredentialProvider, + result: Schema.Result(Schema.Void, LoadFailure), + }, ClickedRemoveSource: { provider: CredentialProvider }, GotRemoveDialogMessage: { message: Dialog.Message }, ClickedConfirmRemove: {}, - CompletedRemoveKey: { provider: CredentialProvider, result: Schema.Result(Schema.Void, LoadFailure) }, + CompletedRemoveCredential: { + provider: CredentialProvider, + result: Schema.Result(Schema.Void, LoadFailure), + }, ...ChartEnvMessages, }) export type Message = typeof Message.Type diff --git a/apps/web/src/page/cost/model.ts b/apps/web/src/page/cost/model.ts index ab8a0a5e91..19f6e209e4 100644 --- a/apps/web/src/page/cost/model.ts +++ b/apps/web/src/page/cost/model.ts @@ -42,12 +42,12 @@ export const Model = Schema.Struct({ addDialog: Dialog.Model, addStep: AddStep, /** The credential being typed (a key or a token); cleared whichever way the dialog closes. */ - keyDraft: Field.Model, + credentialDraft: Field.Model, /** Why the last save was refused, shown under the field until the credential changes. */ - keyError: Schema.NullOr(Schema.String), + credentialError: Schema.NullOr(Schema.String), removeDialog: Dialog.Model, - /** The provider whose source the remove dialog asks about. */ - removeProvider: CredentialProvider, + /** The provider whose source the remove dialog asks about; null until it first opens. */ + removeProvider: Schema.NullOr(CredentialProvider), /** `Settling`: the write answered and the sources are being read again; the list on show is the old one. */ sourceWrite: Schema.Literals(["Idle", "Saving", "Removing", "Settling"]), }) diff --git a/apps/web/src/page/cost/update.ts b/apps/web/src/page/cost/update.ts index 497dffbc99..11438fd069 100644 --- a/apps/web/src/page/cost/update.ts +++ b/apps/web/src/page/cost/update.ts @@ -34,6 +34,7 @@ import { import { DEFAULT_GROUP_BY, groupByOf, + hasSourceOf, hasSynced, isUnderADay, requestFilters, @@ -42,7 +43,7 @@ import { withoutFilter, withoutFilters, } from "./derive" -import { credentialSource } from "./credential-sources" +import { CREDENTIAL_SOURCES } from "./credential-sources" import { Message } from "./message" import type { AddStep, Model, Search } from "./model" import type { CredentialProvider } from "./providers" @@ -147,10 +148,10 @@ export const init = (route: { readonly search: Search }, context: PageContext): grants: isClerkAuthEnabled ? null : Policy.allPermissions, addDialog: Dialog.init("cost-add-source", "dialog"), addStep: "pick", - keyDraft: Field.init(), - keyError: null, + credentialDraft: Field.init(), + credentialError: null, removeDialog: Dialog.init("cost-remove-source", "alert-dialog"), - removeProvider: "openrouter", + removeProvider: null, sourceWrite: "Idle", } return chain( @@ -184,12 +185,16 @@ const liftRemoveDialog = (model: Model, step: ReturnType): }) /** Nothing typed and no refusal on show: how the dialog opens and what it leaves behind. */ -const clearedKey = (model: Model): Model => ({ ...model, keyDraft: Field.init(), keyError: null }) +const clearedCredential = (model: Model): Model => ({ + ...model, + credentialDraft: Field.init(), + credentialError: null, +}) const openAdd = (model: Model, addStep: AddStep): Return => model.sourceWrite !== "Idle" ? { model } - : liftAddDialog({ ...clearedKey(model), addStep }, Dialog.open(model.addDialog)) + : liftAddDialog({ ...clearedCredential(model), addStep }, Dialog.open(model.addDialog)) /** * The sources and the window's reads again, after a write's own step: the reads name the org's @@ -221,10 +226,8 @@ const withoutSourceOf = (model: Model, provider: CredentialProvider): Model => ( }) /** Whether the org has `provider`'s source, which a saved credential then replaces the credential of. */ -const hasSourceOf = (model: Model, provider: CredentialProvider): boolean => - Option.exists(AsyncData.getData(model.sources.data), ({ sources }) => - Arr.some(sources, (source) => source.provider === provider), - ) +const hasSource = (model: Model, provider: CredentialProvider): boolean => + Option.exists(AsyncData.getData(model.sources.data), ({ sources }) => hasSourceOf(sources, provider)) export const routeChanged = ( model: Model, @@ -344,56 +347,58 @@ export const update = (model: Model, message: Message, context: PageContext): Re }, CompletedReadGrants: ({ grants }) => ({ model: { ...model, grants } }), ClickedAddSource: () => openAdd(model, "pick"), - PickedProvider: ({ provider }) => ({ model: { ...clearedKey(model), addStep: provider } }), + PickedProvider: ({ provider }) => ({ model: { ...clearedCredential(model), addStep: provider } }), ClickedBackToProviders: () => - model.sourceWrite === "Idle" ? { model: { ...clearedKey(model), addStep: "pick" } } : { model }, - ClickedReplaceKey: ({ provider }) => openAdd(model, provider), + model.sourceWrite === "Idle" + ? { model: { ...clearedCredential(model), addStep: "pick" } } + : { model }, + ClickedReplaceCredential: ({ provider }) => openAdd(model, provider), // A save in flight keeps the dialog: its answer decides whether it closes. GotAddDialogMessage: ({ message: inner }) => { if (model.sourceWrite === "Saving") return { model } const step = Dialog.update(model.addDialog, inner) const lifted = liftAddDialog(model, step) return step.outMessage?._tag === "Closed" - ? { ...lifted, model: clearedKey(lifted.model) } + ? { ...lifted, model: clearedCredential(lifted.model) } : lifted }, - GotKeyDraftMessage: ({ message: inner }) => + GotCredentialDraftMessage: ({ message: inner }) => model.sourceWrite === "Saving" ? { model } : { model: { ...model, - keyDraft: Field.update(model.keyDraft, inner).model, + credentialDraft: Field.update(model.credentialDraft, inner).model, // The refusal was about the key that was there. - keyError: inner._tag === "ChangedValue" ? null : model.keyError, + credentialError: inner._tag === "ChangedValue" ? null : model.credentialError, }, }, - SubmittedKey: () => { - const secret = Str.trim(model.keyDraft.value) + SubmittedCredential: () => { + const secret = Str.trim(model.credentialDraft.value) if (secret === "" || model.sourceWrite !== "Idle" || model.addStep === "pick") return { model } - const source = credentialSource(model.addStep) + const source = CREDENTIAL_SOURCES[model.addStep] // The request refuses a longer one before it is sent, which would leave the save unanswered. if (secret.length > source.field.maxLength) { - return { model: { ...model, keyError: source.field.tooLong } } + return { model: { ...model, credentialError: source.field.tooLong } } } return { - model: { ...model, sourceWrite: "Saving", keyError: null }, + model: { ...model, sourceWrite: "Saving", credentialError: null }, commands: [source.save(secret)], } }, // A refused credential keeps the dialog open with the reason under the field. - CompletedSaveKey: ({ provider, result }) => + CompletedSaveCredential: ({ provider, result }) => Result.match(result, { onFailure: (failure): Return => ({ - model: { ...model, sourceWrite: "Idle", keyError: failureMessage(failure) }, + model: { ...model, sourceWrite: "Idle", credentialError: failureMessage(failure) }, }), onSuccess: (): Return => { const closed = liftAddDialog( - clearedKey({ ...model, sourceWrite: "Settling" }), + clearedCredential({ ...model, sourceWrite: "Settling" }), Dialog.close(model.addDialog), ) - const source = credentialSource(provider) - const outMessage = hasSourceOf(model, provider) + const source = CREDENTIAL_SOURCES[provider] + const outMessage = hasSource(model, provider) ? toast("success", source.replace.toast, "The next sync runs within a few minutes.") : toast("success", source.add.toast, "The first sync runs within a few minutes.") return withReread({ ...closed, outMessage }, context) @@ -408,13 +413,13 @@ export const update = (model: Model, message: Message, context: PageContext): Re ? { model } : liftRemoveDialog(model, Dialog.update(model.removeDialog, inner)), ClickedConfirmRemove: () => - model.sourceWrite !== "Idle" + model.sourceWrite !== "Idle" || model.removeProvider === null ? { model } : { model: { ...model, sourceWrite: "Removing" }, - commands: [credentialSource(model.removeProvider).delete()], + commands: [CREDENTIAL_SOURCES[model.removeProvider].delete()], }, - CompletedRemoveKey: ({ provider, result }) => { + CompletedRemoveCredential: ({ provider, result }) => { const closed = liftRemoveDialog( Result.isSuccess(result) ? withoutSourceOf({ ...model, sourceWrite: "Settling" }, provider) @@ -428,7 +433,10 @@ export const update = (model: Model, message: Message, context: PageContext): Re }), onSuccess: (): Return => withReread( - { ...closed, outMessage: toast("success", credentialSource(provider).remove.toast) }, + { + ...closed, + outMessage: toast("success", CREDENTIAL_SOURCES[provider].remove.toast), + }, context, ), }) diff --git a/apps/web/src/page/cost/view-add-source.ts b/apps/web/src/page/cost/view-add-source.ts index 9644d37620..33b8c4b09a 100644 --- a/apps/web/src/page/cost/view-add-source.ts +++ b/apps/web/src/page/cost/view-add-source.ts @@ -15,7 +15,8 @@ import { vstack } from "@maple/views/ui/stack" import { textLink } from "@maple/views/ui/text-link" import { typography } from "@maple/views/ui/typography" import { AppRoute, hrefOf } from "../../route" -import { type CredentialSource, credentialSource } from "./credential-sources" +import { CREDENTIAL_SOURCES, type CredentialSource } from "./credential-sources" +import { hasSourceOf } from "./derive" import { Message } from "./message" import type { Model } from "./model" import { COST_PROVIDERS, type CostProviderEntry, CredentialProvider } from "./providers" @@ -27,10 +28,6 @@ const CONNECT_AWS_HREF = hrefOf(AppRoute.SourcesNew({ search: { integration: "aw /** Where an AWS connection and its Cost module are changed. */ export const AWS_INTEGRATION_HREF = hrefOf(AppRoute.Integrations({ search: { integration: "aws" } })) -/** Whether the org has `provider`'s source, which there is at most one of. */ -const hasSourceOf = (sources: ReadonlyArray, provider: CredentialProvider): boolean => - Arr.some(sources, (source) => source.provider === provider) - const mark = (h: H, provider: CostProviderEntry): Html => icon(h, provider.icon, { size: 18, className: cn("shrink-0", provider.iconClassName) }) @@ -42,7 +39,7 @@ const credentialRow = ( isLast: boolean, ): Html => { const entry: CostProviderEntry = COST_PROVIDERS[provider] - const { pick } = credentialSource(provider) + const { pick } = CREDENTIAL_SOURCES[provider] return hasSourceOf(sources, provider) ? listRow(h, { divided: !isLast, @@ -61,6 +58,15 @@ const credentialRow = ( }) } +/** The providers that bill nothing for the requests Maple makes, by name. */ +const FREE_REQUESTS = Arr.join( + Arr.map( + Arr.filter(CredentialProvider.literals, (provider) => COST_PROVIDERS[provider].requestUsd === null), + (provider) => COST_PROVIDERS[provider].name, + ), + " and ", +) + /** The providers to add: AWS goes to its connect flow, the others to their credential form. */ const providers = (h: H, sources: ReadonlyArray): Html => vstack(h, { gap: "3" }, [ @@ -78,7 +84,7 @@ const providers = (h: H, sources: ReadonlyArray): Html => ), ]), typography(h, { size: "xs", tone: "muted", as: "p", className: "font-prose" }, [ - "AWS bills USD\u00a00.01 for each request Maple makes, a few dollars a month per account. OpenRouter and Cloudflare requests are free. ", + `AWS bills USD\u00a00.01 for each request Maple makes, a few dollars a month per account. ${FREE_REQUESTS} requests are free. `, textLink(h, { href: AWS_INTEGRATION_HREF, tone: "inherit", className: "underline" }, [ "Turn on Cost for an AWS account that is connected", ]), @@ -86,34 +92,38 @@ const providers = (h: H, sources: ReadonlyArray): Html => ]), ]) -const KEY_IDS: FieldIds = { +const CREDENTIAL_IDS: FieldIds = { control: "cost-source-credential", label: "cost-source-credential-label", error: "cost-source-credential-error", description: "cost-source-credential-description", } -const keyField = (h: H, model: Model, { field: text }: CredentialSource): Html => { - const state = Field.stateOf(model.keyDraft, model.keyError !== null) +const credentialField = (h: H, model: Model, { field: text }: CredentialSource): Html => { + const state = Field.stateOf(model.credentialDraft, model.credentialError !== null) return field(h, { state }, [ - fieldLabel(h, { ids: KEY_IDS, state }, text.label), + fieldLabel(h, { ids: CREDENTIAL_IDS, state }, text.label), input(h, { - ids: KEY_IDS, + ids: CREDENTIAL_IDS, state, attributes: [ h.Type("password"), h.Attribute("autocomplete", "off"), h.Placeholder(text.placeholder), ...(model.sourceWrite === "Saving" ? [h.Disabled(true)] : []), - ...Field.controlAttributes(h, model.keyDraft, (message) => - Message.GotKeyDraftMessage({ message }), + ...Field.controlAttributes(h, model.credentialDraft, (message) => + Message.GotCredentialDraftMessage({ message }), ), ], }), - ...(model.keyError === null + ...(model.credentialError === null ? [] - : [fieldError(h, { ids: KEY_IDS, state, attributes: [h.Role("alert")] }, [model.keyError])]), - fieldDescription(h, { ids: KEY_IDS, state }, [ + : [ + fieldError(h, { ids: CREDENTIAL_IDS, state, attributes: [h.Role("alert")] }, [ + model.credentialError, + ]), + ]), + fieldDescription(h, { ids: CREDENTIAL_IDS, state }, [ text.help.before, textLink( h, @@ -139,17 +149,17 @@ const credentialForm = ( close: Parameters[0], ): Html => { const id = model.addDialog.id - const source = credentialSource(provider) + const source = CREDENTIAL_SOURCES[provider] const isReplacing = hasSourceOf(sources, provider) const isSaving = model.sourceWrite === "Saving" - const canSubmit = !isSaving && Str.trim(model.keyDraft.value) !== "" + const canSubmit = !isSaving && Str.trim(model.credentialDraft.value) !== "" const text = isReplacing ? source.replace : source.add - return formDialogForm(h, canSubmit ? Message.SubmittedKey() : undefined, [ + return formDialogForm(h, canSubmit ? Message.SubmittedCredential() : undefined, [ DialogView.dialogHeader(h, "dialog", [ DialogView.dialogTitle(h, "dialog", id, text.title), DialogView.dialogDescription(h, "dialog", id, text.description), ]), - DialogView.dialogPanel(h, "dialog", id, [keyField(h, model, source)]), + DialogView.dialogPanel(h, "dialog", id, [credentialField(h, model, source)]), DialogView.dialogFooter(h, "dialog", [ // A replace has no providers to go back to. isReplacing @@ -214,7 +224,8 @@ export const addSourceDialog = (h: H, model: Model, sources: ReadonlyArray { - const { remove } = credentialSource(model.removeProvider) + if (model.removeProvider === null) return h.empty + const { remove } = CREDENTIAL_SOURCES[model.removeProvider] return confirmDialog(h, { slotId: "cost-remove-source", model: model.removeDialog, diff --git a/apps/web/src/page/cost/view-sources.ts b/apps/web/src/page/cost/view-sources.ts index e2ea3b4863..5e91d8543f 100644 --- a/apps/web/src/page/cost/view-sources.ts +++ b/apps/web/src/page/cost/view-sources.ts @@ -11,7 +11,7 @@ import { stripClass } from "@maple/views/ui/rule" import { hstack, vstack } from "@maple/views/ui/stack" import { typography } from "@maple/views/ui/typography" import { relativeTime } from "../kit/relative-time" -import { credentialSource } from "./credential-sources" +import { CREDENTIAL_SOURCES } from "./credential-sources" import { formatDay, requestsCost } from "./derive" import { Message } from "./message" import { COST_PROVIDERS, type CostProviderEntry, credentialProviderOf } from "./providers" @@ -43,9 +43,9 @@ const actions = (h: H, source: CostSourceSummary, isBusy: boolean): ReadonlyArra variant: "ghost", size: "sm", isDisabled: isBusy, - attributes: [h.OnClick(Message.ClickedReplaceKey({ provider }))], + attributes: [h.OnClick(Message.ClickedReplaceCredential({ provider }))], }, - [credentialSource(provider).replace.label], + [CREDENTIAL_SOURCES[provider].replace.label], ), button( h, diff --git a/apps/web/src/page/cost/view.ts b/apps/web/src/page/cost/view.ts index da02910567..4fabf97647 100644 --- a/apps/web/src/page/cost/view.ts +++ b/apps/web/src/page/cost/view.ts @@ -1,3 +1,4 @@ +import type { CostProvider } from "@maple/domain/cost" import type { CostSourceSummary } from "@maple/domain/http" import { Option, Array as Arr } from "effect" import { AsyncData, Submodel } from "foldkit" @@ -107,9 +108,15 @@ const onRamp = ( const MONTHS_SCOPE = "this month and the last, and older months fill in after that" +const namesOf = (providers: ReadonlyArray): string => + Arr.join( + Arr.map(providers, (provider) => COST_PROVIDERS[provider].name), + " and ", + ) + /** * What the first syncs bring in, by the providers that have no cost yet (waiting or failing - * alike): AWS and Cloudflare keep a year or more, OpenRouter four weeks. + * alike): some keep a year or more, some four weeks. */ const firstSyncScope = (sources: ReadonlyArray): string => { const unsynced = Arr.dedupe( @@ -118,14 +125,14 @@ const firstSyncScope = (sources: ReadonlyArray): string => { (source) => source.provider, ), ) - const months = Arr.filter(unsynced, (provider) => COST_PROVIDERS[provider].firstSync === "months") - if (months.length === unsynced.length) return `It brings in ${MONTHS_SCOPE}.` - if (months.length === 0) return "It brings in the last four weeks, which is all OpenRouter keeps." - const names = Arr.join( - Arr.map(months, (provider) => COST_PROVIDERS[provider].name), - " and ", - ) - return `${names} ${months.length === 1 ? "brings" : "bring"} in ${MONTHS_SCOPE}. OpenRouter brings in its last four weeks.` + const keepsWeeks = (provider: CostProvider) => COST_PROVIDERS[provider].firstSync === "four_weeks" + const weeks = Arr.filter(unsynced, keepsWeeks) + const months = Arr.filter(unsynced, (provider) => !keepsWeeks(provider)) + if (weeks.length === 0) return `It brings in ${MONTHS_SCOPE}.` + if (months.length === 0) { + return `It brings in the last four weeks, which is all ${namesOf(weeks)} ${weeks.length === 1 ? "keeps" : "keep"}.` + } + return `${namesOf(months)} ${months.length === 1 ? "brings" : "bring"} in ${MONTHS_SCOPE}. ${namesOf(weeks)} ${weeks.length === 1 ? "brings in its" : "bring in their"} last four weeks.` } /** Sources exist and none has cost yet: what the reader is waiting for. */ diff --git a/docs/cloud-cost-plan.md b/docs/cloud-cost-plan.md index 6a32b88031..af0467d92a 100644 --- a/docs/cloud-cost-plan.md +++ b/docs/cloud-cost-plan.md @@ -50,8 +50,8 @@ not read through an integration's connection keeps a pasted credential, and and provider, with the sealed credential, the vendor's account id and a label. `CostConnectionStore` seals, upserts (refusing a credential of another account), removes and loads it, and `reconcileCostConnectionSources` keeps `cost_sources` in step for each provider named in -`COST_CONNECTION_PROVIDERS`. A new provider of this kind adds its literal there and needs no -migration. +`COST_CONNECTION_PROVIDERS`. A new provider of this kind needs no migration, slot or reconcile: it +adds its literal there, and brings its own vendor check, adapter and endpoints. ```ts interface CostProvider { @@ -265,6 +265,9 @@ currency the figures are in and lists the others. Nothing is added across curren - Live: yesterday is served, today is not, and the oldest day served is one year before today. The profile asks from the first day of the eleventh month back through yesterday. The two newest days are stored as not final. +- Live: a range that reaches past the year is answered, not refused, with the older days left out, + and a period that started before the year still returns its days inside it. So the 31 days asked + before the oldest month cost nothing. - Live: a wrong or revoked token answers 401 (sync reason `auth`), a token without billing access 403 (`not_ready`). A 400 is a request Maple wrote wrong (`upstream`). - Rows: the product family (`Workers`, `R2`, `D1` ...) is `CloudService`, the priced metric is @@ -278,8 +281,15 @@ currency the figures are in and lists the others. Nothing is added across curren period's start. - Assumed: Cloudflare reports the current period only, so earlier months use today's price. A plan whose price changed shows the new price for its past months too. -- Open: whether a subscription in trial is charged its listed price, and whether a completed day - is ever restated. The four newest days are pulled again daily. +- Schema: a subscription's `state` is one of `Trial`, `Provisioned`, `Paid`, `AwaitingPayment`, + `Cancelled`, `Failed`, `Expired`. Live: every listed one was `Paid`, free items included. + Assumed: `Trial`, `Failed` and `Expired` were not charged, so they get no fee row. +- Known limit: a fee is known only while its subscription is listed. If Cloudflare drops a + cancelled subscription from the list, the next re-pull of the open months stores those days + without its fee. Months already walked as history keep theirs. +- More than 500 subscriptions (10 pages) fail the pull as an answer Maple cannot read, since part + of the list would under-count. +- Open: whether a completed day is ever restated. The four newest days are pulled again daily. - Not built: enterprise contracts, zone-level analytics, the restricted "Version 2" endpoints, invoices and credits. @@ -319,8 +329,10 @@ Each phase is one PR stack. than they use. 3. **More providers.** OpenRouter and Cloudflare are built. Railway, GCP, PlanetScale, Supabase, one PR each. Each provider adds a literal to `CostProvider`, an adapter and a sync profile. One - read with a pasted credential also adds itself to `COST_CONNECTION_PROVIDERS` and to the Cost - page's `CREDENTIAL_SOURCES`; one read through a connection adds a reconcile function. + read with a pasted credential also adds itself to `COST_CONNECTION_PROVIDERS`, a connection + service that checks the credential with the vendor, its `PUT` / `DELETE` endpoints, and on the + Cost page two commands and an entry in `CREDENTIAL_SOURCES`. One read through a connection adds + a reconcile function instead. 4. **Closing the loop.** Budgets and cost anomalies as managed alert signals, a cost block in the weekly digest, cost per request on service pages, realized savings once an accepted recommendation stops firing. diff --git a/packages/backend/src/services/cost/CloudflareCostConnectionService.ts b/packages/backend/src/services/cost/CloudflareCostConnectionService.ts index 9f9e38f055..e679589788 100644 --- a/packages/backend/src/services/cost/CloudflareCostConnectionService.ts +++ b/packages/backend/src/services/cost/CloudflareCostConnectionService.ts @@ -1,8 +1,6 @@ -/** - * The Cloudflare API token an org's Cloudflare charges are read with: checked with Cloudflare, - * then kept in the shared cost connection store. Saving it makes the org's Cloudflare cost source, - * replacing it keeps the source and its history, removing it removes the source. - */ +// The Cloudflare API token an org's Cloudflare charges are read with: checked with Cloudflare, then +// kept in the shared cost connection store. Saving it makes the org's Cloudflare cost source, +// replacing it keeps the source and its history, removing it removes the source. import { CLOUDFLARE_COST_TOKEN_MAX_LENGTH, CloudCostDisabledError, @@ -20,9 +18,6 @@ import { addDays, utcToday } from "./sync-plan" const VALIDATE_TIMEOUT = Duration.seconds(20) -/** What the usage read of the check asks for: enough days back to include a billing period's start. */ -const CHECK_DAYS = 32 - const NO_BILLING_ACCESS = "This token cannot read the account's billing. Create a custom token with the Billing: Read permission." @@ -76,17 +71,7 @@ export class CloudflareCostConnectionService extends Context.Service< const asked = ( step: "accounts" | "billing", call: Effect.Effect, - ) => - call.pipe( - // The page shows the mapped message; which failure it was stays on the span. - Effect.tapError((error) => - Effect.annotateCurrentSpan({ - "maple.cost.cloudflare.error": error.kind, - "maple.cost.cloudflare.status": error.status ?? 0, - }), - ), - Effect.mapError(toTokenError(step)), - ) + ) => Effect.mapError(call, toTokenError(step)) /** * The one account the token reads, once it has proved it can read that account's billing: @@ -110,17 +95,16 @@ export class CloudflareCostConnectionService extends Context.Service< CloudflareBilling.getBillableUsageInfo(httpClient, token, account.id), ) if (!info.covered) { - return yield* refused( - "Cloudflare has no usage-based billing data for this account. Accounts on an enterprise contract are not supported yet.", - ) + return yield* refused(CloudflareBilling.NOT_COVERED) } const now = yield* Clock.currentTimeMillis - const today = utcToday(now) + // Yesterday is the newest day Cloudflare serves. + const yesterday = addDays(utcToday(now), -1) yield* asked( "billing", CloudflareBilling.getBillableUsage(httpClient, token, account.id, { - from: addDays(today, -CHECK_DAYS), - to: today, + startDay: yesterday, + endDay: yesterday, }), ) return { accountId: account.id, accountLabel: account.name } diff --git a/packages/backend/src/services/cost/CostConnectionStore.ts b/packages/backend/src/services/cost/CostConnectionStore.ts index efb3930485..8dcad146d4 100644 --- a/packages/backend/src/services/cost/CostConnectionStore.ts +++ b/packages/backend/src/services/cost/CostConnectionStore.ts @@ -1,9 +1,6 @@ -/** - * Where a pasted cost credential lives: sealed in `cost_connections`, one per org and provider. - * Saving one makes the org's cost source for that provider, replacing it keeps the source and its - * history, removing it removes the source. Each provider checks its credential with its vendor - * first, then stores it here, and its adapter reads it back for a pull. - */ +// Where a pasted cost credential lives: sealed in `cost_connections`, one per org and provider. +// Saving one makes the provider's cost source, replacing it keeps the source and its history, +// removing it removes the source. A provider checks its credential with its vendor before saving. import * as PG from "@maple-dev/effect-orm/postgres" import { CostConnections, CostSources } from "@maple/db/tables" import type { OrgId, UserId } from "@maple/primitives" @@ -14,7 +11,7 @@ import { makeDbExecute } from "@maple/backend/platform/db-execute" import { Env } from "@maple/backend/platform/Env" import { openSecret, sealSecret, secretKeyringFromEnv } from "@maple/backend/platform/SecretBox" import { costConnectionSecretSlot } from "@maple/backend/platform/secret-slots" -import type { CostPullSource } from "./CostProvider" +import { CostProviderError, type CostPullSource } from "./CostProvider" import { type CostConnectionProvider, reconcileCostConnectionSources } from "./cost-connection-sources" /** `secret`: the deployment's key could not seal or open the value. `database`: Postgres failed. */ @@ -28,6 +25,17 @@ export class CostConnectionStoreError extends Schema.TaggedError new CostConnectionStoreError({ message, reason: "secret" }) +/** A failed `load`, as what the sync does next: a credential that will not open needs replacing. */ +export const loadFailureOf = + (provider: CostConnectionProvider) => + (error: CostConnectionStoreError): CostProviderError => + new CostProviderError({ + message: error.reason === "secret" ? error.message : "Cost connection database error", + provider, + reason: error.reason === "secret" ? "auth" : "upstream", + requests: 0, + }) + export interface CostConnectionInput { readonly orgId: OrgId readonly userId: UserId @@ -41,14 +49,12 @@ export interface CostConnectionInput { export interface CostConnectionCredential { readonly secret: Redacted.Redacted readonly accountId: string - readonly accountLabel: string } export interface CostConnectionStoreApi { /** - * Seals and saves the credential, and makes the provider's cost source. `other_account`: the - * org's source reads another account, whose days this one's spend would overwrite, so nothing - * is written. + * Seals and saves the credential, and makes the provider's cost source. `other_account`: the org's + * source reads another account, whose days this one's spend would overwrite, so nothing is written. */ readonly save: ( input: CostConnectionInput, @@ -200,7 +206,6 @@ export class CostConnectionStore extends Context.Service({ secret: Redacted.make(secret), accountId: row.accountId, - accountLabel: row.accountLabel, }) }) diff --git a/packages/backend/src/services/cost/cost-connection-sources.ts b/packages/backend/src/services/cost/cost-connection-sources.ts index aae74a6228..c7e9132b7b 100644 --- a/packages/backend/src/services/cost/cost-connection-sources.ts +++ b/packages/backend/src/services/cost/cost-connection-sources.ts @@ -1,7 +1,6 @@ -// Brings `cost_sources` in step with `cost_connections` for one provider that is read with a -// pasted credential: a source while the credential exists, named after its account, gone with it. -// The sync lane runs it for every org each tick; a credential write runs it for its own org, so -// the Cost page sees the source before the next tick. +// Brings `cost_sources` in step with `cost_connections` for one provider read with a pasted +// credential: a source while the credential exists, named after its account, gone with it. The +// sync lane runs it for every org each tick, a credential write for its own org at once. import type { MapleDb } from "@maple/db/client" import { CostConnections, CostSources } from "@maple/db/tables" import type { CostProvider } from "@maple/domain/cost" @@ -11,10 +10,7 @@ import * as PG from "@maple-dev/effect-orm/postgres" import { Array as Arr, Effect } from "effect" import { randomUUID } from "node:crypto" -/** - * The providers whose credential lives in `cost_connections`. The sync reconciles each one: a - * provider added here needs no table, slot or reconcile of its own. - */ +/** The providers whose credential lives in `cost_connections`: one added here needs no table, slot or reconcile of its own. */ export const COST_CONNECTION_PROVIDERS = ["cloudflare"] as const satisfies ReadonlyArray export type CostConnectionProvider = (typeof COST_CONNECTION_PROVIDERS)[number] diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts index d2b370e6d0..39e601d7ff 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts @@ -5,7 +5,7 @@ import { TestClock } from "effect/testing" import { cleanupTestDbs, createTestDb, queryFirstRow, type TestDb } from "@maple/backend/testing/test-pglite" import { CloudflareCostConnectionService } from "../../CloudflareCostConnectionService" import type { CostProviderError } from "../../CostProvider" -import { CloudflareCostProvider, piecesOf } from "./CloudflareCostProvider" +import { CloudflareCostProvider } from "./CloudflareCostProvider" import { FAKE_ACCOUNT, type FakeCloudflare, @@ -164,12 +164,8 @@ describe("CloudflareCostProvider", () => { Effect.gen(function* () { const connectionRef = yield* saveToken(testDb, world) const pulled = yield* pull(ORG, connectionRef, window) - assert.deepStrictEqual(piecesOf(window), [ - { startDay: "2025-12-01", endDay: "2026-01-24" }, - { startDay: "2026-01-25", endDay: "2026-03-20" }, - { startDay: "2026-03-21", endDay: "2026-03-31" }, - ]) - // The fake refuses a range over 90 days, as Cloudflare does. + // Three pieces of 55 days at most, each led by 31 days. The fake refuses a range over 90 + // days, as Cloudflare does. assert.deepStrictEqual(world.urls.slice(1, 4), [ `${ACCOUNT_PATH}/billable-usage?from=2025-10-31&to=2026-01-25`, `${ACCOUNT_PATH}/billable-usage?from=2025-12-25&to=2026-03-21`, @@ -188,6 +184,72 @@ describe("CloudflareCostProvider", () => { ) }) + it.effect("reads the oldest month it keeps, though the range it asks reaches past what is served", () => { + const testDb = createTestDb(trackedDbs) + // Cloudflare serves a year: the lead before the floor month asks for older days and gets none. + const world = makeFakeCloudflare({ + oldestDay: "2025-11-11", + anchorDay: 5, + usage: [ + usageRow("2025-11-10", "R2", "R2 Storage", 9), + // These days belong to the period that started on 5 November, before the oldest day. + usageRow("2025-12-01", "R2", "R2 Storage", 1), + usageRow("2025-12-04", "R2", "R2 Storage", 2), + usageRow("2025-12-31", "R2", "R2 Storage", 3), + ], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const pulled = yield* pull(ORG, connectionRef, { + startDay: "2025-12-01", + endDay: "2025-12-31", + }) + assert.strictEqual( + world.urls[1], + `${ACCOUNT_PATH}/billable-usage?from=2025-10-31&to=2026-01-01`, + ) + assert.deepStrictEqual( + Arr.map(pulled.rows, (row) => [row.day, row.billedCost]), + [ + ["2025-12-01", 1], + ["2025-12-04", 2], + ["2025-12-31", 3], + ], + ) + }), + ) + }) + + it.effect( + "fails a subscription listing longer than it reads, since part of the fees would under-count", + () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ + subscriptionsPageSize: 1, + subscriptions: Arr.makeBy(11, (index) => ({ + price: 5, + frequency: "monthly", + current_period_start: "2026-11-01T00:00:00Z", + rate_plan: { public_name: `Plan ${index}` }, + })), + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + assert.strictEqual( + (yield* Effect.flip(pull(ORG, connectionRef))).reason, + "invalid_response", + ) + }), + ) + }, + ) + it.effect("reads every page of the subscriptions", () => { const testDb = createTestDb(trackedDbs) const fee = (name: string) => ({ diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts index f1abb32111..dacc62c89d 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts @@ -11,18 +11,13 @@ import { type CostPullSource, type CostPullWindow, } from "../../CostProvider" -import { CostConnectionStore, type CostConnectionStoreError } from "../../CostConnectionStore" +import { CostConnectionStore, loadFailureOf } from "../../CostConnectionStore" import { addDays, utcToday } from "../../sync-plan" import * as CloudflareBilling from "./cloudflare-billing-api" import { toSubscriptionRows, toUsageRows } from "./normalize" -/** - * A billing period's rows come back only when the period starts inside the asked range, and a - * period is 31 days at most: asking from this many days before a day always includes its period. - */ -export const PERIOD_LEAD_DAYS = 31 -/** Days of a window one request reads: with the lead and the exclusive end, under the API's 90 day cap. */ -export const PIECE_DAYS = 55 +/** Days of a window one request reads: with the period it leads with, under the API's 90 day cap. */ +const PIECE_DAYS = 55 /** The newest days Cloudflare may still be counting. */ const OPEN_DAYS = 2 @@ -56,13 +51,8 @@ const toCostProviderError = (error: CloudflareBilling.CloudflareBillingError): C } } -const toStoreFailure = (error: CostConnectionStoreError): CostProviderError => - error.reason === "secret" - ? failed("auth", error.message) - : failed("upstream", "Cloudflare connection database error") - /** The window as consecutive runs of at most `PIECE_DAYS` days, oldest first. */ -export const piecesOf = (window: CostPullWindow): ReadonlyArray => +const piecesOf = (window: CostPullWindow): ReadonlyArray => Arr.unfold(window.startDay, (startDay): Option.Option => { if (startDay > window.endDay) return Option.none() const last = addDays(startDay, PIECE_DAYS - 1) @@ -87,43 +77,28 @@ export class CloudflareCostProvider extends Context.Service(call: Effect.Effect) => - call.pipe( - // The page shows the mapped message; which failure it was stays on the span. - Effect.tapError((error) => - Effect.annotateCurrentSpan({ - "maple.cost.cloudflare.error": error.kind, - "maple.cost.cloudflare.status": error.status ?? 0, - }), - ), - Effect.mapError(toCostProviderError), - ) + Effect.mapError(call, toCostProviderError) const info = yield* asked( CloudflareBilling.getBillableUsageInfo(httpClient, secret, accountId), ) if (!info.covered) { - return yield* failed( - "not_ready", - "Cloudflare has no usage-based billing data for this account. Accounts on an enterprise contract are not supported yet.", - ) + return yield* failed("not_ready", CloudflareBilling.NOT_COVERED) } const now = yield* Clock.currentTimeMillis const openDay = addDays(utcToday(now), -OPEN_DAYS) - // One request at a time. Each answer is cut to its own piece: it also holds the days - // of the lead, which belong to the piece before. + // One request at a time. Each answer is cut to its own piece: it also holds days from + // before it, which belong to the piece before. const usage = yield* Effect.forEach(piecesOf(window), (piece) => - asked( - CloudflareBilling.getBillableUsage(httpClient, secret, accountId, { - from: addDays(piece.startDay, -PERIOD_LEAD_DAYS), - to: addDays(piece.endDay, 1), - }), - ).pipe(Effect.map((items) => toUsageRows(items, accountId, piece, openDay))), + asked(CloudflareBilling.getBillableUsage(httpClient, secret, accountId, piece)).pipe( + Effect.map((items) => toUsageRows(items, accountId, piece, openDay)), + ), ) const subscriptions = yield* asked( CloudflareBilling.listSubscriptions(httpClient, secret, accountId), diff --git a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts index bae8ca1914..058b9c6926 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts @@ -1,17 +1,20 @@ -/** - * The slice of Cloudflare's REST API the cost source uses: the accounts a token reads, an - * account's daily billable usage and its subscriptions. Each call reads the status off a plain - * request and decodes only the fields Maple reads. Failures fold into one - * {@link CloudflareBillingError} whose `kind` says what the caller does next. - */ +// The slice of Cloudflare's REST API the cost source uses: the accounts a token reads, an account's +// daily billable usage and its subscriptions. Each call reads the status off a plain request, and +// every failure is one `CloudflareBillingError` whose `kind` says what the caller does next. import { Duration, Effect, Redacted, Schema, Array as Arr } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http" +import type { CostPullWindow } from "../../CostProvider" +import { addDays } from "../../sync-plan" const BASE_URL = "https://api.cloudflare.com/client/v4" const REQUEST_TIMEOUT = Duration.seconds(30) const PAGE_SIZE = 50 const MAX_SUBSCRIPTION_PAGES = 10 +/** What a save and a pull both say about an account `billable-usage/info` reports as not covered. */ +export const NOT_COVERED = + "Cloudflare has no usage-based billing data for this account. Accounts on an enterprise contract are not supported yet." + export class CloudflareBillingError extends Schema.TaggedError()( "@maple/backend/cost/CloudflareBillingError", { @@ -86,6 +89,13 @@ const call = ( TimeoutError: failed("upstream", "request timed out"), SchemaError: failed("invalid_response", "answered with a body Maple cannot read"), }), + // Callers show their own message; which failure it was stays on the span. + Effect.tapError((error) => + Effect.annotateCurrentSpan({ + "maple.cost.cloudflare.error": error.kind, + "maple.cost.cloudflare.status": error.status ?? 0, + }), + ), // A redirect is not Cloudflare answering, so the token is never followed to another host. Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }), Effect.annotateSpans("peer.service", "cloudflare"), @@ -93,7 +103,6 @@ const call = ( } const Account = Schema.Struct({ id: Schema.String, name: Schema.String }) -export type CloudflareAccount = typeof Account.Type /** * The accounts the token can see, and how many there are in all. A token limited to one account @@ -149,23 +158,26 @@ export const UsageRow = Schema.Struct({ }) export type UsageRow = typeof UsageRow.Type +/** A billing period is 31 days at most: a range starting this long before a day includes its period's start. */ +const PERIOD_LEAD_DAYS = 31 + /** - * The days before `to` of every billing period that starts in `[from, to)`: `to` is exclusive, and - * a period that started before `from` returns nothing, whatever days of it the range covers. The - * range is 90 days at most. One answer, no paging. + * Every usage row of the window's days. Cloudflare answers the days before `to` of each billing + * period that starts in `[from, to)`, so the range starts a period before the window and ends the + * day after it, and the answer also holds days before the window. 90 days at most, no paging. */ export const getBillableUsage = ( httpClient: HttpClient.HttpClient, token: Redacted.Redacted, accountId: string, - range: { readonly from: string; readonly to: string }, + window: CostPullWindow, ) => call( httpClient, token, HttpClientRequest.setUrlParams( HttpClientRequest.get(`${BASE_URL}/accounts/${encodeURIComponent(accountId)}/billable-usage`), - range, + { from: addDays(window.startDay, -PERIOD_LEAD_DAYS), to: addDays(window.endDay, 1) }, ), Schema.Array(UsageRow), ).pipe(Effect.map((body) => body.result)) @@ -175,6 +187,8 @@ export const Subscription = Schema.Struct({ price: Schema.optionalKey(Schema.NullOr(Schema.Number)), currency: Schema.optionalKey(Schema.NullOr(Schema.String)), frequency: Schema.optionalKey(Schema.NullOr(Schema.String)), + /** `Paid`, `Trial`, `Cancelled`, `Failed`, `Expired` and others. */ + state: Schema.optionalKey(Schema.NullOr(Schema.String)), current_period_start: Schema.optionalKey(Schema.NullOr(isoDay)), created_date: Schema.optionalKey(Schema.NullOr(isoDay)), rate_plan: Schema.optionalKey( @@ -205,7 +219,7 @@ const subscriptionsPage = ( Schema.Array(Subscription), ) -/** Every subscription of the account, zone plans included, over as many pages as it takes. */ +/** Every subscription of the account, zone plans included. More pages than Maple reads fail: part of them would under-count. */ export const listSubscriptions = ( httpClient: HttpClient.HttpClient, token: Redacted.Redacted, @@ -213,7 +227,13 @@ export const listSubscriptions = ( ) => Effect.gen(function* () { const first = yield* subscriptionsPage(httpClient, token, accountId, 1) - const pages = Math.min(first.result_info?.total_pages ?? 1, MAX_SUBSCRIPTION_PAGES) + const pages = first.result_info?.total_pages ?? 1 + if (pages > MAX_SUBSCRIPTION_PAGES) { + return yield* new CloudflareBillingError({ + message: `Cloudflare lists ${pages} pages of subscriptions, more than Maple reads`, + kind: "invalid_response", + }) + } const rest = yield* Effect.forEach(pages < 2 ? [] : Arr.range(2, pages), (page) => Effect.map(subscriptionsPage(httpClient, token, accountId, page), (body) => body.result), ) diff --git a/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts b/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts index a7872024c9..ad0455cb97 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts @@ -27,6 +27,8 @@ export interface FakeCloudflare { usage: ReadonlyArray | string /** The day of the month a billing period starts on. */ anchorDay: number + /** The oldest day served: a year back on the real API. Older days of a range are left out, not refused. */ + oldestDay: string subscriptions: ReadonlyArray /** Subscriptions one page holds, to make a listing span pages. */ subscriptionsPageSize: number @@ -45,6 +47,7 @@ export const makeFakeCloudflare = (overrides: Partial = {}): Fak covered: true, usage: [], anchorDay: 1, + oldestDay: "2000-01-01", subscriptions: [], subscriptionsPageSize: 50, cloudCost: true, @@ -107,7 +110,7 @@ const usageAnswer = (world: FakeCloudflare, params: URLSearchParams): Response = Arr.filter(world.usage, (row) => { const day = Str.slice(0, 10)(row.ChargePeriodStart) const start = periodStart(day, world.anchorDay) - return start >= from && start < to && day < to + return start >= from && start < to && day < to && day >= world.oldestDay }), ) } diff --git a/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts b/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts index 6f34c528a8..b60d33e804 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/normalize.test.ts @@ -253,6 +253,12 @@ describe("toSubscriptionRows", () => { ).toEqual([]) }) + it("charges a paid or cancelled subscription, not one in trial or one that was never paid", () => { + const charged = (state: string) => fees([plan({ state })]).length + expect([charged("Paid"), charged("Cancelled"), charged("Provisioned")]).toEqual([2, 2, 2]) + expect([charged("Trial"), charged("Failed"), charged("Expired")]).toEqual([0, 0, 0]) + }) + it("adds up two subscriptions to one plan on one day, and marks an open day", () => { const rows = fees([ plan({ current_period_start: "2026-11-09T00:00:00Z" }), diff --git a/packages/backend/src/services/cost/providers/cloudflare/normalize.ts b/packages/backend/src/services/cost/providers/cloudflare/normalize.ts index 3ddd824223..fa8da2cbb0 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/normalize.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/normalize.ts @@ -18,7 +18,7 @@ const FAMILY_CATEGORIES = new Map([ const OTHER_CATEGORY = "Other" /** `CloudService` of every subscription fee: the plans are not products with usage of their own. */ -export const SUBSCRIPTIONS_SERVICE = "Subscriptions" +const SUBSCRIPTIONS_SERVICE = "Subscriptions" const dayOf = (timestamp: string): string => Str.slice(0, 10)(timestamp) @@ -84,6 +84,9 @@ export const toUsageRows = ( }) } +/** States whose listed price was not charged: a trial is free, the other two were never paid. */ +const UNCHARGED_STATES = ["Trial", "Failed", "Expired"] + /** Monthly periods a fee is stepped back over: the sync reads a year, and a period starts within the last month. */ const MONTHLY_PERIODS = 14 @@ -107,10 +110,8 @@ const periodStarts = (subscription: Subscription, window: CostPullWindow): Reado } /** - * Each paid subscription's fee, as a purchase on the day a billing period started inside the - * window. Cloudflare reports the current period only: earlier months are stepped back from it at - * today's price, which assumes the price did not change. A yearly fee is on its current period's - * start alone. + * Each paid subscription's fee, as a purchase on the days its billing periods started in the window. + * Cloudflare reports the current period only: earlier ones use today's price, as if it never changed. */ export const toSubscriptionRows = ( subscriptions: ReadonlyArray, @@ -120,7 +121,7 @@ export const toSubscriptionRows = ( ): ReadonlyArray => { const fees = Arr.flatMap(subscriptions, (subscription) => { const price = subscription.price ?? 0 - return price > 0 + return price > 0 && !Arr.contains(UNCHARGED_STATES, subscription.state ?? "") ? Arr.map(periodStarts(subscription, window), (day) => ({ day, price, diff --git a/packages/backend/src/services/cost/providers/fake-cost-env.ts b/packages/backend/src/services/cost/providers/fake-cost-env.ts index af462ef031..8927470380 100644 --- a/packages/backend/src/services/cost/providers/fake-cost-env.ts +++ b/packages/backend/src/services/cost/providers/fake-cost-env.ts @@ -1,7 +1,7 @@ // The deployment config the cost provider fakes run their services on. import { ConfigProvider } from "effect" -export const FAKE_ENCRYPTION_KEY = Buffer.alloc(32, 9).toString("base64") +const FAKE_ENCRYPTION_KEY = Buffer.alloc(32, 9).toString("base64") /** `encryptionKey` is `MAPLE_INGEST_KEY_ENCRYPTION_KEY`, for a deployment whose secret key is unusable. */ export const fakeCostEnvConfig = (encryptionKey = FAKE_ENCRYPTION_KEY) => diff --git a/packages/domain/src/http/cloudflare-cost.ts b/packages/domain/src/http/cloudflare-cost.ts index 2811fe6f9b..2cc35601c5 100644 --- a/packages/domain/src/http/cloudflare-cost.ts +++ b/packages/domain/src/http/cloudflare-cost.ts @@ -10,7 +10,7 @@ import { import { SessionAuthorization } from "./current-tenant" import { CloudCostDisabledError } from "./query-engine" -/** Cloudflare tokens are 40 to about 60 characters; anything longer is not one. */ +/** Far above any Cloudflare token (about 40 to 60 characters): it only bounds what the request carries. */ export const CLOUDFLARE_COST_TOKEN_MAX_LENGTH = 256 export class CloudflareCostTokenRequest extends Schema.Class( diff --git a/packages/domain/src/http/cost-connections.ts b/packages/domain/src/http/cost-connections.ts index 91e24bf23d..ceec11915a 100644 --- a/packages/domain/src/http/cost-connections.ts +++ b/packages/domain/src/http/cost-connections.ts @@ -17,7 +17,7 @@ export class CostConnectionRemoved extends Schema.Class(" /** The database or the provider could not be reached; its own text can carry internals, so it stays server-side. */ export class CostConnectionPersistenceError extends HttpTaggedError()( "@maple/http/cost-connections/CostConnectionPersistenceError", - { message: Schema.String, cause: Schema.optionalKey(Schema.String) }, + { message: Schema.String }, { status: 503, code: "cost_connection_unavailable", From ad7359575ea45ea303280ef2add1446b04e9e274 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 00:06:20 +0100 Subject: [PATCH 4/9] test(cost): a refused cost credential's reason reaches the page, a failed save's text does not --- .../web-data/src/lib/error-messages.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/web-data/src/lib/error-messages.test.ts b/packages/web-data/src/lib/error-messages.test.ts index e96f23be23..51e83dee38 100644 --- a/packages/web-data/src/lib/error-messages.test.ts +++ b/packages/web-data/src/lib/error-messages.test.ts @@ -5,6 +5,8 @@ import { QueryEngineExecutionError, WarehouseQuotaExceededError } from "@maple/d import { BillingConflictError, BillingPaymentRequiredError, + CostConnectionPersistenceError, + CostConnectionValidationError, OpenRouterKeyPersistenceError, OpenRouterKeyValidationError, } from "@maple/domain/http" @@ -148,6 +150,22 @@ describe("displayError", () => { ) }) + it("keeps the reason a cost credential was refused, and hides what a failed save answered", () => { + const refused = new CostConnectionValidationError({ + message: "This token can read 3 accounts. Create one limited to a single account.", + }) + expect(displayError(refused).message).toBe( + "This token can read 3 accounts. Create one limited to a single account.", + ) + + const failed = new CostConnectionPersistenceError({ + message: 'insert into "cost_connections" failed: connection reset', + }) + expect(displayError(failed).message).toBe( + "The credential could not be checked or saved right now. Try again in a few seconds.", + ) + }) + it("finds a declared error in an Effect cause", () => { const input = errorEnvelope({ _tag: "@maple/http/errors/DashboardNotFoundError", From c584c578d176b8300a1568a689763287fdf6e782 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 00:14:56 +0100 Subject: [PATCH 5/9] test(cost): the Cloudflare pull over a window that reaches past the served year The test for the oldest month passed with the fake's one year limit removed: the only day the limit dropped was outside the pulled window. The window now holds the oldest served day, in a billing period that started before it, and the day before it must be absent. A subscription list longer than Maple reads now says so on the span, with the page count: its error is made outside the shared call, which annotates the rest. --- apps/web/src/page/cost/update.ts | 2 +- apps/web/src/page/cost/view-sources.ts | 2 +- .../cloudflare/CloudflareCostProvider.test.ts | 20 +++++++++---------- .../cloudflare/cloudflare-billing-api.ts | 4 ++++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/web/src/page/cost/update.ts b/apps/web/src/page/cost/update.ts index 11438fd069..3b6888f97f 100644 --- a/apps/web/src/page/cost/update.ts +++ b/apps/web/src/page/cost/update.ts @@ -369,7 +369,7 @@ export const update = (model: Model, message: Message, context: PageContext): Re model: { ...model, credentialDraft: Field.update(model.credentialDraft, inner).model, - // The refusal was about the key that was there. + // The refusal was about the credential that was there. credentialError: inner._tag === "ChangedValue" ? null : model.credentialError, }, }, diff --git a/apps/web/src/page/cost/view-sources.ts b/apps/web/src/page/cost/view-sources.ts index 5e91d8543f..0b2c566633 100644 --- a/apps/web/src/page/cost/view-sources.ts +++ b/apps/web/src/page/cost/view-sources.ts @@ -169,7 +169,7 @@ export const sourcesSection = ( nowMs: number, timeZone: string, options: Readonly<{ - /** A key write is in flight: the actions wait for it. */ + /** A credential write is in flight: the actions wait for it. */ isBusy: boolean /** The viewer may add, replace and remove sources; without it no such action is offered. */ canManage: boolean diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts index 39e601d7ff..3f2a5d9a42 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts @@ -184,16 +184,15 @@ describe("CloudflareCostProvider", () => { ) }) - it.effect("reads the oldest month it keeps, though the range it asks reaches past what is served", () => { + it.effect("reads a window that reaches past the year Cloudflare serves, and gets the served days", () => { const testDb = createTestDb(trackedDbs) - // Cloudflare serves a year: the lead before the floor month asks for older days and gets none. + // The oldest day served falls inside the window, in a billing period that started before it. const world = makeFakeCloudflare({ oldestDay: "2025-11-11", anchorDay: 5, usage: [ usageRow("2025-11-10", "R2", "R2 Storage", 9), - // These days belong to the period that started on 5 November, before the oldest day. - usageRow("2025-12-01", "R2", "R2 Storage", 1), + usageRow("2025-11-11", "R2", "R2 Storage", 1), usageRow("2025-12-04", "R2", "R2 Storage", 2), usageRow("2025-12-31", "R2", "R2 Storage", 3), ], @@ -204,17 +203,18 @@ describe("CloudflareCostProvider", () => { Effect.gen(function* () { const connectionRef = yield* saveToken(testDb, world) const pulled = yield* pull(ORG, connectionRef, { - startDay: "2025-12-01", + startDay: "2025-11-01", endDay: "2025-12-31", }) - assert.strictEqual( - world.urls[1], - `${ACCOUNT_PATH}/billable-usage?from=2025-10-31&to=2026-01-01`, - ) + // The range starts before anything served: it is answered, not refused. + assert.deepStrictEqual(world.urls.slice(1, 3), [ + `${ACCOUNT_PATH}/billable-usage?from=2025-10-01&to=2025-12-26`, + `${ACCOUNT_PATH}/billable-usage?from=2025-11-25&to=2026-01-01`, + ]) assert.deepStrictEqual( Arr.map(pulled.rows, (row) => [row.day, row.billedCost]), [ - ["2025-12-01", 1], + ["2025-11-11", 1], ["2025-12-04", 2], ["2025-12-31", 3], ], diff --git a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts index 058b9c6926..6c5b10b7a7 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts @@ -229,6 +229,10 @@ export const listSubscriptions = ( const first = yield* subscriptionsPage(httpClient, token, accountId, 1) const pages = first.result_info?.total_pages ?? 1 if (pages > MAX_SUBSCRIPTION_PAGES) { + yield* Effect.annotateCurrentSpan({ + "maple.cost.cloudflare.error": "too_many_subscription_pages", + "maple.cost.cloudflare.subscription_pages": pages, + }) return yield* new CloudflareBillingError({ message: `Cloudflare lists ${pages} pages of subscriptions, more than Maple reads`, kind: "invalid_response", From a22ad1fa28a74c7734c06deb5835357af6d8eb79 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 00:45:16 +0100 Subject: [PATCH 6/9] fix(cost): a Cloudflare answer must say it succeeded and hold all it lists A 200 with success false and an empty result decoded as an answer: read as no rows, it would store every day of the window as empty. Every envelope now requires success true and fails as an answer Maple cannot read. The subscription listing took its page count from total_pages alone, which the published response does not list: without it only the first page was read. The count now falls back to total_count over the page size, and a list read short of its count, or a full page that says nothing of its paging, fails the pull. The usage answer has no paging today; one that lists more rows than it returns fails too. --- .../cloudflare/CloudflareCostProvider.test.ts | 88 +++++++++++++++++++ .../cloudflare/cloudflare-billing-api.ts | 76 +++++++++++----- .../providers/cloudflare/fake-cloudflare.ts | 33 +++++-- 3 files changed, 168 insertions(+), 29 deletions(-) diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts index 3f2a5d9a42..bdf861f4ed 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.test.ts @@ -250,6 +250,94 @@ describe("CloudflareCostProvider", () => { }, ) + it.effect("reads every page of a listing that gives its count and no page count", () => { + const testDb = createTestDb(trackedDbs) + const fee = (name: string) => ({ + price: 5, + frequency: "monthly", + current_period_start: "2026-11-01T00:00:00Z", + rate_plan: { public_name: name }, + }) + const world = makeFakeCloudflare({ + subscriptionsPageSize: 2, + subscriptionsInfo: "count", + subscriptions: [fee("Plan A"), fee("Plan B"), fee("Plan C")], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + const pulled = yield* pull(ORG, connectionRef) + assert.deepStrictEqual( + Arr.map(pulled.rows, (row) => row.usageType), + ["Plan A", "Plan B", "Plan C"], + ) + // A listing that says nothing of its paging is whole only while its page is not full: + // 50 were asked for, so 50 may be part of more. + world.subscriptionsInfo = "none" + world.subscriptionsPageSize = 50 + assert.lengthOf((yield* pull(ORG, connectionRef)).rows, 3) + world.subscriptions = Arr.makeBy(50, (index) => fee(`Plan ${index}`)) + assert.strictEqual((yield* Effect.flip(pull(ORG, connectionRef))).reason, "invalid_response") + }), + ) + }) + + it.effect("fails a listing that counts more subscriptions than it returned", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare({ + subscriptionsNeverReturned: 1, + subscriptions: [ + { + price: 5, + frequency: "monthly", + current_period_start: "2026-11-01T00:00:00Z", + rate_plan: { public_name: "Plan A" }, + }, + ], + }) + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + assert.strictEqual((yield* Effect.flip(pull(ORG, connectionRef))).reason, "invalid_response") + }), + ) + }) + + it.effect("fails an answer that does not say it succeeded, or that holds part of the usage", () => { + const testDb = createTestDb(trackedDbs) + const world = makeFakeCloudflare() + const row = + '{"ServiceName":"R2 Storage","ChargePeriodStart":"2026-11-01T00:00:00Z","BillingCurrency":"USD","BilledCost":1,"EffectiveCost":1,"ListCost":1}' + return run( + testDb, + world, + Effect.gen(function* () { + const connectionRef = yield* saveToken(testDb, world) + // Read as an answer, the empty result would store every day of the window as empty. + const answers = [ + '{"success":false,"errors":[{"code":1000,"message":"try later"}],"result":[]}', + '{"errors":[],"result":[]}', + `{"success":true,"result":[${row}],"result_info":{"page":1,"per_page":1,"total_pages":3,"total_count":3}}`, + `{"success":true,"result":[${row}],"result_info":{"total_count":2}}`, + ] + const reasons = yield* Effect.forEach(answers, (answer) => + Effect.gen(function* () { + world.usage = answer + return (yield* Effect.flip(pull(ORG, connectionRef))).reason + }), + ) + assert.deepStrictEqual(reasons, Arr.replicate("invalid_response", 4)) + // The same row in an answer that holds all it lists is read. + world.usage = `{"success":true,"result":[${row}],"result_info":{"total_count":1}}` + assert.lengthOf((yield* pull(ORG, connectionRef)).rows, 1) + }), + ) + }) + it.effect("reads every page of the subscriptions", () => { const testDb = createTestDb(trackedDbs) const fee = (name: string) => ({ diff --git a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts index 6c5b10b7a7..2efa31c752 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/cloudflare-billing-api.ts @@ -43,18 +43,35 @@ const errorKind = (status: number): CloudflareBillingError["kind"] => ? "rate_limited" : "upstream" -/** Cloudflare's envelope: `result` is the answer, `result_info` the paging of a listing. */ +const ResultInfo = Schema.Struct({ + per_page: Schema.optionalKey(Schema.NullOr(Schema.Number)), + total_pages: Schema.optionalKey(Schema.NullOr(Schema.Number)), + total_count: Schema.optionalKey(Schema.NullOr(Schema.Number)), +}) + +/** + * Cloudflare's envelope: `result` is the answer, `result_info` the paging of a listing. A 200 that + * does not say `success: true` is not an answer: read as one, its empty result would store empty days. + */ const envelope = (result: S) => Schema.Struct({ + success: Schema.Literal(true), result, - result_info: Schema.optionalKey( - Schema.NullOr( - Schema.Struct({ - total_pages: Schema.optionalKey(Schema.NullOr(Schema.Number)), - total_count: Schema.optionalKey(Schema.NullOr(Schema.Number)), - }), - ), - ), + result_info: Schema.optionalKey(Schema.NullOr(ResultInfo)), + }) + +/** A listing Maple holds only part of: stored as it is, the missing rest would read as no cost. */ +const partial = (what: string, held: number, total: number) => + Effect.gen(function* () { + yield* Effect.annotateCurrentSpan({ + "maple.cost.cloudflare.error": "partial_listing", + "maple.cost.cloudflare.listed": total, + "maple.cost.cloudflare.read": held, + }) + return yield* new CloudflareBillingError({ + message: `Cloudflare lists ${total} ${what} and Maple read ${held}`, + kind: "invalid_response", + }) }) const call = ( @@ -180,7 +197,16 @@ export const getBillableUsage = ( { from: addDays(window.startDay, -PERIOD_LEAD_DAYS), to: addDays(window.endDay, 1) }, ), Schema.Array(UsageRow), - ).pipe(Effect.map((body) => body.result)) + ).pipe( + // The answer has no paging today. One that starts to page must not be read as the whole window. + Effect.flatMap((body) => { + const info = body.result_info + const total = info?.total_count ?? body.result.length + return (info?.total_pages ?? 1) > 1 || total > body.result.length + ? partial("usage rows", body.result.length, total) + : Effect.succeed(body.result) + }), + ) /** A plan or add-on the account pays a fixed fee for. Free items carry no period. */ export const Subscription = Schema.Struct({ @@ -219,7 +245,11 @@ const subscriptionsPage = ( Schema.Array(Subscription), ) -/** Every subscription of the account, zone plans included. More pages than Maple reads fail: part of them would under-count. */ +/** + * Every subscription of the account, zone plans included. The page count comes from `total_pages`, + * or from `total_count` where the listing does not say. A list Maple cannot read whole fails: + * part of it would under-count the fees. + */ export const listSubscriptions = ( httpClient: HttpClient.HttpClient, token: Redacted.Redacted, @@ -227,19 +257,21 @@ export const listSubscriptions = ( ) => Effect.gen(function* () { const first = yield* subscriptionsPage(httpClient, token, accountId, 1) - const pages = first.result_info?.total_pages ?? 1 - if (pages > MAX_SUBSCRIPTION_PAGES) { - yield* Effect.annotateCurrentSpan({ - "maple.cost.cloudflare.error": "too_many_subscription_pages", - "maple.cost.cloudflare.subscription_pages": pages, - }) - return yield* new CloudflareBillingError({ - message: `Cloudflare lists ${pages} pages of subscriptions, more than Maple reads`, - kind: "invalid_response", - }) + const info = first.result_info + const total = info?.total_count ?? null + const perPage = info?.per_page ?? PAGE_SIZE + const stated = info?.total_pages ?? (total === null ? null : Math.ceil(total / Math.max(1, perPage))) + // With neither figure, a page that is not full is the whole list. A full one may be part of it. + const pages = stated ?? (first.result.length < perPage ? 1 : null) + if (pages === null || pages > MAX_SUBSCRIPTION_PAGES) { + return yield* partial("subscriptions", first.result.length, total ?? first.result.length) } const rest = yield* Effect.forEach(pages < 2 ? [] : Arr.range(2, pages), (page) => Effect.map(subscriptionsPage(httpClient, token, accountId, page), (body) => body.result), ) - return [...first.result, ...Arr.flatten(rest)] + const subscriptions = [...first.result, ...Arr.flatten(rest)] + if (total !== null && subscriptions.length < total) { + return yield* partial("subscriptions", subscriptions.length, total) + } + return subscriptions }) diff --git a/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts b/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts index ad0455cb97..ef7c50a3c4 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/fake-cloudflare.ts @@ -32,6 +32,10 @@ export interface FakeCloudflare { subscriptions: ReadonlyArray /** Subscriptions one page holds, to make a listing span pages. */ subscriptionsPageSize: number + /** What the listing says of its paging: both figures, the count alone as the docs have it, or nothing. */ + subscriptionsInfo: "pages_and_count" | "count" | "none" + /** Subscriptions the listing counts and never returns. */ + subscriptionsNeverReturned: number /** Whether the org is in the `cloudCost` rollout. */ cloudCost: boolean readonly urls: Array @@ -50,6 +54,8 @@ export const makeFakeCloudflare = (overrides: Partial = {}): Fak oldestDay: "2000-01-01", subscriptions: [], subscriptionsPageSize: 50, + subscriptionsInfo: "pages_and_count", + subscriptionsNeverReturned: 0, cloudCost: true, urls: [], authorizations: [], @@ -90,8 +96,15 @@ const periodStart = (day: string, anchorDay: number): string => { return `${Str.slice(0, 8)(previous)}${anchor}` } -const envelope = (result: unknown, info: Record = {}) => - Response.json({ success: true, errors: [], messages: [], result, result_info: info }) +/** A listing's answer. The usage and info calls answer without `result_info`, as the real API does. */ +const envelope = (result: unknown, info?: Record) => + Response.json({ + success: true, + errors: [], + messages: [], + result, + ...(info === undefined ? undefined : { result_info: info }), + }) const refused = (status: number) => Response.json( @@ -118,11 +131,17 @@ const usageAnswer = (world: FakeCloudflare, params: URLSearchParams): Response = const subscriptionsAnswer = (world: FakeCloudflare, params: URLSearchParams): Response => { const page = Number(params.get("page") ?? "1") const size = world.subscriptionsPageSize - return envelope(Arr.take(Arr.drop(world.subscriptions, (page - 1) * size), size), { - page, - total_pages: Math.max(1, Math.ceil(world.subscriptions.length / size)), - total_count: world.subscriptions.length, - }) + const total = world.subscriptions.length + world.subscriptionsNeverReturned + const result = Arr.take(Arr.drop(world.subscriptions, (page - 1) * size), size) + const count = { page, per_page: size, total_count: total } + return world.subscriptionsInfo === "none" + ? Response.json({ success: true, errors: [], messages: [], result }) + : envelope( + result, + world.subscriptionsInfo === "count" + ? count + : { ...count, total_pages: Math.max(1, Math.ceil(total / size)) }, + ) } const fetchStub = From 2bce55596a457e928c8e4355f334ed3c2e533555 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 00:45:16 +0100 Subject: [PATCH 7/9] fix(cost): place Cloudflare subscription fees for clamped, undated and non-monthly plans A subscription made on the 29th to 31st has later period starts clamped to shorter months, so stepping back from the current one landed days before it was made and its first period's fee was dropped. That fee now goes on the day it was made. Without created_date, which the published response does not list, a monthly plan got a fee on every period start in the window, months before it may have existed. Only the current period's fee is stored then. Weekly and quarterly plans step by their own period. A priced plan Maple cannot place on a day is counted on the pull's span as fees_skipped. Two usage rows of one day and metric are still one cost row, since the warehouse reads them by that key, but only quantities in the row's own unit add up. --- docs/cloud-cost-plan.md | 26 +++-- .../cloudflare/CloudflareCostProvider.ts | 4 +- .../providers/cloudflare/normalize.test.ts | 81 +++++++++++++- .../cost/providers/cloudflare/normalize.ts | 101 +++++++++++++----- 4 files changed, 176 insertions(+), 36 deletions(-) diff --git a/docs/cloud-cost-plan.md b/docs/cloud-cost-plan.md index af0467d92a..2aa30ace9b 100644 --- a/docs/cloud-cost-plan.md +++ b/docs/cloud-cost-plan.md @@ -274,11 +274,18 @@ currency the figures are in and lists the others. Nothing is added across curren `UsageType`, the region is `global`, and the category comes from a small map of the families. Rows with no billed and no list cost are left out. - Live: usage rows do not include plan fees. `GET /accounts/{id}/subscriptions` (paged) lists them - with `price`, `frequency` and `current_period_start`. Each paid monthly subscription becomes a - purchase row (`CloudService` "Subscriptions", the rate plan as `UsageType`, the zone as the - resource) on the day each billing period started, stepped back month by month from the current - period and never before the subscription was created. A yearly one gets one row on its current - period's start. + with `price`, `frequency` and `current_period_start`. Each paid subscription becomes a purchase + row (`CloudService` "Subscriptions", the rate plan as `UsageType`, the zone as the resource) on + the day each billing period started, stepped back from the current period by its frequency + (a week, a month or three months) and never before the subscription was created. A yearly one + gets one row on its current period's start. +- A subscription made on the 29th to 31st has later starts clamped to shorter months, so stepping + back lands days before it was made: that first period's fee goes on the day it was made. +- Live: `created_date` was on every subscription. Schema: the published response does not list + it. Without it only the current period's fee is stored, since earlier periods cannot be told + from months before the subscription existed. +- A priced subscription Maple cannot place on a day (another frequency, or no period start) has + no row. The pull's span counts them as `maple.cost.cloudflare.fees_skipped`. - Assumed: Cloudflare reports the current period only, so earlier months use today's price. A plan whose price changed shows the new price for its past months too. - Schema: a subscription's `state` is one of `Trial`, `Provisioned`, `Paid`, `AwaitingPayment`, @@ -287,8 +294,13 @@ currency the figures are in and lists the others. Nothing is added across curren - Known limit: a fee is known only while its subscription is listed. If Cloudflare drops a cancelled subscription from the list, the next re-pull of the open months stores those days without its fee. Months already walked as history keep theirs. -- More than 500 subscriptions (10 pages) fail the pull as an answer Maple cannot read, since part - of the list would under-count. +- Live: the listing's `result_info` has `per_page`, `total_pages` and `total_count`. Schema: only + the count is documented, so the page count falls back to the count over the page size. A list + read short of its count, over 10 pages, or a full page that says nothing of its paging, fails + the pull as an answer Maple cannot read: part of the list would under-count. +- Every answer must say `success: true`. Read as an answer, a 200 with an empty result would + store every day of the window as empty. Live: the usage answer has no `result_info`. One that + appears and lists more rows than it returned fails the pull too. - Open: whether a completed day is ever restated. The four newest days are pulled again daily. - Not built: enterprise contracts, zone-level analytics, the restricted "Version 2" endpoints, invoices and credits. diff --git a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts index dacc62c89d..9307cccd36 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/CloudflareCostProvider.ts @@ -14,7 +14,7 @@ import { import { CostConnectionStore, loadFailureOf } from "../../CostConnectionStore" import { addDays, utcToday } from "../../sync-plan" import * as CloudflareBilling from "./cloudflare-billing-api" -import { toSubscriptionRows, toUsageRows } from "./normalize" +import { toSubscriptionRows, toUsageRows, unplacedFees } from "./normalize" /** Days of a window one request reads: with the period it leads with, under the API's 90 day cap. */ const PIECE_DAYS = 55 @@ -108,6 +108,8 @@ export class CloudflareCostProvider extends Context.Service { ]) }) + it("never adds quantities of another unit to a row, though their cost is one", () => { + const rows = usage([ + usageRow("2026-10-05", "R2", "R2 Storage", 1.5, { + ConsumedQuantity: 10, + ConsumedUnit: "GB-months", + }), + usageRow("2026-10-05", "R2", "R2 Storage", 2, { + ConsumedQuantity: 700, + ConsumedUnit: "Gigabytes", + }), + ]) + expect(Arr.map(rows, (row) => [row.billedCost, row.consumedQuantity, row.consumedUnit])).toEqual([ + [3.5, 10, "GB-months"], + ]) + }) + it("adds up two rows of one day and metric, which the warehouse would read as one", () => { const rows = usage([ usageRow("2026-10-05", "R2", "R2 Storage", 1.5, { @@ -247,12 +263,73 @@ describe("toSubscriptionRows", () => { plan({ price: 0 }), { price: 0, frequency: "not-applicable", created_date: "2025-01-01T00:00:00Z" }, plan({ frequency: "not-applicable" }), - plan({ frequency: "quarterly" }), + plan({ frequency: "biennial" }), plan({ current_period_start: null }), ]), ).toEqual([]) }) + it("counts the charged subscriptions it could not place on a day, and no free or unpaid one", () => { + const decode = Schema.decodeSync(Schema.Array(Subscription)) + expect( + unplacedFees( + decode([ + plan(), + plan({ frequency: "biennial" }), + plan({ current_period_start: null }), + plan({ frequency: "biennial", price: 0 }), + plan({ frequency: "biennial", state: "Trial" }), + ]), + ), + ).toBe(2) + }) + + it("keeps the first period of a subscription made on the 31st, whose later starts were clamped", () => { + const rows = fees( + [plan({ created_date: "2025-10-31T08:00:00Z", current_period_start: "2026-02-28T00:00:00Z" })], + { startDay: "2025-10-01", endDay: "2026-03-10" }, + ) + expect(Arr.map(rows, (row) => row.day)).toEqual([ + "2026-02-28", + "2026-01-28", + "2025-12-28", + "2025-11-28", + "2025-10-31", + ]) + // A start that is early for another reason is not a period: nothing is charged before the 20th. + const moved = plan({ + created_date: "2026-10-20T00:00:00Z", + current_period_start: "2026-11-15T00:00:00Z", + }) + expect( + Arr.map(fees([moved], { startDay: "2026-10-01", endDay: "2026-11-20" }), (row) => row.day), + ).toEqual(["2026-11-15"]) + }) + + it("charges only the current period when Cloudflare does not say when the subscription was made", () => { + const { created_date: _made, ...undated } = plan() + expect(Arr.map(fees([undated]), (row) => row.day)).toEqual(["2026-11-04"]) + expect(Arr.map(fees([plan({ created_date: null })]), (row) => row.day)).toEqual(["2026-11-04"]) + }) + + it("steps a quarterly fee by three months and a weekly one by seven days", () => { + const quarterly = plan({ + frequency: "quarterly", + price: 30, + current_period_start: "2026-11-01T00:00:00Z", + }) + expect( + Arr.map(fees([quarterly], { startDay: "2026-01-01", endDay: "2026-11-10" }), (row) => row.day), + ).toEqual(["2026-11-01", "2026-08-01", "2026-05-01", "2026-02-01"]) + const weekly = plan({ + frequency: "weekly", + current_period_start: "2026-11-09T00:00:00Z", + created_date: "2026-10-21T00:00:00Z", + }) + // Made on a Wednesday, billed on Mondays: the week it was made in is not a period of its own. + expect(Arr.map(fees([weekly]), (row) => row.day)).toEqual(["2026-11-09", "2026-11-02", "2026-10-26"]) + }) + it("charges a paid or cancelled subscription, not one in trial or one that was never paid", () => { const charged = (state: string) => fees([plan({ state })]).length expect([charged("Paid"), charged("Cancelled"), charged("Provisioned")]).toEqual([2, 2, 2]) diff --git a/packages/backend/src/services/cost/providers/cloudflare/normalize.ts b/packages/backend/src/services/cost/providers/cloudflare/normalize.ts index fa8da2cbb0..f653fb6949 100644 --- a/packages/backend/src/services/cost/providers/cloudflare/normalize.ts +++ b/packages/backend/src/services/cost/providers/cloudflare/normalize.ts @@ -60,6 +60,7 @@ export const toUsageRows = ( const first = Arr.headNonEmpty(group) const day = dayOf(first.ChargePeriodStart) const family = first.ServiceFamilyName ?? "" + const unit = first.ConsumedUnit ?? "" return { day, accountId, @@ -76,8 +77,11 @@ export const toUsageRows = ( billedCost: sum(group, (item) => item.BilledCost), effectiveCost: sum(group, (item) => item.EffectiveCost), listCost: sum(group, (item) => item.ListCost), - consumedQuantity: sum(group, (item) => item.ConsumedQuantity ?? 0), - consumedUnit: first.ConsumedUnit ?? "", + // Rows of one key are one cost. Only quantities in the row's own unit add up. + consumedQuantity: sum(group, (item) => + (item.ConsumedUnit ?? "") === unit ? (item.ConsumedQuantity ?? 0) : 0, + ), + consumedUnit: unit, tags: {}, estimated: day >= openDay, } @@ -87,28 +91,70 @@ export const toUsageRows = ( /** States whose listed price was not charged: a trial is free, the other two were never paid. */ const UNCHARGED_STATES = ["Trial", "Failed", "Expired"] -/** Monthly periods a fee is stepped back over: the sync reads a year, and a period starts within the last month. */ -const MONTHLY_PERIODS = 14 +/** Months back a fee is stepped over: the sync reads a year, and a period starts within the last one. */ +const MONTHS_BACK = 14 -/** `day` moved back by `months`, on the same day of the month or the month's last. */ -const monthsBefore = (day: string, months: number): string => - DateTime.formatIsoDateUtc(DateTime.subtract(DateTime.makeUnsafe(`${day}T00:00:00Z`), { months })) +/** How often a subscription bills, as the step between its period starts and how many reach `MONTHS_BACK`. */ +const FREQUENCIES = new Map< + string, + { readonly step: { readonly months?: number; readonly days?: number }; readonly periods: number } +>([ + ["weekly", { step: { days: 7 }, periods: 62 }], + ["monthly", { step: { months: 1 }, periods: MONTHS_BACK }], + ["quarterly", { step: { months: 3 }, periods: 5 }], + // A year back is before anything the sync reads. + ["yearly", { step: { months: 12 }, periods: 1 }], +]) + +const monthOf = (day: string): string => Str.slice(0, 7)(day) +const dayOfMonth = (day: string): number => Number(Str.slice(8, 10)(day)) +/** A month step can only move a start earlier in its month from this day on. */ +const SHORTEST_MONTH_DAYS = 28 -/** The days in the window a subscription's billing period started on, by how often it bills. */ -const periodStarts = (subscription: Subscription, window: CostPullWindow): ReadonlyArray => { +/** + * The days a subscription's billing periods started on, newest first: stepped back from the + * current one, never before the subscription was made. None when it cannot be placed on a day. + */ +const periodStarts = (subscription: Subscription): ReadonlyArray => { const start = subscription.current_period_start - if (start === undefined || start === null) return [] + const frequency = FREQUENCIES.get(subscription.frequency ?? "") + if (start === undefined || start === null || frequency === undefined) return [] const current = dayOf(start) - const created = dayOf(subscription.created_date ?? "") - const starts = - subscription.frequency === "monthly" - ? Arr.makeBy(MONTHLY_PERIODS, (months) => monthsBefore(current, months)) - : subscription.frequency === "yearly" - ? [current] - : [] - return Arr.filter(starts, (day) => inWindow(day, window) && day >= created) + const made = subscription.created_date + // Without the day it was made there is no telling which earlier periods existed: only this one. + if (made === undefined || made === null) return [current] + const created = dayOf(made) + const from = DateTime.makeUnsafe(`${current}T00:00:00Z`) + const starts = Arr.makeBy(frequency.periods, (index) => { + const day = DateTime.formatIsoDateUtc( + DateTime.subtract(from, { + months: (frequency.step.months ?? 0) * index, + days: (frequency.step.days ?? 0) * index, + }), + ) + // A start on the 29th to 31st is clamped in shorter months, which shifts every step before + // it: the first period then lands days before the subscription was made, in the same month. + const isClamped = + frequency.step.months !== undefined && + day < created && + monthOf(day) === monthOf(created) && + dayOfMonth(day) >= SHORTEST_MONTH_DAYS + return isClamped ? created : day + }) + return Arr.dedupe(Arr.filter(starts, (day) => day >= created)) } +/** Whether a subscription's listed price is charged: it has one, and its state is not a free or unpaid one. */ +const isCharged = (subscription: Subscription): boolean => + (subscription.price ?? 0) > 0 && !Arr.contains(UNCHARGED_STATES, subscription.state ?? "") + +/** Charged subscriptions whose fee is in no row: they bill by a frequency Maple does not step, or name no period. */ +export const unplacedFees = (subscriptions: ReadonlyArray): number => + Arr.filter( + subscriptions, + (subscription) => isCharged(subscription) && periodStarts(subscription).length === 0, + ).length + /** * Each paid subscription's fee, as a purchase on the days its billing periods started in the window. * Cloudflare reports the current period only: earlier ones use today's price, as if it never changed. @@ -121,14 +167,17 @@ export const toSubscriptionRows = ( ): ReadonlyArray => { const fees = Arr.flatMap(subscriptions, (subscription) => { const price = subscription.price ?? 0 - return price > 0 && !Arr.contains(UNCHARGED_STATES, subscription.state ?? "") - ? Arr.map(periodStarts(subscription, window), (day) => ({ - day, - price, - plan: subscription.rate_plan?.public_name ?? subscription.product?.name ?? "", - zone: subscription.zone?.name ?? "", - currency: subscription.currency ?? "USD", - })) + return isCharged(subscription) + ? Arr.map( + Arr.filter(periodStarts(subscription), (day) => inWindow(day, window)), + (day) => ({ + day, + price, + plan: subscription.rate_plan?.public_name ?? subscription.product?.name ?? "", + zone: subscription.zone?.name ?? "", + currency: subscription.currency ?? "USD", + }), + ) : [] }) // Two subscriptions to one plan (and zone) on one day are one key, so their fees add up. From b33b7ac1f029abee786eeaf6a255fdc3027b345d Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 00:45:16 +0100 Subject: [PATCH 8/9] fix(cost): name each source row's actions by its provider With two sources in the list, two buttons read "Remove" to a screen reader. Each action's accessible name now says whose source it acts on: "Remove Cloudflare source", "Replace OpenRouter key", "Manage AWS connection". --- apps/web/src/page/cost/cost.scene.test.ts | 27 +++++----- apps/web/src/page/cost/view-sources.ts | 66 ++++++++++++++--------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/apps/web/src/page/cost/cost.scene.test.ts b/apps/web/src/page/cost/cost.scene.test.ts index 6ae8418855..abe3be2cd6 100644 --- a/apps/web/src/page/cost/cost.scene.test.ts +++ b/apps/web/src/page/cost/cost.scene.test.ts @@ -311,12 +311,12 @@ describe("cost page", () => { // Request billing is AWS's: one line for the AWS source, none for OpenRouter. expectView(text("86 provider requests this month (USD 0.86)")).toExist(), expectView(text(/0 provider requests/)).not.toExist(), - expectView(role("link", { name: "Manage connection" })).toHaveAttr( + expectView(role("link", { name: "Manage AWS connection" })).toHaveAttr( "href", "/integrations?integration=aws", ), - expectView(role("button", { name: "Replace key" })).toExist(), - expectView(role("button", { name: "Remove" })).toExist(), + expectView(role("button", { name: "Replace OpenRouter key" })).toExist(), + expectView(role("button", { name: "Remove OpenRouter source" })).toExist(), ) // Replace opens the key form with no providers to go back to. @@ -367,7 +367,7 @@ describe("cost page", () => { resolveTheme, dialogMounted, expectView(text("user_2openrouterAccount")).not.toExist(), - expectView(role("button", { name: "Replace key" })).not.toExist(), + expectView(role("button", { name: "Replace OpenRouter key" })).not.toExist(), expectView(role("button", { name: "Add source" })).toBeDisabled(), ) // That read failing brings no action back on the removed source: the list keeps without it. @@ -384,7 +384,7 @@ describe("cost page", () => { resolveTheme, dialogMounted, expectView(text("123456789012")).toExist(), - expectView(role("button", { name: "Replace key" })).not.toExist(), + expectView(role("button", { name: "Replace OpenRouter key" })).not.toExist(), expectView(role("button", { name: "Add source" })).toBeEnabled(), ) @@ -536,8 +536,11 @@ describe("cost page", () => { expectView(text("1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d")).toExist(), // Neither provider bills for requests. expectView(text(/provider requests/)).not.toExist(), - expectView(role("button", { name: "Replace token" })).toExist(), - expectView(role("button", { name: "Replace key" })).toExist(), + expectView(role("button", { name: "Replace Cloudflare token" })).toExist(), + expectView(role("button", { name: "Replace OpenRouter key" })).toExist(), + // Two rows offer Remove: each one's name says whose source it removes. + expectView(role("button", { name: "Remove Cloudflare source" })).toExist(), + expectView(role("button", { name: "Remove OpenRouter source" })).toExist(), ) const replacing = pageUpdate( @@ -607,7 +610,7 @@ describe("cost page", () => { resolveTheme, dialogMounted, expectView(text("1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d")).not.toExist(), - expectView(role("button", { name: "Replace token" })).not.toExist(), + expectView(role("button", { name: "Replace Cloudflare token" })).not.toExist(), // The other provider's source is still listed. expectView(text("user_2openrouterAccount")).toExist(), ) @@ -647,9 +650,9 @@ describe("cost page", () => { // The sources still show how their sync is doing. expectView(text("user_2openrouterAccount")).toExist(), expectView(role("button", { name: "Add source" })).not.toExist(), - expectView(role("button", { name: "Replace key" })).not.toExist(), - expectView(role("button", { name: "Remove" })).not.toExist(), - expectView(role("link", { name: "Manage connection" })).not.toExist(), + expectView(role("button", { name: /^Replace/ })).not.toExist(), + expectView(role("button", { name: /^Remove/ })).not.toExist(), + expectView(role("link", { name: /^Manage/ })).not.toExist(), ) scene( { update: pageUpdate, view }, @@ -679,7 +682,7 @@ describe("cost page", () => { ), resolveTheme, expectView(role("button", { name: "Add source" })).toExist(), - expectView(role("button", { name: "Replace key" })).toExist(), + expectView(role("button", { name: "Replace OpenRouter key" })).toExist(), ) }) diff --git a/apps/web/src/page/cost/view-sources.ts b/apps/web/src/page/cost/view-sources.ts index 0b2c566633..6bee88c1e7 100644 --- a/apps/web/src/page/cost/view-sources.ts +++ b/apps/web/src/page/cost/view-sources.ts @@ -1,7 +1,7 @@ import type { CostSourceStatus, CostSourceSummary } from "@maple/domain/http" import { formatNumber, pluralize } from "@maple/ui/lib/format" import { cn } from "@maple/ui/lib/utils" -import { Array as Arr } from "effect" +import { Array as Arr, String as Str } from "effect" import type { Html, HtmlBuilder } from "foldkit/html" import { button, linkButton } from "@maple/views/ui/button" import { badge } from "@maple/views/ui/display" @@ -31,33 +31,49 @@ const STATUS = { Readonly<{ label: string; variant: "ok" | "info" | "muted" | "crit" }> > +/** A row's action. Its accessible name says whose source it acts on: two rows can offer the same one. */ +const rowButton = (h: H, label: string, name: string, onClick: Message, isBusy: boolean): Html => + button( + h, + { + variant: "ghost", + size: "sm", + isDisabled: isBusy, + attributes: [h.AriaLabel(name), h.OnClick(onClick)], + }, + [label], + ) + /** What a row offers: a connection's source is managed there, a pasted credential's here. */ const actions = (h: H, source: CostSourceSummary, isBusy: boolean): ReadonlyArray => { + const { name } = COST_PROVIDERS[source.provider] const provider = credentialProviderOf(source.provider) - return provider === null - ? [linkButton(h, { variant: "ghost", size: "sm", href: AWS_INTEGRATION_HREF }, ["Manage connection"])] - : [ - button( - h, - { - variant: "ghost", - size: "sm", - isDisabled: isBusy, - attributes: [h.OnClick(Message.ClickedReplaceCredential({ provider }))], - }, - [CREDENTIAL_SOURCES[provider].replace.label], - ), - button( - h, - { - variant: "ghost", - size: "sm", - isDisabled: isBusy, - attributes: [h.OnClick(Message.ClickedRemoveSource({ provider }))], - }, - ["Remove"], - ), - ] + if (provider === null) { + return [ + linkButton( + h, + { + variant: "ghost", + size: "sm", + href: AWS_INTEGRATION_HREF, + attributes: [h.AriaLabel(`Manage ${name} connection`)], + }, + ["Manage connection"], + ), + ] + } + const { label } = CREDENTIAL_SOURCES[provider].replace + return [ + rowButton( + h, + label, + // "Replace key" as "Replace OpenRouter key". + Str.replace("Replace", `Replace ${name}`)(label), + Message.ClickedReplaceCredential({ provider }), + isBusy, + ), + rowButton(h, "Remove", `Remove ${name} source`, Message.ClickedRemoveSource({ provider }), isBusy), + ] } /** What the provider billed the account for Maple's requests this month; nothing where requests are free. */ From 25bbfad542260276a1631a7bdfa534c08dfb33eb Mon Sep 17 00:00:00 2001 From: Makisuo Date: Fri, 13 Nov 2026 01:22:16 +0100 Subject: [PATCH 9/9] feat(cost): Cloudflare marks, account label and drill-down on the reworked Cost page A Cloudflare product (Workers, R2, D1, the plan fees and so on) wore the AWS mark, since every service that is not a model slug was taken for an AWS one. It now wears Cloudflare's, also beside an AWS source, and a product Maple has no name for does too when the org has no AWS source that could bill it. A Cloudflare account reads as "Cloudflare" with its id beside it, by the rule OpenRouter's follows: the id means nothing to a reader. Its rows filter and drill down like the others, which the scene test now runs for an account, a provider and a product. The cost lab has a state filtered to Cloudflare. --- apps/web/src/lab/cost-lab.ts | 5 ++- apps/web/src/page/cost/cost.scene.test.ts | 53 +++++++++++++++++++++++ apps/web/src/page/cost/marks.test.ts | 27 ++++++++++++ apps/web/src/page/cost/marks.ts | 41 +++++++++++++++--- 4 files changed, 118 insertions(+), 8 deletions(-) diff --git a/apps/web/src/lab/cost-lab.ts b/apps/web/src/lab/cost-lab.ts index ead66884a6..b75f70233c 100644 --- a/apps/web/src/lab/cost-lab.ts +++ b/apps/web/src/lab/cost-lab.ts @@ -57,6 +57,7 @@ export const LabState = Schema.Literals([ "empty-window", "loading", "by-provider", + "cloudflare", "add-source", "add-openrouter", "key-refused", @@ -107,7 +108,9 @@ const searchFor = (state: LabState): Search => ? { groupBy: "region", cloud_service: ["Example Compute"] } : state === "by-provider" ? { groupBy: "provider" } - : {} + : state === "cloudflare" + ? { provider: ["cloudflare"] } + : {} /** The states that start with nothing connected, so the dialog offers every provider. */ const startsEmpty = (state: LabState): boolean => diff --git a/apps/web/src/page/cost/cost.scene.test.ts b/apps/web/src/page/cost/cost.scene.test.ts index 60bfcee03c..6897b42f50 100644 --- a/apps/web/src/page/cost/cost.scene.test.ts +++ b/apps/web/src/page/cost/cost.scene.test.ts @@ -888,6 +888,59 @@ describe("cost page", () => { ) }) + test("Cloudflare rows read by name and drill down like the others: an account and a provider to their services", () => { + const account = "1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d" + const cloudflare = source({ + id: "0190f2b3-0000-7000-8000-00000000c061", + provider: "cloudflare", + accountId: account, + requestsThisMonth: 0, + }) + const rowsOf = (search: Search, keys: ReadonlyArray): Model => { + const model = answered(entered(search, [source(), cloudflare]).model) + return pageUpdate( + model, + Message.CompletedLoadBreakdown({ + key: model.breakdown.key ?? "", + result: Result.succeed( + Schema.decodeSync(CostBreakdownResponse)({ + currency: "USD", + rows: Arr.map(keys, (key) => ({ key, cost: 100, previousCost: 80 })), + }), + ), + }), + ).model + } + const accountHref = `/cost?groupBy=cloud_service&account=%5B%22${account}%22%5D` + scene( + { update: pageUpdate, view }, + given(rowsOf({ groupBy: "account" }, [account, "123456789012"])), + resolveTheme, + // The account reads as what it is, and filters by its id. + expectView(role("link", { name: /^Cloudflare/ })).toHaveAttr("href", accountHref), + expectView(role("link", { name: /^AWS 123456789012/ })).toExist(), + ) + scene( + { update: pageUpdate, view }, + given(rowsOf({ groupBy: "provider" }, ["cloudflare", "aws"])), + resolveTheme, + expectView(role("link", { name: "Cloudflare" })).toHaveAttr( + "href", + "/cost?groupBy=cloud_service&provider=%5B%22cloudflare%22%5D", + ), + ) + // A product under that filter moves on to regions, where Cloudflare has the one. + scene( + { update: pageUpdate, view }, + given(rowsOf({ provider: ["cloudflare"] }, ["Workers", "R2"])), + resolveTheme, + expectView(role("link", { name: "Workers" })).toHaveAttr( + "href", + "/cost?groupBy=region&cloud_service=%5B%22Workers%22%5D&provider=%5B%22cloudflare%22%5D", + ), + ) + }) + test("picking another grouping moves the URL, then re-reads the chart and the table, not the totals", () => { const model = populated({}) scene( diff --git a/apps/web/src/page/cost/marks.test.ts b/apps/web/src/page/cost/marks.test.ts index 70f688758c..84a82578f8 100644 --- a/apps/web/src/page/cost/marks.test.ts +++ b/apps/web/src/page/cost/marks.test.ts @@ -51,6 +51,33 @@ describe("cost key marks", () => { expect(keyMark("cloud_service", "", [])).toBeNull() }) + test("a Cloudflare product wears the Cloudflare mark, also beside an AWS source", () => { + const cloudflare = keyMark("provider", "cloudflare", [])?.icon + const both = [...sources, source("cloudflare", "1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d")] + expect(keyMark("cloud_service", "Workers", both)?.icon).toBe(cloudflare) + expect(keyMark("cloud_service", "Durable Objects", [])?.icon).toBe(cloudflare) + // Plan fees are filed under one service name. + expect(keyMark("cloud_service", "Subscriptions", both)?.icon).toBe(cloudflare) + // An AWS service beside it keeps the AWS mark. + expect(keyMark("cloud_service", "Amazon Simple Storage Service", both)?.icon).toBe( + keyMark("provider", "aws", [])?.icon, + ) + // A product Maple has no name for is Cloudflare's when no AWS source could bill it. + const alone = [source("cloudflare", "1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d")] + expect(keyMark("cloud_service", "Something New", alone)?.icon).toBe(cloudflare) + }) + + test("a Cloudflare account reads by name with its id aside, and bills in no region of its own", () => { + const account = "1a2b3c4d5e6f1a2b3c4d5e6f1a2b3c4d" + const both = [...sources, source("cloudflare", account)] + expect(keyLabel("account", account, both)).toBe("Cloudflare") + expect(keyDetail("account", account, both)).toBe(account) + expect(keyMark("account", account, both)?.icon).toBe(keyMark("provider", "cloudflare", [])?.icon) + // An AWS account's id is already in its name. + expect(keyDetail("account", "465760687006", both)).toBeNull() + expect(keyMark("region", "global", both)?.icon).not.toBe(keyMark("provider", "aws", [])?.icon) + }) + test("an AWS region says where it is; an unknown one adds nothing", () => { expect(keyDetail("region", "eu-central-1", [])).toBe("Frankfurt") expect(keyDetail("region", "mars-north-1", [])).toBeNull() diff --git a/apps/web/src/page/cost/marks.ts b/apps/web/src/page/cost/marks.ts index df189137fc..1075c9437f 100644 --- a/apps/web/src/page/cost/marks.ts +++ b/apps/web/src/page/cost/marks.ts @@ -23,7 +23,31 @@ const providerMark = (provider: CostProvider): KeyMark => { /** OpenRouter names a model `vendor/model`; an AWS service name never has a slash. */ const isModelSlug = (key: string): boolean => Str.includes("/")(key) -/** An AWS account id is twelve digits; an OpenRouter one is its user or org id. */ +/** + * What Cloudflare bills by: its product families, and the plan fees Maple files under one name. A + * family it adds later reads as Cloudflare's once the org has no AWS source to mistake it for. + */ +const CLOUDFLARE_SERVICES = new Set([ + "Workers", + "Durable Objects", + "Containers", + "Queues", + "R2", + "D1", + "Workers KV", + "Workers AI", + "Email", + "Subscriptions", +]) + +/** The provider a service that is not a model belongs to: AWS, unless it is Cloudflare's. */ +const serviceProvider = (key: string, sources: ReadonlyArray): CostProvider => { + if (CLOUDFLARE_SERVICES.has(key)) return "cloudflare" + const reads = (provider: CostProvider) => Arr.some(sources, (source) => source.provider === provider) + return reads("cloudflare") && !reads("aws") ? "cloudflare" : "aws" +} + +/** An AWS account id is twelve digits; an OpenRouter one is its user or org id, a Cloudflare one 32 hex digits. */ const isAwsAccountId = (key: string): boolean => /^\d{12}$/u.test(key) /** The provider whose source reads the account, from the org's sources, then by the id's shape. */ @@ -70,7 +94,9 @@ export const keyMark = ( return Option.match(decodeProvider(key), { onNone: () => null, onSome: providerMark }) case "cloud_service": if (isModelSlug(key)) return { icon: modelSlugIcon(key) } - return key === "AWS Lambda" ? { icon: "AwsLambdaIcon" } : providerMark("aws") + return key === "AWS Lambda" + ? { icon: "AwsLambdaIcon" } + : providerMark(serviceProvider(key, sources)) case "service_category": return glyph(isCategory(key) ? CATEGORY_ICONS[key] : "CubeIcon") case "region": @@ -83,8 +109,8 @@ export const keyMark = ( } /** - * An account by what reads it: an AWS one by its id, an OpenRouter one by name alone while it is - * the org's only one (its id means nothing to a reader). Unknown ids read as they are. + * An account by what reads it: an AWS one by its id, an OpenRouter or Cloudflare one by name alone + * while it is the org's only one (its id means nothing to a reader). Unknown ids read as they are. */ export const accountLabel = (key: string, sources: ReadonlyArray): string => { const provider = accountProvider(key, sources) @@ -134,8 +160,8 @@ const isAwsRegion = (key: string): key is keyof typeof AWS_REGIONS => Rec.has(AWS_REGIONS, key) /** - * What the name leaves out: where an AWS region is, the id behind an OpenRouter account's name. - * Null when the name says it all. + * What the name leaves out: where an AWS region is, the id behind an account that reads by its + * provider's name. Null when the name says it all. */ export const keyDetail = ( dimension: CostGroupBy, @@ -144,7 +170,8 @@ export const keyDetail = ( ): string | null => { if (dimension === "region") return isAwsRegion(key) ? AWS_REGIONS[key] : null if (dimension === "account") { - return accountProvider(key, sources) === "openrouter" ? key : null + const provider = accountProvider(key, sources) + return provider === undefined || provider === "aws" ? null : key } return null }