From 10bb58104ceba41ac9130341bd2a3aa3c053072f Mon Sep 17 00:00:00 2001 From: johnyeo Date: Wed, 3 Jun 2026 17:00:42 +0100 Subject: [PATCH 01/12] mcp analytics and refactor --- apps/leaf/src/mcp/http.ts | 14 +- package.json | 2 + packages/mcp/README.md | 22 +- .../mcp/src/{mcp-server => }/agent/axiom.ts | 52 +- .../{mcp-server => }/agent/pending-actions.ts | 10 +- packages/mcp/src/analytics/analyticsSink.ts | 37 ++ packages/mcp/src/analytics/analyticsTypes.ts | 42 ++ packages/mcp/src/analytics/axiomSink.ts | 72 +++ packages/mcp/src/analytics/emitToolEvent.ts | 67 +++ packages/mcp/src/analytics/index.ts | 12 + packages/mcp/src/analytics/instrumentTools.ts | 92 +++ packages/mcp/src/analytics/sessionId.ts | 24 + .../src/{mcp-server => }/console-logger.ts | 3 +- packages/mcp/src/constants.ts | 18 + packages/mcp/src/index.ts | 19 +- .../mcp/src/mcp-server/agent/ask-autumn.ts | 124 ---- packages/mcp/src/mcp-server/agent/auth.ts | 53 -- packages/mcp/src/mcp-server/agent/server.ts | 36 -- packages/mcp/src/mcp-server/agent/tools.ts | 554 ------------------ packages/mcp/src/mcp-server/oauth.ts | 333 ----------- .../agent/resources.ts => resources/index.ts} | 66 ++- packages/mcp/src/server/auth/auth.ts | 78 +++ packages/mcp/src/server/auth/oauth.ts | 202 +++++++ packages/mcp/src/server/auth/utils/errors.ts | 15 + .../mcp/src/server/auth/utils/principal.ts | 47 ++ packages/mcp/src/server/auth/utils/request.ts | 67 +++ packages/mcp/src/server/auth/utils/schemas.ts | 31 + packages/mcp/src/server/auth/utils/urls.ts | 63 ++ .../mcp/src/{mcp-server => server}/flags.ts | 0 packages/mcp/src/server/server.ts | 15 + packages/mcp/src/tools/balances.ts | 49 ++ packages/mcp/src/tools/billing.ts | 88 +++ packages/mcp/src/tools/customers.ts | 53 ++ packages/mcp/src/tools/index.ts | 135 +++++ packages/mcp/src/tools/plans.ts | 43 ++ packages/mcp/src/tools/utils/annotations.ts | 13 + packages/mcp/src/tools/utils/builders.ts | 100 ++++ packages/mcp/src/tools/utils/client.ts | 45 ++ packages/mcp/src/tools/utils/dates.ts | 53 ++ packages/mcp/src/tools/utils/debug.ts | 5 + packages/mcp/src/tools/utils/factories.ts | 164 ++++++ packages/mcp/src/tools/utils/types.ts | 61 ++ .../tests/evals/create-balance-evals.test.ts | 11 +- .../tests/evals/list-customers-evals.test.ts | 12 +- .../unit/mcp-server/agent/ask-autumn.test.ts | 244 -------- .../tests/unit/mcp-server/agent/axiom.test.ts | 7 +- .../mcp-server/agent/pending-actions.test.ts | 20 +- .../unit/mcp-server/agent/server.test.ts | 24 +- .../tests/unit/mcp-server/agent/tools.test.ts | 121 ++-- .../mcp/tests/unit/mcp-server/oauth.test.ts | 6 +- packages/mcp/tests/utils/eval-test-utils.ts | 11 +- packages/mcp/tests/utils/test-redis.ts | 2 +- scripts/axiom/cli.ts | 45 ++ scripts/axiom/createLeafDataset.ts | 105 ++++ scripts/axiom/setOtelVirtualFields.ts | 8 +- server/src/initHono.ts | 3 - server/src/routers/mcpProxyRouter.ts | 71 --- server/src/utils/auth.ts | 2 +- .../createSchedule/createScheduleParamsV0.ts | 41 +- 59 files changed, 2078 insertions(+), 1634 deletions(-) rename packages/mcp/src/{mcp-server => }/agent/axiom.ts (90%) rename packages/mcp/src/{mcp-server => }/agent/pending-actions.ts (95%) create mode 100644 packages/mcp/src/analytics/analyticsSink.ts create mode 100644 packages/mcp/src/analytics/analyticsTypes.ts create mode 100644 packages/mcp/src/analytics/axiomSink.ts create mode 100644 packages/mcp/src/analytics/emitToolEvent.ts create mode 100644 packages/mcp/src/analytics/index.ts create mode 100644 packages/mcp/src/analytics/instrumentTools.ts create mode 100644 packages/mcp/src/analytics/sessionId.ts rename packages/mcp/src/{mcp-server => }/console-logger.ts (91%) create mode 100644 packages/mcp/src/constants.ts delete mode 100644 packages/mcp/src/mcp-server/agent/ask-autumn.ts delete mode 100644 packages/mcp/src/mcp-server/agent/auth.ts delete mode 100644 packages/mcp/src/mcp-server/agent/server.ts delete mode 100644 packages/mcp/src/mcp-server/agent/tools.ts delete mode 100644 packages/mcp/src/mcp-server/oauth.ts rename packages/mcp/src/{mcp-server/agent/resources.ts => resources/index.ts} (91%) create mode 100644 packages/mcp/src/server/auth/auth.ts create mode 100644 packages/mcp/src/server/auth/oauth.ts create mode 100644 packages/mcp/src/server/auth/utils/errors.ts create mode 100644 packages/mcp/src/server/auth/utils/principal.ts create mode 100644 packages/mcp/src/server/auth/utils/request.ts create mode 100644 packages/mcp/src/server/auth/utils/schemas.ts create mode 100644 packages/mcp/src/server/auth/utils/urls.ts rename packages/mcp/src/{mcp-server => server}/flags.ts (100%) create mode 100644 packages/mcp/src/server/server.ts create mode 100644 packages/mcp/src/tools/balances.ts create mode 100644 packages/mcp/src/tools/billing.ts create mode 100644 packages/mcp/src/tools/customers.ts create mode 100644 packages/mcp/src/tools/index.ts create mode 100644 packages/mcp/src/tools/plans.ts create mode 100644 packages/mcp/src/tools/utils/annotations.ts create mode 100644 packages/mcp/src/tools/utils/builders.ts create mode 100644 packages/mcp/src/tools/utils/client.ts create mode 100644 packages/mcp/src/tools/utils/dates.ts create mode 100644 packages/mcp/src/tools/utils/debug.ts create mode 100644 packages/mcp/src/tools/utils/factories.ts create mode 100644 packages/mcp/src/tools/utils/types.ts delete mode 100644 packages/mcp/tests/unit/mcp-server/agent/ask-autumn.test.ts create mode 100644 scripts/axiom/cli.ts create mode 100644 scripts/axiom/createLeafDataset.ts delete mode 100644 server/src/routers/mcpProxyRouter.ts diff --git a/apps/leaf/src/mcp/http.ts b/apps/leaf/src/mcp/http.ts index 23fec47ea..9f5c38067 100644 --- a/apps/leaf/src/mcp/http.ts +++ b/apps/leaf/src/mcp/http.ts @@ -1,7 +1,6 @@ import { buildAuthForRequest, type ConsoleLogger, - createAskAutumnMCPServer, createAutumnOperationsMCPServer, getAuthorizationServerMetadata, getProtectedResourceMetadata, @@ -20,7 +19,7 @@ export interface McpRouteOptions extends MCPServerFlags { } type AppContext = Context<{ Bindings: HttpBindings }>; -type McpPath = "/mcp" | "/internal/mcp"; +type McpPath = "/mcp"; type McpApp = Hono<{ Bindings: HttpBindings }>; export function registerMcpRoutes(app: McpApp, options: McpRouteOptions) { @@ -28,12 +27,6 @@ export function registerMcpRoutes(app: McpApp, options: McpRouteOptions) { c.json(getProtectedResourceMetadata(c.req.raw.headers, options, "/mcp")), ); - app.get("/.well-known/oauth-protected-resource/internal/mcp", (c) => - c.json( - getProtectedResourceMetadata(c.req.raw.headers, options, "/internal/mcp"), - ), - ); - app.get("/.well-known/oauth-authorization-server", (c) => c.json(getAuthorizationServerMetadata(options)), ); @@ -41,7 +34,7 @@ export function registerMcpRoutes(app: McpApp, options: McpRouteOptions) { const handleMcp = async ( c: AppContext, path: McpPath, - server: ReturnType, + server: ReturnType, ) => { let auth: Awaited>; try { @@ -79,9 +72,6 @@ export function registerMcpRoutes(app: McpApp, options: McpRouteOptions) { app.all("/mcp", (c) => handleMcp(c, "/mcp", createAutumnOperationsMCPServer()), ); - app.all("/internal/mcp", (c) => - handleMcp(c, "/internal/mcp", createAskAutumnMCPServer()), - ); return app; } diff --git a/package.json b/package.json index 5872e1a2c..31e81e561 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,8 @@ "tb": "bun scripts/tinybird/index.ts", "tb:prod": "bun scripts/tinybird/index.ts prod", "tb:prod-legacy": "bun scripts/tinybird/index.ts prod-legacy", + "axiom": "ENV_FILE=.env infisical run --env=dev --recursive -- bun scripts/axiom/cli.ts", + "axiom:prod": "ENV_FILE=.env.prod infisical run --env=prod --recursive -- bun scripts/axiom/cli.ts", "trigger:deploy": "bunx trigger.dev deploy", "setupci": "node scripts/setup/setupci.js", "replicate": "bun scripts/db/replicate.ts", diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 9abe0f38b..ee1c58cf3 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -2,11 +2,10 @@ Mastra-backed MCP library for Autumn operations. -The hosted runtime lives in `apps/leaf` (see `src/mcp/http.ts`) and exposes two -Streamable HTTP MCP routes: +The hosted runtime lives in `apps/leaf` (see `src/mcp/http.ts`) and exposes a +Streamable HTTP MCP route: - `/mcp` - public, API-shaped operational tools. -- `/internal/mcp` - internal Autumn agent tool. ## `/mcp` @@ -31,19 +30,6 @@ The write tools are marked destructive. Clients should call the matching preview tool first where one exists and only call a write tool after explicit user confirmation. -## `/internal/mcp` - -Use this for Autumn-controlled agent flows. - -Tools: - -- `ask_autumn({ message, context? })` - -`ask_autumn` can look up customers/plans, inspect scoped Axiom logs when -available, preview billing changes, and apply confirmed billing writes. Billing -writes are preview-first: the server stores the pending action internally and -executes it only after a follow-up confirmation. - ## Local The routes are served by the `@autumn/leaf` app. From the repo root: @@ -52,15 +38,13 @@ The routes are served by the `@autumn/leaf` app. From the repo root: bun run leaf ``` -This starts both MCP routes (on the leaf port, `3099` by default): +This starts the MCP route (on the leaf port, `3099` by default): - `http://localhost:3099/mcp` -- `http://localhost:3099/internal/mcp` OAuth metadata is route-aware: - `http://localhost:3099/.well-known/oauth-protected-resource/mcp` -- `http://localhost:3099/.well-known/oauth-protected-resource/internal/mcp` OAuth uses the Autumn Better Auth issuer from `--server-url`: OAuth uses the Autumn Better Auth issuer from `MCP_SERVER_URL`: diff --git a/packages/mcp/src/mcp-server/agent/axiom.ts b/packages/mcp/src/agent/axiom.ts similarity index 90% rename from packages/mcp/src/mcp-server/agent/axiom.ts rename to packages/mcp/src/agent/axiom.ts index 82f7fe73e..3f59587b3 100644 --- a/packages/mcp/src/mcp-server/agent/axiom.ts +++ b/packages/mcp/src/agent/axiom.ts @@ -1,4 +1,10 @@ import { createHash } from "node:crypto"; +import { + makeScopeChecker, + type ScopeString, + Scopes, +} from "@autumn/shared/scopeDefinitions"; +import { ms } from "@autumn/shared/unixUtils"; import { Axiom } from "@axiomhq/js"; import { createTool } from "@mastra/core/tools"; import { @@ -9,18 +15,12 @@ import { isValid, parseISO, } from "date-fns"; -import { - makeScopeChecker, - Scopes, - type ScopeString, -} from "@autumn/shared/scopeDefinitions"; -import { ms } from "@autumn/shared/unixUtils"; import * as z from "zod/v4"; import { + type AutumnMcpAuth, createAutumnClient, getAutumnAuth, - type AutumnMcpAuth, -} from "./auth.js"; +} from "../server/auth/auth.js"; const axiomDataset = "express"; const defaultStartTime = "now-30m"; @@ -78,7 +78,9 @@ const getRangeMs = (startTime: string, endTime: string) => { }; const assertCanUseAxiom = (auth: AutumnMcpAuth) => { - if (!makeScopeChecker(auth.scopes).has(Scopes.Analytics.Read as ScopeString)) { + if ( + !makeScopeChecker(auth.scopes).has(Scopes.Analytics.Read as ScopeString) + ) { throw new Error("analytics:read scope is required to query Axiom logs."); } }; @@ -133,7 +135,9 @@ export const prepareAxiomQuery = ({ const rangeMs = getRangeMs(startTime, endTime); if (rangeMs === null || rangeMs <= 0 || rangeMs > maxRangeMs) { - throw new Error("Axiom queries must use a bounded time range of at most 7 days."); + throw new Error( + "Axiom queries must use a bounded time range of at most 7 days.", + ); } const trimmed = apl.trim(); @@ -152,7 +156,9 @@ export const prepareAxiomQuery = ({ } if (/\|\s*\[\s*['"][^'"]+['"]\s*\](?=\s*(?:\||$))/i.test(rest)) { - throw new Error("Axiom queries may only use the express dataset source once."); + throw new Error( + "Axiom queries may only use the express dataset source once.", + ); } if (/\bsearch\b/i.test(rest) && rangeMs > searchMaxRangeMs) { @@ -165,7 +171,9 @@ export const prepareAxiomQuery = ({ `| where ['context.org_id'] == '${escapeAplString(auth.orgId)}'`, `| where ['context.env'] == '${escapeAplString(auth.env)}'`, rest, - ].filter(Boolean).join("\n"), + ] + .filter(Boolean) + .join("\n"), startTime, endTime, }; @@ -180,11 +188,13 @@ export const createAxiomTools = () => ({ id: "queryAxiomLogs", description: "Run a read-only Axiom APL query against Autumn logs. The query is always constrained to the authenticated Autumn org and environment.", - inputSchema: z.object({ - apl: z.string().min(1), - startTime: z.string().optional(), - endTime: z.string().optional(), - }).strict(), + inputSchema: z + .object({ + apl: z.string().min(1), + startTime: z.string().optional(), + endTime: z.string().optional(), + }) + .strict(), execute: async ({ apl, startTime, endTime }, context) => { const auth = await withAxiomOrg(getAutumnAuth(context)); const query = prepareAxiomQuery({ auth, apl, startTime, endTime }); @@ -198,9 +208,11 @@ export const createAxiomTools = () => ({ id: "getAxiomDatasetFields", description: "List available Axiom field metadata for the express dataset, scoped to the authenticated Autumn org and environment.", - inputSchema: z.object({ - dataset: z.literal(axiomDataset), - }).strict(), + inputSchema: z + .object({ + dataset: z.literal(axiomDataset), + }) + .strict(), execute: async ({ dataset }, context) => { const auth = await withAxiomOrg(getAutumnAuth(context)); const query = prepareAxiomQuery({ diff --git a/packages/mcp/src/mcp-server/agent/pending-actions.ts b/packages/mcp/src/agent/pending-actions.ts similarity index 95% rename from packages/mcp/src/mcp-server/agent/pending-actions.ts rename to packages/mcp/src/agent/pending-actions.ts index 7ca55f55d..5cfd9512d 100644 --- a/packages/mcp/src/mcp-server/agent/pending-actions.ts +++ b/packages/mcp/src/agent/pending-actions.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { ms } from "@autumn/shared/unixUtils"; import { addMilliseconds, isPast } from "date-fns"; import { Redis } from "ioredis"; -import type { AutumnMcpAuth } from "./auth.js"; +import type { AutumnMcpAuth } from "../server/auth/auth.js"; export type BillingToolName = | "attach" @@ -89,7 +89,7 @@ const getRedis = (): PendingActionRedis => { }; const parseStoredAction = (value: string | null) => - (value ? (JSON.parse(value) as PendingBillingAction) : null); + value ? (JSON.parse(value) as PendingBillingAction) : null; const createAction = ({ auth, @@ -150,7 +150,11 @@ export const claimLatestPendingAction = async (auth: AutumnMcpAuth) => { if (!token || !action || isExpired(action)) { logPendingAction("claim-miss", { backend: "redis", - reason: !token ? "missing_latest" : !action ? "missing_action" : "expired", + reason: !token + ? "missing_latest" + : !action + ? "missing_action" + : "expired", token: token ? shortHash(token) : null, ...actionDebug(auth), }); diff --git a/packages/mcp/src/analytics/analyticsSink.ts b/packages/mcp/src/analytics/analyticsSink.ts new file mode 100644 index 000000000..397325f81 --- /dev/null +++ b/packages/mcp/src/analytics/analyticsSink.ts @@ -0,0 +1,37 @@ +import type { AnalyticsSink } from "./analyticsTypes.js"; +import { createAxiomAnalyticsSink } from "./axiomSink.js"; + +const DEFAULT_DATASET = "leaf"; + +const noopSink: AnalyticsSink = { + emit() {}, + flush: async () => {}, +}; + +let cachedSink: AnalyticsSink | null | undefined; +let overrideSink: AnalyticsSink | null | undefined; + +/** + * Override the analytics sink (tests, or wiring a pino/OTEL sink from the host + * app). Pass `null` to disable. Pass `undefined` to fall back to env defaults. + */ +export const setAnalyticsSink = (sink: AnalyticsSink | null | undefined) => { + overrideSink = sink; + if (sink !== undefined) cachedSink = undefined; +}; + +export const getAnalyticsSink = (): AnalyticsSink => { + if (overrideSink !== undefined) return overrideSink ?? noopSink; + if (cachedSink === undefined) { + cachedSink = createAxiomAnalyticsSink({ + token: process.env.AXIOM_TOKEN, + orgId: process.env.AXIOM_ORG_ID, + dataset: process.env.MCP_ANALYTICS_DATASET ?? DEFAULT_DATASET, + }); + } + return cachedSink ?? noopSink; +}; + +/** True when a real sink is configured — lets callers skip hot-path work. */ +export const isAnalyticsEnabled = (): boolean => + getAnalyticsSink() !== noopSink; diff --git a/packages/mcp/src/analytics/analyticsTypes.ts b/packages/mcp/src/analytics/analyticsTypes.ts new file mode 100644 index 000000000..55d3cfb22 --- /dev/null +++ b/packages/mcp/src/analytics/analyticsTypes.ts @@ -0,0 +1,42 @@ +/** + * Where a tool call originated: + * - `mcp` — an external MCP client hitting our hosted server (e.g. Claude + * Code, Cursor). The #1 usage-analytics target. + * - `agent` — our own Autumn Ops agent (e.g. Slack) invoking tools + * internally. Drives agent reliability / failure detection. + */ +export type McpAnalyticsSurface = "mcp" | "agent"; + +export type McpAnalyticsEvent = { + event: "mcp.tool_call"; + surface: McpAnalyticsSurface; + tool: string; + status: "ok" | "error"; + durationMs: number; + principalId: string; + env: string; + /** Resolved lazily; may be absent if org resolution fails. */ + orgId?: string | undefined; + /** HTTP User-Agent of the calling MCP client. Absent for `agent` surface. */ + client?: string | undefined; + /** Stateless session grouping: hash(principal + client + time window). */ + sessionId: string; + scopes?: string[] | undefined; + /** Tool request payload (stored as an Axiom map field). */ + input?: unknown; + /** Tool result payload (stored as an Axiom map field). */ + output?: unknown; + error?: string | undefined; +}; + +/** + * Pluggable destination for analytics events. Implementations must be + * non-blocking: `emit` runs on the hot path of every tool call and must never + * throw or await network I/O inline. Swap this (Axiom direct, `@axiomhq/pino`, + * an OTEL exporter, a test spy) without touching the instrumentation layer. + */ +export interface AnalyticsSink { + emit(event: McpAnalyticsEvent): void; + /** Drain any buffered events. Call on graceful shutdown. */ + flush(): Promise; +} diff --git a/packages/mcp/src/analytics/axiomSink.ts b/packages/mcp/src/analytics/axiomSink.ts new file mode 100644 index 000000000..92577d8df --- /dev/null +++ b/packages/mcp/src/analytics/axiomSink.ts @@ -0,0 +1,72 @@ +import { Axiom } from "@axiomhq/js"; +import type { AnalyticsSink, McpAnalyticsEvent } from "./analyticsTypes.js"; + +const maxPayloadBytes = + Number(process.env.MCP_ANALYTICS_MAX_PAYLOAD_BYTES) || 512_000; + +/** + * Map fields require an object value. Wrap scalars/arrays so heterogeneous + * tool outputs still land in a single Axiom map field instead of conflicting + * on type. + */ +const asMap = (value: unknown): Record => + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : { value }; + +/** + * Keep individual events under Axiom's 1MB field cap. Oversized payloads are + * replaced with a marker rather than dropping the whole (otherwise rejected) + * event. + */ +const guardPayload = (value: unknown): unknown => { + if (value === undefined) return undefined; + try { + const json = JSON.stringify(value); + if (json && json.length > maxPayloadBytes) { + return { _truncated: true, _bytes: json.length }; + } + return value; + } catch { + return { _unserializable: true }; + } +}; + +const toAxiomRecord = (event: McpAnalyticsEvent) => ({ + _time: new Date().toISOString(), + event: event.event, + surface: event.surface, + tool: event.tool, + status: event.status, + duration_ms: event.durationMs, + org_id: event.orgId, + principal_id: event.principalId, + env: event.env, + client: event.client, + session_id: event.sessionId, + scopes: event.scopes, + // Map fields — see scripts/axiom/createLeafDataset.ts + input: asMap(guardPayload(event.input)), + output: asMap(guardPayload(event.output)), + error: event.error, +}); + +export const createAxiomAnalyticsSink = ({ + token, + orgId, + dataset, +}: { + token?: string | undefined; + orgId?: string | undefined; + dataset: string; +}): AnalyticsSink | null => { + if (!token) return null; + const client = new Axiom({ token, orgId }); + return { + emit(event) { + // Axiom batches internally; no inline await on the hot path. + client.ingest(dataset, [toAxiomRecord(event)]); + }, + flush: () => client.flush(), + }; +}; diff --git a/packages/mcp/src/analytics/emitToolEvent.ts b/packages/mcp/src/analytics/emitToolEvent.ts new file mode 100644 index 000000000..ecbaadc79 --- /dev/null +++ b/packages/mcp/src/analytics/emitToolEvent.ts @@ -0,0 +1,67 @@ +import { resolveAutumnOrgId } from "../agent/axiom.js"; +import type { AutumnMcpAuth } from "../server/auth/auth.js"; +import { getAnalyticsSink } from "./analyticsSink.js"; +import type { McpAnalyticsSurface } from "./analyticsTypes.js"; +import { deriveSessionId } from "./sessionId.js"; + +/** + * Builds and dispatches a single tool-call analytics event. Org resolution and + * the actual sink write run off the hot path so the tool response is never + * delayed by analytics. + */ +export const emitMcpToolEvent = ({ + surface, + toolId, + auth, + client, + status, + durationMs, + input, + output, + error, +}: { + surface: McpAnalyticsSurface; + toolId: string; + auth: AutumnMcpAuth; + client: string | undefined; + status: "ok" | "error"; + durationMs: number; + input?: unknown; + output?: unknown; + error?: string | undefined; +}) => { + const sink = getAnalyticsSink(); + + // Resolve org off the hot path; resolveAutumnOrgId is cached (~5min). + void (async () => { + let orgId = auth.orgId; + if (!orgId) { + try { + orgId = await resolveAutumnOrgId(auth); + } catch { + // Best-effort: emit without org_id rather than dropping the event. + } + } + const now = Date.now(); + sink.emit({ + event: "mcp.tool_call", + surface, + tool: toolId, + status, + durationMs, + orgId, + principalId: auth.principalId, + env: auth.env, + client, + sessionId: deriveSessionId({ + principalId: auth.principalId, + client, + now, + }), + scopes: auth.scopes, + input, + output, + error, + }); + })(); +}; diff --git a/packages/mcp/src/analytics/index.ts b/packages/mcp/src/analytics/index.ts new file mode 100644 index 000000000..15106380b --- /dev/null +++ b/packages/mcp/src/analytics/index.ts @@ -0,0 +1,12 @@ +export { + getAnalyticsSink, + isAnalyticsEnabled, + setAnalyticsSink, +} from "./analyticsSink.js"; +export type { + AnalyticsSink, + McpAnalyticsEvent, + McpAnalyticsSurface, +} from "./analyticsTypes.js"; +export { createAxiomAnalyticsSink } from "./axiomSink.js"; +export { instrumentToolsWithAnalytics } from "./instrumentTools.js"; diff --git a/packages/mcp/src/analytics/instrumentTools.ts b/packages/mcp/src/analytics/instrumentTools.ts new file mode 100644 index 000000000..28e4c18fb --- /dev/null +++ b/packages/mcp/src/analytics/instrumentTools.ts @@ -0,0 +1,92 @@ +import type { createTool } from "@mastra/core/tools"; +import { type AutumnMcpAuth, getAutumnAuth } from "../server/auth/auth.js"; +import { isAnalyticsEnabled } from "./analyticsSink.js"; +import type { McpAnalyticsSurface } from "./analyticsTypes.js"; +import { emitMcpToolEvent } from "./emitToolEvent.js"; + +type AnyTool = ReturnType; +type ToolContext = Parameters>[1]; + +const getClientFromContext = (context: ToolContext): string | undefined => { + const extra = ( + context as { + mcp?: { + extra?: { + requestInfo?: { headers?: Record }; + }; + }; + } + )?.mcp?.extra; + return extra?.requestInfo?.headers?.["user-agent"]; +}; + +const extractRequest = (input: unknown): unknown => + input && typeof input === "object" && "request" in input + ? (input as { request: unknown }).request + : input; + +/** + * Wraps each tool's `execute` to emit a usage event per call. Auth/identity is + * read from the same MCP context the tools already use, so an unauthenticated + * call simply skips analytics (it would have failed in the tool anyway). + * + * Tools are created fresh per request (see `createAutumnOperationsMCPServer`), + * so mutating `execute` here carries no shared-state risk. + * + * @param tools The toolset to instrument (mutated in place and returned). + * @param surface Origin of the calls — `mcp` (external clients) or `agent` + * (our own Autumn Ops agent). + */ +export const instrumentToolsWithAnalytics = < + T extends Record, +>({ + tools, + surface, +}: { + tools: T; + surface: McpAnalyticsSurface; +}): T => { + if (!isAnalyticsEnabled()) return tools; + + for (const [toolId, tool] of Object.entries(tools)) { + const original = tool.execute; + if (!original) continue; + tool.execute = (async (input: unknown, context: ToolContext) => { + const started = Date.now(); + let auth: AutumnMcpAuth | undefined; + try { + auth = getAutumnAuth(context); + } catch { + return original(input as never, context as never); + } + const client = getClientFromContext(context); + try { + const output = await original(input as never, context as never); + emitMcpToolEvent({ + surface, + toolId, + auth, + client, + status: "ok", + durationMs: Date.now() - started, + input: extractRequest(input), + output, + }); + return output; + } catch (error) { + emitMcpToolEvent({ + surface, + toolId, + auth, + client, + status: "error", + durationMs: Date.now() - started, + input: extractRequest(input), + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + }) as AnyTool["execute"]; + } + return tools; +}; diff --git a/packages/mcp/src/analytics/sessionId.ts b/packages/mcp/src/analytics/sessionId.ts new file mode 100644 index 000000000..f6318f3a0 --- /dev/null +++ b/packages/mcp/src/analytics/sessionId.ts @@ -0,0 +1,24 @@ +import { createHash } from "node:crypto"; +import { ms } from "@autumn/shared/unixUtils"; + +const sessionWindowMs = ms.minutes(30); + +const hash = (value: string) => + createHash("sha256").update(value).digest("hex").slice(0, 32); + +/** + * Stateless session grouping. The serverless MCP transport issues no + * Mcp-Session-Id, so we synthesize one from the principal + client + a coarse + * time bucket — calls from the same client within the window collapse into one + * session. + */ +export const deriveSessionId = ({ + principalId, + client, + now, +}: { + principalId: string; + client: string | undefined; + now: number; +}) => + hash(`${principalId}|${client ?? ""}|${Math.floor(now / sessionWindowMs)}`); diff --git a/packages/mcp/src/mcp-server/console-logger.ts b/packages/mcp/src/console-logger.ts similarity index 91% rename from packages/mcp/src/mcp-server/console-logger.ts rename to packages/mcp/src/console-logger.ts index 299a1e0ea..bf1da90f1 100644 --- a/packages/mcp/src/mcp-server/console-logger.ts +++ b/packages/mcp/src/console-logger.ts @@ -16,7 +16,8 @@ export type ConsoleLogger = Record & { export function createConsoleLogger(level: ConsoleLoggerLevel): ConsoleLogger { const min = consoleLoggerLevels.indexOf(level); const noop = () => {}; - const log = (method: "debug" | "info" | "warn" | "error"): LogMethod => + const log = + (method: "debug" | "info" | "warn" | "error"): LogMethod => (message, data) => { if (data) console[method](message, data); else console[method](message); diff --git a/packages/mcp/src/constants.ts b/packages/mcp/src/constants.ts new file mode 100644 index 000000000..f6482c8f2 --- /dev/null +++ b/packages/mcp/src/constants.ts @@ -0,0 +1,18 @@ +import type { ScopeString } from "@autumn/shared/scopeDefinitions"; +import { Scopes } from "@autumn/shared/scopeDefinitions"; + +/** Shared defaults for talking to the Autumn API from the MCP server. */ +export const DEFAULT_AUTUMN_API_URL = "https://api.useautumn.com"; +export const DEFAULT_API_VERSION = "2.3.0"; + +/** Scopes requested when exchanging an OAuth token for an Autumn API key. */ +export const MCP_OAUTH_SCOPES = [ + Scopes.Customers.Read, + Scopes.Customers.Write, + Scopes.Plans.Read, + Scopes.Plans.Write, + Scopes.Billing.Read, + Scopes.Billing.Write, + Scopes.Balances.Write, + Scopes.Analytics.Read, +] as const satisfies readonly ScopeString[]; diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index c37c05ee5..2144896ee 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1,19 +1,24 @@ export { - createAskAutumnMCPServer, - createAutumnOperationsMCPServer, - createMCPServer, -} from "./mcp-server/agent/server.js"; + type AnalyticsSink, + createAxiomAnalyticsSink, + getAnalyticsSink, + isAnalyticsEnabled, + type McpAnalyticsEvent, + type McpAnalyticsSurface, + setAnalyticsSink, +} from "./analytics/index.js"; export { type ConsoleLogger, type ConsoleLoggerLevel, consoleLoggerLevels, createConsoleLogger, -} from "./mcp-server/console-logger.js"; -export type { MCPServerFlags } from "./mcp-server/flags.js"; +} from "./console-logger.js"; export { buildAuthForRequest, getAuthorizationServerMetadata, getProtectedResourceMetadata, type OAuthEnvironment, OAuthHttpError, -} from "./mcp-server/oauth.js"; +} from "./server/auth/oauth.js"; +export type { MCPServerFlags } from "./server/flags.js"; +export { createAutumnOperationsMCPServer } from "./server/server.js"; diff --git a/packages/mcp/src/mcp-server/agent/ask-autumn.ts b/packages/mcp/src/mcp-server/agent/ask-autumn.ts deleted file mode 100644 index 9eb082363..000000000 --- a/packages/mcp/src/mcp-server/agent/ask-autumn.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Agent } from "@mastra/core/agent"; -import { createTool } from "@mastra/core/tools"; -import * as z from "zod/v4"; -import { - type AutumnMcpAuth, - createRequestContext, - getAutumnAuth, -} from "./auth.js"; -import { getLatestPendingAction } from "./pending-actions.js"; -import { createAgentAutumnOperationTools } from "./tools.js"; - -const model = "anthropic/claude-sonnet-4-6"; - -const instructions = `You are Autumn's operational billing assistant. -Use Autumn tools for customer, plan, and billing work. -Use Axiom tools only for read-only investigation of Autumn logs. - -Rules: -- Read requests can be answered directly. -- For plan-attribute queries, call listPlans first and filter returned plans locally. -- For customer-heavy queries, push filters into listCustomers and paginate for complete results. -- For customer lookup, use listCustomers first when the id/email/name is ambiguous. -- For plan lookup, use listPlans first when the plan is ambiguous. -- Avoid getCustomer fan-out unless listCustomers is missing details required by the user. -- For customer creation, use createCustomer only when the user explicitly asks to create or pre-create a customer. -- For plan creation, gather plan id, name, price, items/features, trials, and add-on/default behavior before calling createPlan. -- For standalone credit or balance grants, use previewCreateBalance before createBalance. Use entity_id for entity-scoped grants, included_grant for the granted amount, expires_at in milliseconds for expiring grants, and omit reset when using expires_at. -- For multi-phase billing schedules, gather customer, optional entity, ordered phase start times, and phase plans before calling previewCreateSchedule. -- Use dateToEpochMilliseconds to convert user-facing dates into epoch milliseconds before calling tools with starts_at or expires_at fields; if a named timezone matters, ask for or use an explicit offset. -- If a fee schedule says year 1 is already paid or has no billing changes, do not add an immediate/year-1 phase; start the schedule at the first future billing change. -- For custom consumable grants, map "per month/year" to customize.items[].reset.interval. Omit reset only for unlimited, non-consumable, or clearly one-time grants. -- For billing changes, call previewAttach or previewUpdateSubscription first. These preview tools automatically create the pending billing action. -- previewCreateSchedule stores the pending createSchedule write; after it returns pending, ask the user to confirm the exact schedule before applying it. -- previewCreateBalance stores the pending createBalance write; after it returns pending, ask the user to confirm the exact balance grant before applying it. -- createPlan stores a pending write; after it returns pending, ask the user to confirm the exact plan configuration before applying it. -- Never expose internal ids or server bookkeeping details. -- After a billing preview, tell the user to explicitly apply or approve the exact previewed change. -- If the user semantically confirms, applies, or approves a billing preview, call confirmBillingAction even if the preview is not visible in the current message. The tool validates whether a pending action exists. -- Never claim a billing write has been applied unless confirmBillingAction succeeds. -- If customer, plan, entity, subscription, or environment is ambiguous, ask a short clarifying question. -- Keep responses concise. Use JSON only when it materially helps debugging.`; - -// To be added when we add axiom: -// - For log investigations, start with narrow structured fields such as context.customer_id, context.org_slug, req.url, req.id, stripe_event.id, stripe_event.type, workflow.id, or workflow.name. -// - For wide log windows, use a cheap aggregate query first, then focused <= 1 hour queries. Prefer ERROR/WARN levels first. -// - Axiom queries are already scoped to the authenticated org and environment; do not add or mention separate org filters unless useful to explain the investigation. -// - Axiom tools are read-only and must never be used as part of a billing confirmation or write flow. - -const createAgent = () => - new Agent({ - id: "autumn-ops", - name: "Autumn Ops", - description: - "Answers Autumn customer, plan, and billing questions using controlled Autumn operations.", - instructions, - model, - tools: createAgentAutumnOperationTools(), - }); - -const getAuth = ( - toolContext: Parameters< - NonNullable["execute"]> - >[1], - defaultAuth?: AutumnMcpAuth, -) => { - try { - return getAutumnAuth(toolContext); - } catch (error) { - if (defaultAuth) return defaultAuth; - throw error; - } -}; - -const getPendingAction = async (auth: AutumnMcpAuth) => { - try { - return await getLatestPendingAction(auth); - } catch { - return null; - } -}; - -export const createAskAutumnTool = (defaultAuth?: AutumnMcpAuth) => - createTool({ - id: "ask_autumn", - description: - "Ask Autumn to look up customers/plans or safely preview and confirm billing changes.", - inputSchema: z.object({ - message: z.string().min(1), - context: z.record(z.string(), z.unknown()).optional(), - }), - mcp: { - annotations: { - title: "Ask Autumn", - readOnlyHint: false, - destructiveHint: true, - idempotentHint: false, - openWorldHint: false, - }, - }, - execute: async ({ message, context }, toolContext) => { - const auth = getAuth(toolContext, defaultAuth); - const pendingAction = await getPendingAction(auth); - const contextText = context - ? `\n\nCaller context:\n${JSON.stringify(context, null, 2)}` - : ""; - const pendingText = pendingAction - ? `\n\nPending billing action:\nTool: ${pendingAction.toolName}\nPreview: ${pendingAction.preview}\nIf the user confirms this preview, call confirmBillingAction.` - : ""; - const output = await createAgent().generate(message, { - maxSteps: 8, - requestContext: createRequestContext(auth), - context: [ - { - role: "system", - content: `Current Autumn environment: ${auth.env}.${pendingText}${contextText}`, - }, - ], - }); - - return output.text; - }, - }); - -export const askAutumnTool = createAskAutumnTool(); diff --git a/packages/mcp/src/mcp-server/agent/auth.ts b/packages/mcp/src/mcp-server/agent/auth.ts deleted file mode 100644 index aba42a7ab..000000000 --- a/packages/mcp/src/mcp-server/agent/auth.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { createHash } from "node:crypto"; -import { RequestContext } from "@mastra/core/request-context"; -import type { ToolExecutionContext } from "@mastra/core/tools"; -import type { OAuthEnvironment } from "../oauth.js"; - -export type AutumnMcpAuth = { - apiKey: string; - env: OAuthEnvironment; - principalId: string; - resource: string; - scopes: string[]; - orgId?: string | undefined; - serverURL?: string | undefined; - xApiVersion?: string | undefined; - failOpen?: boolean | undefined; -}; - -type MaybeToolContext = Pick; - -const hash = (value: string) => - createHash("sha256").update(value).digest("hex").slice(0, 32); - -export const principalFromSecret = (kind: string, value: string) => - `${kind}:${hash(value)}`; - -export const createAutumnClient = (auth: AutumnMcpAuth) => ({ - baseUrl: auth.serverURL ?? "https://api.useautumn.com", - headers: { - Authorization: `Bearer ${auth.apiKey}`, - "Content-Type": "application/json", - Accept: "application/json", - "x-api-version": auth.xApiVersion ?? "2.3.0", - ...(auth.failOpen === undefined - ? {} - : { "fail-open": String(auth.failOpen) }), - }, -}); - -export const getAutumnAuth = (context?: MaybeToolContext): AutumnMcpAuth => { - const direct = context?.mcp?.extra?.authInfo as AutumnMcpAuth | undefined; - const nested = context?.requestContext?.get?.("mcp.extra") as - | { authInfo?: AutumnMcpAuth } - | undefined; - const auth = direct ?? nested?.authInfo; - if (!auth?.apiKey) throw new Error("Autumn MCP authentication is required."); - return auth; -}; - -export const createRequestContext = (auth: AutumnMcpAuth) => { - const requestContext = new RequestContext(); - requestContext.set("mcp.extra", { authInfo: auth }); - return requestContext; -}; diff --git a/packages/mcp/src/mcp-server/agent/server.ts b/packages/mcp/src/mcp-server/agent/server.ts deleted file mode 100644 index 3750e4c0b..000000000 --- a/packages/mcp/src/mcp-server/agent/server.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MCPServer } from "@mastra/mcp"; -import { createAskAutumnTool } from "./ask-autumn.js"; -import type { AutumnMcpAuth } from "./auth.js"; -import { autumnMcpResources } from "./resources.js"; -import { createRawAutumnOperationTools } from "./tools.js"; - -export const createAskAutumnMCPServer = (_opts?: { - defaultAuth?: AutumnMcpAuth; -}) => - new MCPServer({ - id: "autumn-internal-mcp", - name: "Autumn Internal MCP", - version: "0.0.1", - description: - "Ask Autumn to safely operate on customers, plans, and billing.", - instructions: - "Use ask_autumn for all Autumn work. Billing writes require preview and explicit user confirmation.", - tools: { - ask_autumn: createAskAutumnTool(_opts?.defaultAuth), - }, - resources: autumnMcpResources, - }); - -export const createAutumnOperationsMCPServer = () => - new MCPServer({ - id: "autumn-mcp", - name: "Autumn MCP", - version: "0.0.1", - description: "Operate on Autumn customers, plans, and billing.", - instructions: - "Use preview tools before billing writes. Write tools are destructive and should only be called after explicit user confirmation.", - tools: createRawAutumnOperationTools(), - resources: autumnMcpResources, - }); - -export const createMCPServer = createAskAutumnMCPServer; diff --git a/packages/mcp/src/mcp-server/agent/tools.ts b/packages/mcp/src/mcp-server/agent/tools.ts deleted file mode 100644 index b3ae76f65..000000000 --- a/packages/mcp/src/mcp-server/agent/tools.ts +++ /dev/null @@ -1,554 +0,0 @@ -import { - AttachParamsV1Schema, - CreateBalanceParamsV0Schema, - CreateCustomerParamsV1Schema, - CreatePlanParamsV2Schema, - CreateSchedulePhaseSchema, - CreateScheduleParamsV0Schema, - GetCustomerParamsV1Schema, - GetPlanParamsV0Schema, - ListCustomersV2_3ParamsSchema, - ListPlanParamsSchema, - UpdateSubscriptionV1ParamsSchema, -} from "@autumn/shared/publicApiSchemas"; -import { createTool } from "@mastra/core/tools"; -import { isValid, parseISO } from "date-fns"; -import * as z from "zod/v4"; -import { createAutumnClient, getAutumnAuth } from "./auth.js"; -import { - claimLatestPendingAction, - createPendingAction, -} from "./pending-actions.js"; - -type ToolContext = Parameters< - NonNullable["execute"]> ->[1]; -type ConfirmedWriteToolName = - | "attach" - | "updateSubscription" - | "createPlan" - | "createSchedule" - | "createBalance"; -type OperationToolConfig = { - id: string; - description: string; - schema: z.ZodType; - endpoint: string; - destructive?: boolean; - idempotent?: boolean; -}; -type BillingPreviewToolConfig = { - id: string; - description: string; - schema: z.ZodType; - previewEndpoint: string; - writeToolName: ConfirmedWriteToolName; -}; -type LocalPreviewToolConfig = { - id: string; - description: string; - schema: z.ZodType; - writeToolName: ConfirmedWriteToolName; - preview: (request: unknown) => unknown; -}; - -export const endpointByTool = { - listCustomers: "/v1/customers.list", - createCustomer: "/v1/customers.get_or_create", - getCustomer: "/v1/customers.get", - listPlans: "/v1/plans.list", - createPlan: "/v1/plans.create", - getPlan: "/v1/plans.get", - previewAttach: "/v1/billing.preview_attach", - attach: "/v1/billing.attach", - previewUpdateSubscription: "/v1/billing.preview_update", - updateSubscription: "/v1/billing.update", - previewCreateSchedule: "/v1/billing.preview_create_schedule", - createSchedule: "/v1/billing.create_schedule", - createBalance: "/v1/balances.create", -} as const; - -const epochMillisecondsSchema = z - .union([z.number(), z.string()]) - .transform((value, context) => { - if (typeof value === "number") { - if (Number.isFinite(value)) return value; - } else { - const normalized = /^\d{4}-\d{2}-\d{2}$/.test(value) - ? `${value}T00:00:00.000Z` - : value; - const hasExplicitZone = /(?:z|[+-]\d{2}:?\d{2})$/i.test(normalized); - const parsed = parseISO(hasExplicitZone ? normalized : `${normalized}Z`); - if (isValid(parsed)) return parsed.getTime(); - } - - context.addIssue({ - code: "custom", - message: - "Expected epoch milliseconds or an ISO date/timestamp string.", - }); - return z.NEVER; - }); - -const createSchedulePhaseMcpSchema = CreateSchedulePhaseSchema.extend({ - starts_at: epochMillisecondsSchema.meta({ - description: - "Phase start time as epoch milliseconds or an ISO date string. Date-only values use midnight UTC.", - }), -}); - -const createScheduleMcpSchema = CreateScheduleParamsV0Schema.extend({ - phases: z.tuple([createSchedulePhaseMcpSchema]).rest( - createSchedulePhaseMcpSchema, - ), -}); - -const createBalanceMcpSchema = CreateBalanceParamsV0Schema.extend({ - expires_at: epochMillisecondsSchema.optional().meta({ - description: - "Expiry time as epoch milliseconds or an ISO date string. Date-only values use midnight UTC.", - }), -}); - -const listCustomersMcpSchema = ListCustomersV2_3ParamsSchema.extend({ - limit: z - .preprocess( - (value) => (typeof value === "number" && value > 1000 ? 1000 : value), - z.number().int().positive().max(1000).optional(), - ) - .meta({ description: "Maximum customers per page. Max 1000." }), -}); - -const writeSchemaByTool = { - attach: AttachParamsV1Schema, - updateSubscription: UpdateSubscriptionV1ParamsSchema, - createPlan: CreatePlanParamsV2Schema, - createSchedule: createScheduleMcpSchema, - createBalance: createBalanceMcpSchema, -} as const satisfies Record; - -export const schemaByTool = { - listCustomers: listCustomersMcpSchema, - createCustomer: CreateCustomerParamsV1Schema, - getCustomer: GetCustomerParamsV1Schema, - listPlans: ListPlanParamsSchema, - createPlan: CreatePlanParamsV2Schema, - getPlan: GetPlanParamsV0Schema, - previewAttach: AttachParamsV1Schema, - attach: AttachParamsV1Schema, - previewUpdateSubscription: UpdateSubscriptionV1ParamsSchema, - updateSubscription: UpdateSubscriptionV1ParamsSchema, - previewCreateSchedule: createScheduleMcpSchema, - createSchedule: createScheduleMcpSchema, - previewCreateBalance: createBalanceMcpSchema, - createBalance: createBalanceMcpSchema, -} as const satisfies Record< - keyof typeof endpointByTool | "previewCreateBalance", - z.ZodType ->; - -const toolConfigs: OperationToolConfig[] = [ - { - id: "listCustomers", - description: - "List Autumn customers. Use search, plans, subscription_status, and processors filters for customer-heavy queries. limit max is 1000. For queued/upcoming plan version queries, use subscription_status scheduled and omit the earliest matching version unless the user asks for all historical versions (versions 1,2,3 -> filter 2,3). 'live', 'paying', and active subscribers usually mean subscription_status active. When a plan is named, include the plans filter instead of listing broad customer sets. If listPlans returned matching versions, pass only relevant versions in plans[].versions, never guessed versions. For every/all/complete requests, paginate by calling again with start_cursor set to the previous response's next_cursor until next_cursor is empty.", - schema: listCustomersMcpSchema, - endpoint: endpointByTool.listCustomers, - }, - { - id: "createCustomer", - description: - "Create an Autumn customer, or return the existing customer with the same id. Use when the user explicitly wants a customer record created.", - schema: CreateCustomerParamsV1Schema, - endpoint: endpointByTool.createCustomer, - idempotent: true, - }, - { - id: "getCustomer", - description: "Fetch one Autumn customer by id.", - schema: GetCustomerParamsV1Schema, - endpoint: endpointByTool.getCustomer, - }, - { - id: "listPlans", - description: - "List Autumn plans. This is usually a cheap full scan; filter returned plans locally and use matching id/version pairs before customer queries based on plan attributes.", - schema: ListPlanParamsSchema, - endpoint: endpointByTool.listPlans, - }, - { - id: "createPlan", - description: - "Create an Autumn plan. Destructive configuration write: gather plan_id, name, price, features/items, trials, and confirmation before running.", - schema: CreatePlanParamsV2Schema, - endpoint: endpointByTool.createPlan, - destructive: true, - }, - { - id: "createBalance", - description: - "Create a standalone customer balance grant. Use when a user asks to give, add, grant, or provision credits/balance to a customer or entity. Destructive: preview first; use entity_id for entity-scoped credits, included_grant for the grant amount, expires_at for expiring grants, and omit reset when using expires_at. For relative expiries like '2 months', use calendar months, not a 30-day approximation. expires_at accepts epoch milliseconds or ISO/date strings.", - schema: createBalanceMcpSchema, - endpoint: endpointByTool.createBalance, - destructive: true, - }, - { - id: "getPlan", - description: "Fetch one Autumn plan by id and optional version.", - schema: GetPlanParamsV0Schema, - endpoint: endpointByTool.getPlan, - }, -]; - -const localPreviewConfigs: LocalPreviewToolConfig[] = [ - { - id: "previewCreateBalance", - description: - "Preview a standalone balance grant before createBalance. Use when a user asks to give, add, grant, or provision credits/balance to a customer or entity. Use for one-time credit grants, referral/promotional credits, and entity-scoped credits. Does not mutate Autumn. For relative expiries like '2 months', use calendar months. expires_at accepts epoch milliseconds or ISO/date strings.", - schema: createBalanceMcpSchema, - writeToolName: "createBalance", - preview: (request) => ({ - action: "createBalance", - request, - impact: - "Creates a standalone balance grant. If entity_id is present, the balance is scoped to that entity. If expires_at is present, the grant expires at that timestamp.", - }), - }, -]; - -const billingPreviewConfigs: BillingPreviewToolConfig[] = [ - { - id: "previewAttach", - description: - "Preview attaching a plan before attach. Include feature_quantities and custom items/prices; map recurring custom grants like 'per month/year' to reset.interval.", - schema: AttachParamsV1Schema, - previewEndpoint: endpointByTool.previewAttach, - writeToolName: "attach", - }, - { - id: "previewUpdateSubscription", - description: - "Preview updating a subscription before updateSubscription. Include quantity/custom item changes; recurring custom grants need reset.interval.", - schema: UpdateSubscriptionV1ParamsSchema, - previewEndpoint: endpointByTool.previewUpdateSubscription, - writeToolName: "updateSubscription", - }, - { - id: "previewCreateSchedule", - description: - "Preview billing impact of a multi-phase schedule before createSchedule. starts_at accepts epoch milliseconds or ISO/date strings; preserve exact calendar dates from the user or contract. Use redirect_mode if_required unless the user explicitly asks to disable checkout/redirects. If changing an existing/customer contract schedule, inspect the customer first. For schedules, put phase-specific feature quantities and contract feature limits/overrides in plan.customize.items, not feature_quantities; map 'per month/year' to reset.interval month/year. If the user says year 1 is already paid or should have no billing changes, do not add a year-1 phase; start phases at the first future billing change.", - schema: createScheduleMcpSchema, - previewEndpoint: endpointByTool.previewCreateSchedule, - writeToolName: "createSchedule", - }, -]; - -const confirmedWriteConfigs: OperationToolConfig[] = [ - { - id: "attach", - description: - "Attach a plan to a customer. Destructive: preview first; preserve feature_quantities, custom prices/items, reset intervals, discounts, and checkout behavior.", - schema: AttachParamsV1Schema, - endpoint: endpointByTool.attach, - destructive: true, - }, - { - id: "updateSubscription", - description: - "Update a subscription. Destructive: preview first; preserve quantity/custom item changes and reset intervals from the previewed request.", - schema: UpdateSubscriptionV1ParamsSchema, - endpoint: endpointByTool.updateSubscription, - destructive: true, - }, - { - id: "createSchedule", - description: - "Create a multi-phase billing schedule. Destructive: preview first; preserve phase starts_at and redirect_mode values from the previewed request. Use redirect_mode if_required unless the user explicitly asks to disable checkout/redirects. If changing an existing/customer contract schedule, inspect the customer first. For schedules, put phase-specific feature quantities and contract feature limits/overrides in plan.customize.items, not feature_quantities. If year 1 is already paid/no billing changes, do not add a year-1 phase; start at the first future billing change.", - schema: createScheduleMcpSchema, - endpoint: endpointByTool.createSchedule, - destructive: true, - }, -]; - -export const dateToEpochMillisecondsTool = createTool({ - id: "dateToEpochMilliseconds", - description: - "Convert a calendar date or ISO timestamp to UTC epoch milliseconds for API timestamp fields. Date-only values default to midnight UTC; include an explicit offset in the date string when timezone matters.", - inputSchema: z - .object({ - date: z.string(), - }) - .strict(), - execute: async ({ date }) => toEpochMilliseconds(date), -}); - -const toEpochMilliseconds = (date: string) => { - const normalized = /^\d{4}-\d{2}-\d{2}$/.test(date) - ? `${date}T00:00:00.000` - : date; - const hasExplicitZone = /(?:z|[+-]\d{2}:?\d{2})$/i.test(normalized); - const parsed = parseISO(hasExplicitZone ? normalized : `${normalized}Z`); - - if (!isValid(parsed)) throw new Error(`Invalid date: ${date}`); - return parsed.getTime(); -}; - -const callAutumn = async ({ - context, - endpoint, - request, -}: { - context?: ToolContext; - endpoint: string; - request: unknown; -}) => { - const auth = getAutumnAuth(context); - const client = createAutumnClient(auth); - const init: RequestInit = { - method: "POST", - headers: client.headers, - body: JSON.stringify(request), - }; - if (context?.mcp?.extra?.signal) init.signal = context.mcp.extra.signal; - const response = await fetch(new URL(endpoint, client.baseUrl), init); - const text = await response.text(); - const body = text ? parseBody(text) : null; - if (!response.ok) { - throw new Error( - `Autumn API request failed (${response.status}): ${typeof body === "string" ? body : JSON.stringify(body)}`, - ); - } - return body; -}; - -const parseBody = (text: string): unknown => { - try { - return JSON.parse(text); - } catch { - return text; - } -}; -const logTool = (event: string, data: Record) => { - if (process.env.MCP_DEBUG_PENDING_ACTIONS !== "1") return; - console.log(`[mcp:agent-tools] ${event} ${JSON.stringify(data)}`); -}; - -const mcpAnnotations = (destructive = false, idempotent = false) => ({ - readOnlyHint: !destructive && !idempotent, - destructiveHint: destructive, - idempotentHint: idempotent, - openWorldHint: false, -}); - -const toTools = ( - configs: Config[], - create: (config: Config) => ReturnType, -) => Object.fromEntries(configs.map((config) => [config.id, create(config)])); - -const operationTool = ({ - id, - description, - schema, - endpoint, - destructive = false, - idempotent = false, -}: OperationToolConfig) => - createTool({ - id, - description, - inputSchema: z.object({ request: schema }).strict(), - mcp: { - annotations: mcpAnnotations(destructive, idempotent), - }, - execute: (input, context) => - callAutumn({ - context, - endpoint, - request: schema.parse((input as { request: unknown }).request), - }), - }); - -const agentBillingPreviewTool = ({ - id, - description, - schema, - previewEndpoint, - writeToolName, -}: { - id: string; - description: string; - schema: z.ZodType; - previewEndpoint: string; - writeToolName: ConfirmedWriteToolName; -}) => - createTool({ - id, - description: `${description} Store the exact pending billing action for later confirmation.`, - inputSchema: z.object({ request: schema }).strict(), - mcp: { - annotations: mcpAnnotations(), - }, - execute: async (input, context) => { - const request = (input as { request: unknown }).request; - const parsedRequest = schema.parse(request); - const auth = getAutumnAuth(context); - logTool("preview-start", { previewTool: id, writeToolName }); - const preview = await callAutumn({ - context, - endpoint: previewEndpoint, - request: parsedRequest, - }); - await createPendingAction({ - auth, - toolName: writeToolName, - request: parsedRequest, - preview: JSON.stringify(preview), - }); - logTool("preview-stored", { previewTool: id, writeToolName }); - return { - preview, - pending: true, - message: - "Preview ready. Ask the user to explicitly apply or approve this exact change.", - }; - }, - }); - -const rawLocalPreviewTool = ({ - id, - description, - schema, - preview, -}: LocalPreviewToolConfig) => - createTool({ - id, - description, - inputSchema: z.object({ request: schema }).strict(), - mcp: { - annotations: mcpAnnotations(), - }, - execute: async (input) => - preview(schema.parse((input as { request: unknown }).request)), - }); - -const agentLocalPreviewTool = ({ - id, - description, - schema, - writeToolName, - preview, -}: LocalPreviewToolConfig) => - createTool({ - id, - description: `${description} Store the exact pending billing action for later confirmation.`, - inputSchema: z.object({ request: schema }).strict(), - mcp: { - annotations: mcpAnnotations(), - }, - execute: async (input, context) => { - const request = (input as { request: unknown }).request; - const parsedRequest = schema.parse(request); - const previewResult = preview(parsedRequest); - await createPendingAction({ - auth: getAutumnAuth(context), - toolName: writeToolName, - request: parsedRequest, - preview: JSON.stringify(previewResult), - }); - return { - preview: previewResult, - pending: true, - message: - "Preview ready. Ask the user to explicitly apply or approve this exact change.", - }; - }, - }); - -const agentPendingWriteTool = ({ - id, - description, - schema, -}: OperationToolConfig) => - createTool({ - id, - description: `${description} This internal agent tool stores the exact request for later confirmation instead of applying it immediately.`, - inputSchema: z.object({ request: schema }).strict(), - mcp: { - annotations: mcpAnnotations(), - }, - execute: async (input, context) => { - const request = (input as { request: unknown }).request; - const parsedRequest = schema.parse(request); - await createPendingAction({ - auth: getAutumnAuth(context), - toolName: id as ConfirmedWriteToolName, - request: parsedRequest, - preview: JSON.stringify(parsedRequest), - }); - return { - pending: true, - request: parsedRequest, - message: - "Request ready. Ask the user to explicitly apply or approve this exact change.", - }; - }, - }); - -export const createRawAutumnOperationTools = () => ({ - ...toTools(toolConfigs, operationTool), - ...toTools(billingPreviewConfigs, (config) => - operationTool({ ...config, endpoint: config.previewEndpoint }), - ), - ...toTools(localPreviewConfigs, rawLocalPreviewTool), - ...toTools(confirmedWriteConfigs, operationTool), -}); - -export const createAgentAutumnOperationTools = () => ({ - ...toTools( - toolConfigs.filter(({ destructive }) => !destructive), - operationTool, - ), - ...toTools( - toolConfigs.filter(({ destructive }) => destructive), - agentPendingWriteTool, - ), - ...toTools(billingPreviewConfigs, agentBillingPreviewTool), - ...toTools(localPreviewConfigs, agentLocalPreviewTool), - dateToEpochMilliseconds: dateToEpochMillisecondsTool, - confirmBillingAction: createTool({ - id: "confirmBillingAction", - description: - "Apply the latest pending billing action after the user semantically confirms the preview.", - inputSchema: z.object({}).strict(), - execute: async (_input, context) => { - const auth = getAutumnAuth(context); - logTool("confirm-start", { env: auth.env }); - const action = await claimLatestPendingAction(auth); - logTool("confirm-claimed", { toolName: action.toolName }); - const result = await executeConfirmedBillingAction({ - auth, - toolName: action.toolName, - request: action.request, - }); - return { - message: `Confirmed and applied ${action.toolName}.`, - result, - }; - }, - }), -}); - -export const executeConfirmedBillingAction = async ({ - auth, - toolName, - request, -}: { - auth: ReturnType; - toolName: ConfirmedWriteToolName; - request: unknown; -}) => - callAutumn({ - context: { mcp: { extra: { authInfo: auth } } } as never, - endpoint: endpointByTool[toolName], - request: writeSchemaByTool[toolName].parse(request), - }); diff --git a/packages/mcp/src/mcp-server/oauth.ts b/packages/mcp/src/mcp-server/oauth.ts deleted file mode 100644 index 95291fd9c..000000000 --- a/packages/mcp/src/mcp-server/oauth.ts +++ /dev/null @@ -1,333 +0,0 @@ -import { type ScopeString, Scopes } from "@autumn/shared/scopeDefinitions"; -import { ms } from "@autumn/shared/unixUtils"; -import { addMilliseconds, isFuture } from "date-fns"; -import * as z from "zod/v4"; -import type { AutumnMcpAuth } from "./agent/auth.js"; -import { principalFromSecret } from "./agent/auth.js"; -import type { ConsoleLogger } from "./console-logger.js"; -import type { MCPServerFlags } from "./flags.js"; - -export const MCP_OAUTH_SCOPES = [ - Scopes.Customers.Read, - Scopes.Customers.Write, - Scopes.Plans.Read, - Scopes.Plans.Write, - Scopes.Billing.Read, - Scopes.Billing.Write, - Scopes.Balances.Write, - Scopes.Analytics.Read, -] as const satisfies readonly ScopeString[]; - -const environmentSchema = z.enum(["sandbox", "live"]); -const xApiVersionSchema = z.string().default("2.3.0"); -const failOpenSchema = z - .union([ - z.boolean(), - z.enum(["true", "false"]).transform((v) => v === "true"), - ]) - .default(true); -const secretKeySchema = z.string().min(1).optional(); -const tokenExchangeSchema = z.object({ - sandbox_key: z.string().optional(), - prod_key: z.string().optional(), - org_id: z.string().optional(), - user_id: z.string().optional(), - client_id: z.string().optional(), - scopes: z.array(z.string()).optional(), -}); - -export type OAuthEnvironment = z.infer; - -export interface MCPOAuthFlags extends MCPServerFlags { - readonly "oauth-enabled"?: boolean | undefined; - readonly "oauth-environment"?: OAuthEnvironment | undefined; -} - -export class OAuthHttpError extends Error { - constructor( - readonly status: number, - message: string, - readonly error = "invalid_token", - readonly wwwAuthenticate?: string, - ) { - super(message); - } -} - -const apiKeyCache = new Map< - string, - { - key: string; - orgId?: string | undefined; - userId?: string | undefined; - clientId?: string | undefined; - scopes?: string[] | undefined; - expiresAt: Date; - } ->(); - -function trimTrailingSlash(url: string): string { - return url.endsWith("/") ? url.slice(0, -1) : url; -} - -export function getResourceUrl( - headers: Headers, - _flags: MCPOAuthFlags, - resourcePath = "/mcp", -): string { - const host = - headers.get("x-autumn-forwarded-host") ?? - headers.get("x-forwarded-host") ?? - headers.get("host"); - if (!host) { - throw new OAuthHttpError(400, "Missing Host header", "invalid_request"); - } - - const proto = - headers.get("x-autumn-forwarded-proto") ?? - headers.get("x-forwarded-proto") ?? - "http"; - return new URL(resourcePath, `${proto}://${host}`).href; -} - -export function getProtectedResourceMetadataUrl(resourceUrl: string): string { - const url = new URL(resourceUrl); - const path = url.pathname === "/" ? "" : url.pathname; - return new URL(`/.well-known/oauth-protected-resource${path}`, url).href; -} - -function getIssuerUrl(flags: MCPOAuthFlags): string { - return trimTrailingSlash( - new URL("/api/auth", flags["server-url"] ?? "https://api.useautumn.com") - .href, - ); -} - -function getApiKeyUrl(flags: MCPOAuthFlags): string { - return new URL("/cli/api-keys", getIssuerUrl(flags)).href; -} - -function getWWWAuthenticate(resourceUrl: string, error?: string): string { - const params = [ - `resource_metadata="${getProtectedResourceMetadataUrl(resourceUrl)}"`, - ]; - if (error) params.push(`error="${error}"`); - return `Bearer ${params.join(", ")}`; -} - -export function getProtectedResourceMetadata( - headers: Headers, - flags: MCPOAuthFlags, - resourcePath = "/mcp", -) { - const resource = getResourceUrl(headers, flags, resourcePath); - return { - resource, - authorization_servers: [getIssuerUrl(flags)], - scopes_supported: [...MCP_OAUTH_SCOPES], - bearer_methods_supported: ["header"], - resource_name: "Autumn MCP", - }; -} - -export function getAuthorizationServerMetadata(flags: MCPOAuthFlags) { - const issuer = getIssuerUrl(flags); - return { - issuer, - authorization_endpoint: `${issuer}/oauth2/authorize`, - token_endpoint: `${issuer}/oauth2/token`, - registration_endpoint: `${issuer}/oauth2/register`, - revocation_endpoint: `${issuer}/oauth2/revoke`, - introspection_endpoint: `${issuer}/oauth2/introspect`, - response_types_supported: ["code"], - grant_types_supported: ["authorization_code", "refresh_token"], - token_endpoint_auth_methods_supported: [ - "client_secret_post", - "client_secret_basic", - "none", - ], - code_challenge_methods_supported: ["S256"], - scopes_supported: [...MCP_OAUTH_SCOPES], - }; -} - -function getEnvironment( - headers: Headers, - flags: MCPOAuthFlags, -): OAuthEnvironment { - const value = - headers.get("x-autumn-environment") ?? - flags["oauth-environment"] ?? - "sandbox"; - const parsed = environmentSchema.safeParse(value); - if (parsed.success) return parsed.data; - - throw new OAuthHttpError( - 400, - "Invalid x-autumn-environment", - "invalid_request", - ); -} - -function parseRequestOption( - value: unknown, - schema: z.ZodType, - message: string, -): T { - const parsed = schema.safeParse(value); - if (parsed.success) return parsed.data; - - throw new OAuthHttpError(400, message, "invalid_request"); -} - -async function exchangeOAuthToken( - headers: Headers, - flags: MCPOAuthFlags, - resource: string, - token: string, -): Promise<{ - key: string; - orgId?: string | undefined; - userId?: string | undefined; - clientId?: string | undefined; - scopes?: string[]; -}> { - const env = getEnvironment(headers, flags); - const cacheKey = `${token}:${resource}:${env}`; - const cached = apiKeyCache.get(cacheKey); - if (cached && isFuture(cached.expiresAt)) return cached; - - const response = await fetch(getApiKeyUrl(flags), { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ resource, scopes: MCP_OAUTH_SCOPES }), - }); - - if (!response.ok) { - throw new OAuthHttpError( - response.status === 403 ? 403 : 401, - await response.text(), - response.status === 403 ? "insufficient_scope" : "invalid_token", - response.status === 403 - ? undefined - : getWWWAuthenticate(resource, "invalid_token"), - ); - } - - const data = tokenExchangeSchema.parse(await response.json()); - const key = env === "live" ? data.prod_key : data.sandbox_key; - if (!key) { - throw new OAuthHttpError( - 502, - "OAuth key exchange did not return an API key", - ); - } - - const exchanged = { - key, - orgId: data.org_id, - userId: data.user_id, - clientId: data.client_id, - scopes: data.scopes, - expiresAt: addMilliseconds(new Date(), ms.minutes(1)), - }; - apiKeyCache.set(cacheKey, exchanged); - return exchanged; -} - -function getOAuthPrincipalId( - token: string, - exchanged: Awaited>, -) { - if (!exchanged.orgId) return principalFromSecret("oauth", token); - - return [ - "oauth", - exchanged.orgId, - exchanged.userId ?? "unknown-user", - exchanged.clientId ?? "unknown-client", - ].join(":"); -} - -function getStaticApiKey(headers: Headers, flags: MCPOAuthFlags) { - const secretKey = headers.get("secret-key"); - if (secretKey) return secretKey; - - const authorization = headers.get("authorization"); - const bearer = authorization?.startsWith("Bearer ") - ? authorization.slice("Bearer ".length) - : undefined; - if (bearer?.startsWith("am_")) return bearer; - - return flags["oauth-enabled"] ? undefined : flags["secret-key"]; -} - -export async function buildAuthForRequest( - headers: Headers, - flags: MCPOAuthFlags, - logger: ConsoleLogger, - resourcePath = "/mcp", -): Promise { - const env = getEnvironment(headers, flags); - const resource = getResourceUrl(headers, flags, resourcePath); - const xApiVersion = parseRequestOption( - headers.get("x-api-version") ?? flags["x-api-version"], - xApiVersionSchema, - "Invalid x-api-version", - ); - const failOpen = parseRequestOption( - headers.get("fail-open") ?? flags["fail-open"], - failOpenSchema, - "Invalid fail-open", - ); - const apiKey = parseRequestOption( - getStaticApiKey(headers, flags), - secretKeySchema, - "Invalid secret-key", - ); - - if (apiKey) { - return { - apiKey, - env, - resource, - principalId: principalFromSecret("secret-key", apiKey), - scopes: [...MCP_OAUTH_SCOPES], - serverURL: flags["server-url"], - xApiVersion, - failOpen, - }; - } - - if (flags["oauth-enabled"]) { - const authHeader = headers.get("authorization"); - if (!authHeader?.startsWith("Bearer ")) { - throw new OAuthHttpError( - 401, - "Missing Authorization bearer token", - "invalid_token", - getWWWAuthenticate(resource), - ); - } - - const token = authHeader.slice("Bearer ".length); - const exchanged = await exchangeOAuthToken(headers, flags, resource, token); - return { - apiKey: exchanged.key, - env, - resource, - principalId: getOAuthPrincipalId(token, exchanged), - scopes: exchanged.scopes ?? [...MCP_OAUTH_SCOPES], - orgId: exchanged.orgId, - serverURL: flags["server-url"], - xApiVersion, - failOpen, - }; - } - - logger.warning("Missing secret-key for MCP request"); - throw new OAuthHttpError(401, "Missing secret-key", "invalid_token"); -} diff --git a/packages/mcp/src/mcp-server/agent/resources.ts b/packages/mcp/src/resources/index.ts similarity index 91% rename from packages/mcp/src/mcp-server/agent/resources.ts rename to packages/mcp/src/resources/index.ts index 79140f969..46498306c 100644 --- a/packages/mcp/src/mcp-server/agent/resources.ts +++ b/packages/mcp/src/resources/index.ts @@ -1,7 +1,28 @@ import type { MCPServerResources } from "@mastra/mcp"; -const docs = { - "autumn://docs/tool-composition": { +type DocInput = { + name: string; + title: string; + description: string; + text: string; +}; + +/** + * Builds a single Autumn docs resource. The `autumn://docs/` URI is + * derived from `name` so each doc is declared once, with no duplicated key. + */ +const defineDoc = ({ name, title, description, text }: DocInput) => ({ + uri: `autumn://docs/${name}`, + name, + title, + description, + text, +}); + +type Doc = ReturnType; + +const docs: Doc[] = [ + defineDoc({ name: "tool-composition", title: "Tool Composition", description: "How to compose Autumn MCP tools for operational questions.", @@ -21,8 +42,8 @@ Use Autumn tools as composable primitives. - For billing writes, always preview first and wait for explicit user confirmation before applying. Docs index: https://docs.useautumn.com/llms.txt`, - }, - "autumn://docs/querying-plans": { + }), + defineDoc({ name: "querying-plans", title: "Querying Plans", description: "How to answer plan-filtering questions with listPlans.", @@ -39,8 +60,8 @@ Use listPlans for questions about: - plan features and included quantities Filter the returned plans locally. If the user asks for customers on matching plans, first resolve the matching plans, then call listCustomers with those plan ids. For upcoming, queued, or scheduled version queries, pass only the relevant target versions to listCustomers; with numeric versions, exclude the earliest historical version unless the user asks for all historical versions.`, - }, - "autumn://docs/creating-plans": { + }), + defineDoc({ name: "creating-plans", title: "Creating Plans", description: "How to gather plan details before using createPlan.", @@ -59,8 +80,8 @@ Before creating a plan, resolve: For consumable features, recurring grants need reset intervals. "500 credits per month" means included 500 with reset.interval "month"; one-time grants use "one_off". If any required pricing or feature detail is ambiguous, ask a concise clarification question before creating the plan.`, - }, - "autumn://docs/querying-customers": { + }), + defineDoc({ name: "querying-customers", title: "Querying Customers", description: "How to answer customer-heavy questions with listCustomers.", @@ -76,8 +97,8 @@ Prefer server-side filters before local filtering: Use limit 1000 for broad scans; that is the maximum page size. Always paginate until next_cursor is empty when the user asks for complete results. Use getCustomer only for details not returned by listCustomers.`, - }, - "autumn://docs/schedules": { + }), + defineDoc({ name: "schedules", title: "Billing Schedules", description: "How to create multi-phase billing schedules safely.", @@ -101,8 +122,8 @@ Custom feature mapping: - Omit reset only for non-consumable, unlimited, or clearly one-time grants. There is no separate public update-schedule tool. For existing subscription changes, use previewUpdateSubscription and updateSubscription when the requested change fits that endpoint. For a new multi-phase transition, call previewCreateSchedule first, show the immediate billing impact and ordered phases, then call createSchedule only after explicit confirmation.`, - }, - "autumn://docs/balances": { + }), + defineDoc({ name: "balances", title: "Standalone Balances", description: @@ -133,8 +154,8 @@ Useful docs: - https://docs.useautumn.com/documentation/customers/balances - https://docs.useautumn.com/documentation/modelling-pricing/sub-entity-balances - https://docs.useautumn.com/api-reference/balances/createBalance`, - }, - "autumn://docs/billing-safety": { + }), + defineDoc({ name: "billing-safety", title: "Billing Safety", description: "Preview-first rules for Autumn billing changes.", @@ -157,13 +178,15 @@ Useful docs: - https://docs.useautumn.com/api-reference/billing/attach - https://docs.useautumn.com/documentation/concepts/plan-items - https://docs.useautumn.com/documentation/customers/balances`, - }, -} as const; + }), +]; + +const docByUri = new Map(docs.map((doc) => [doc.uri, doc])); export const autumnMcpResources: MCPServerResources = { listResources: async () => - Object.entries(docs).map(([uri, doc]) => ({ - uri, + docs.map((doc) => ({ + uri: doc.uri, name: doc.name, title: doc.title, description: doc.description, @@ -175,13 +198,12 @@ export const autumnMcpResources: MCPServerResources = { }, })), getResourceContent: async ({ uri }) => { - if (!Object.hasOwn(docs, uri)) { + const doc = docByUri.get(uri); + if (!doc) { throw new Error(`Unknown Autumn MCP resource: ${uri}`); } - - const doc = docs[uri as keyof typeof docs]; return { text: doc.text }; }, }; -export const autumnMcpResourceUris = Object.keys(docs); +export const autumnMcpResourceUris = docs.map((doc) => doc.uri); diff --git a/packages/mcp/src/server/auth/auth.ts b/packages/mcp/src/server/auth/auth.ts new file mode 100644 index 000000000..4ed0fa00d --- /dev/null +++ b/packages/mcp/src/server/auth/auth.ts @@ -0,0 +1,78 @@ +import { RequestContext } from "@mastra/core/request-context"; +import * as z from "zod/v4"; +import { + DEFAULT_API_VERSION, + DEFAULT_AUTUMN_API_URL, +} from "../../constants.js"; +import { environmentSchema } from "./utils/schemas.js"; + +/** + * Authenticated Autumn identity attached to every MCP request. Defined as a zod + * schema so the same definition both types the value and validates it when read + * back from the (loosely-typed) MCP execution context — no casts required. + */ +export const autumnMcpAuthSchema = z.object({ + apiKey: z.string().min(1), + env: environmentSchema, + principalId: z.string(), + resource: z.string(), + scopes: z.array(z.string()), + orgId: z.string().optional(), + serverURL: z.string().optional(), + xApiVersion: z.string().optional(), + failOpen: z.boolean().optional(), +}); + +export type AutumnMcpAuth = z.infer; + +/** + * Minimal structural view of the MCP tool execution context we read auth from. + * Kept intentionally loose so any Mastra `ToolExecutionContext` satisfies it + * without callers having to cast. + */ +type AuthContext = { + mcp?: { extra?: { authInfo?: unknown } | undefined } | undefined; + requestContext?: { get?: (key: string) => unknown } | undefined; +}; + +/** Reads `mcp.extra.authInfo` back out of a serialized request context. */ +const readNestedAuthInfo = ( + requestContext: AuthContext["requestContext"], +): unknown => { + const extra = requestContext?.get?.("mcp.extra"); + if (typeof extra === "object" && extra !== null && "authInfo" in extra) { + return extra.authInfo; + } + return undefined; +}; + +export const getAutumnAuth = (context?: AuthContext): AutumnMcpAuth => { + const candidate = + context?.mcp?.extra?.authInfo ?? + readNestedAuthInfo(context?.requestContext); + + const parsed = autumnMcpAuthSchema.safeParse(candidate); + if (!parsed.success) { + throw new Error("Autumn MCP authentication is required."); + } + return parsed.data; +}; + +export const createRequestContext = (auth: AutumnMcpAuth) => { + const requestContext = new RequestContext(); + requestContext.set("mcp.extra", { authInfo: auth }); + return requestContext; +}; + +export const createAutumnClient = (auth: AutumnMcpAuth) => ({ + baseUrl: auth.serverURL ?? DEFAULT_AUTUMN_API_URL, + headers: { + Authorization: `Bearer ${auth.apiKey}`, + "Content-Type": "application/json", + Accept: "application/json", + "x-api-version": auth.xApiVersion ?? DEFAULT_API_VERSION, + ...(auth.failOpen === undefined + ? {} + : { "fail-open": String(auth.failOpen) }), + }, +}); diff --git a/packages/mcp/src/server/auth/oauth.ts b/packages/mcp/src/server/auth/oauth.ts new file mode 100644 index 000000000..974800201 --- /dev/null +++ b/packages/mcp/src/server/auth/oauth.ts @@ -0,0 +1,202 @@ +import { ms } from "@autumn/shared/unixUtils"; +import { addMilliseconds, isFuture } from "date-fns"; +import type { ConsoleLogger } from "../../console-logger.js"; +import { MCP_OAUTH_SCOPES } from "../../constants.js"; +import type { AutumnMcpAuth } from "./auth.js"; +import { OAuthHttpError } from "./utils/errors.js"; +import { getOAuthPrincipalId, principalFromSecret } from "./utils/principal.js"; +import { + getEnvironment, + getStaticApiKey, + parseRequestOption, +} from "./utils/request.js"; +import { + failOpenSchema, + type MCPOAuthFlags, + secretKeySchema, + tokenExchangeSchema, + xApiVersionSchema, +} from "./utils/schemas.js"; +import { + getApiKeyUrl, + getIssuerUrl, + getResourceUrl, + getWWWAuthenticate, +} from "./utils/urls.js"; + +// Public surface consumed via `./oauth.js` (index.ts, leaf, tests). +export { MCP_OAUTH_SCOPES } from "../../constants.js"; +export { OAuthHttpError } from "./utils/errors.js"; +export type { MCPOAuthFlags, OAuthEnvironment } from "./utils/schemas.js"; + +type ExchangedToken = { + key: string; + orgId?: string | undefined; + userId?: string | undefined; + clientId?: string | undefined; + scopes?: string[] | undefined; +}; + +const apiKeyCache = new Map(); + +const exchangeOAuthToken = async ({ + headers, + flags, + resource, + token, +}: { + headers: Headers; + flags: MCPOAuthFlags; + resource: string; + token: string; +}): Promise => { + const env = getEnvironment({ headers, flags }); + const cacheKey = `${token}:${resource}:${env}`; + const cached = apiKeyCache.get(cacheKey); + if (cached && isFuture(cached.expiresAt)) return cached; + + const response = await fetch(getApiKeyUrl(flags), { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ resource, scopes: MCP_OAUTH_SCOPES }), + }); + + if (!response.ok) { + throw new OAuthHttpError( + response.status === 403 ? 403 : 401, + await response.text(), + response.status === 403 ? "insufficient_scope" : "invalid_token", + response.status === 403 + ? undefined + : getWWWAuthenticate({ resourceUrl: resource, error: "invalid_token" }), + ); + } + + const data = tokenExchangeSchema.parse(await response.json()); + const key = env === "live" ? data.prod_key : data.sandbox_key; + if (!key) { + throw new OAuthHttpError( + 502, + "OAuth key exchange did not return an API key", + ); + } + + const exchanged = { + key, + orgId: data.org_id, + userId: data.user_id, + clientId: data.client_id, + scopes: data.scopes, + expiresAt: addMilliseconds(new Date(), ms.minutes(1)), + }; + apiKeyCache.set(cacheKey, exchanged); + return exchanged; +}; + +export const getProtectedResourceMetadata = ( + headers: Headers, + flags: MCPOAuthFlags, + resourcePath = "/mcp", +) => ({ + resource: getResourceUrl({ headers, resourcePath }), + authorization_servers: [getIssuerUrl(flags)], + scopes_supported: [...MCP_OAUTH_SCOPES], + bearer_methods_supported: ["header"], + resource_name: "Autumn MCP", +}); + +export const getAuthorizationServerMetadata = (flags: MCPOAuthFlags) => { + const issuer = getIssuerUrl(flags); + return { + issuer, + authorization_endpoint: `${issuer}/oauth2/authorize`, + token_endpoint: `${issuer}/oauth2/token`, + registration_endpoint: `${issuer}/oauth2/register`, + revocation_endpoint: `${issuer}/oauth2/revoke`, + introspection_endpoint: `${issuer}/oauth2/introspect`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + token_endpoint_auth_methods_supported: [ + "client_secret_post", + "client_secret_basic", + "none", + ], + code_challenge_methods_supported: ["S256"], + scopes_supported: [...MCP_OAUTH_SCOPES], + }; +}; + +export const buildAuthForRequest = async ( + headers: Headers, + flags: MCPOAuthFlags, + logger: ConsoleLogger, + resourcePath = "/mcp", +): Promise => { + const env = getEnvironment({ headers, flags }); + const resource = getResourceUrl({ headers, resourcePath }); + const xApiVersion = parseRequestOption({ + value: headers.get("x-api-version") ?? flags["x-api-version"], + schema: xApiVersionSchema, + message: "Invalid x-api-version", + }); + const failOpen = parseRequestOption({ + value: headers.get("fail-open") ?? flags["fail-open"], + schema: failOpenSchema, + message: "Invalid fail-open", + }); + const apiKey = parseRequestOption({ + value: getStaticApiKey({ headers, flags }), + schema: secretKeySchema, + message: "Invalid secret-key", + }); + + if (apiKey) { + return { + apiKey, + env, + resource, + principalId: principalFromSecret({ kind: "secret-key", value: apiKey }), + scopes: [...MCP_OAUTH_SCOPES], + serverURL: flags["server-url"], + xApiVersion, + failOpen, + }; + } + + if (flags["oauth-enabled"]) { + const authHeader = headers.get("authorization"); + if (!authHeader?.startsWith("Bearer ")) { + throw new OAuthHttpError( + 401, + "Missing Authorization bearer token", + "invalid_token", + getWWWAuthenticate({ resourceUrl: resource }), + ); + } + + const token = authHeader.slice("Bearer ".length); + const exchanged = await exchangeOAuthToken({ + headers, + flags, + resource, + token, + }); + return { + apiKey: exchanged.key, + env, + resource, + principalId: getOAuthPrincipalId({ token, exchanged }), + scopes: exchanged.scopes ?? [...MCP_OAUTH_SCOPES], + orgId: exchanged.orgId, + serverURL: flags["server-url"], + xApiVersion, + failOpen, + }; + } + + logger.warning("Missing secret-key for MCP request"); + throw new OAuthHttpError(401, "Missing secret-key", "invalid_token"); +}; diff --git a/packages/mcp/src/server/auth/utils/errors.ts b/packages/mcp/src/server/auth/utils/errors.ts new file mode 100644 index 000000000..36df3fa02 --- /dev/null +++ b/packages/mcp/src/server/auth/utils/errors.ts @@ -0,0 +1,15 @@ +/** + * Error carrying the HTTP status and OAuth metadata the MCP HTTP layer needs to + * build a spec-compliant `WWW-Authenticate` response. Lives in its own module so + * both the request helpers and the OAuth flow can throw it without import cycles. + */ +export class OAuthHttpError extends Error { + constructor( + readonly status: number, + message: string, + readonly error = "invalid_token", + readonly wwwAuthenticate?: string, + ) { + super(message); + } +} diff --git a/packages/mcp/src/server/auth/utils/principal.ts b/packages/mcp/src/server/auth/utils/principal.ts new file mode 100644 index 000000000..778c34e7b --- /dev/null +++ b/packages/mcp/src/server/auth/utils/principal.ts @@ -0,0 +1,47 @@ +import { createHash } from "node:crypto"; + +/** Short, stable digest used to anonymise secrets inside principal ids. */ +const hash = (value: string) => + createHash("sha256").update(value).digest("hex").slice(0, 32); + +/** + * Builds a principal id from a secret without leaking it, e.g. + * `secret-key:`. + */ +export const principalFromSecret = ({ + kind, + value, +}: { + kind: string; + value: string; +}) => `${kind}:${hash(value)}`; + +type ExchangedIdentity = { + orgId?: string | undefined; + userId?: string | undefined; + clientId?: string | undefined; +}; + +/** + * Derives a principal id for an OAuth session. When the token exchange returned + * an org we build a human-readable `oauth:::` id; otherwise we + * fall back to a hashed token so unidentified callers still group consistently. + */ +export const getOAuthPrincipalId = ({ + token, + exchanged, +}: { + token: string; + exchanged: ExchangedIdentity; +}) => { + if (!exchanged.orgId) { + return principalFromSecret({ kind: "oauth", value: token }); + } + + return [ + "oauth", + exchanged.orgId, + exchanged.userId ?? "unknown-user", + exchanged.clientId ?? "unknown-client", + ].join(":"); +}; diff --git a/packages/mcp/src/server/auth/utils/request.ts b/packages/mcp/src/server/auth/utils/request.ts new file mode 100644 index 000000000..10af5b954 --- /dev/null +++ b/packages/mcp/src/server/auth/utils/request.ts @@ -0,0 +1,67 @@ +import type * as z from "zod/v4"; +import { OAuthHttpError } from "./errors.js"; +import { + environmentSchema, + type MCPOAuthFlags, + type OAuthEnvironment, +} from "./schemas.js"; + +/** + * Validates a request-derived value against a schema, surfacing a 400 with a + * caller-supplied message instead of zod's default error shape. + */ +export const parseRequestOption = ({ + value, + schema, + message, +}: { + value: unknown; + schema: z.ZodType; + message: string; +}): T => { + const parsed = schema.safeParse(value); + if (parsed.success) return parsed.data; + + throw new OAuthHttpError(400, message, "invalid_request"); +}; + +/** Resolves the Autumn environment from the request header, then the flag. */ +export const getEnvironment = ({ + headers, + flags, +}: { + headers: Headers; + flags: MCPOAuthFlags; +}): OAuthEnvironment => + parseRequestOption({ + value: + headers.get("x-autumn-environment") ?? + flags["oauth-environment"] ?? + "sandbox", + schema: environmentSchema, + message: "Invalid x-autumn-environment", + }); + +/** + * Extracts a directly-supplied Autumn secret key (no OAuth exchange): a + * `secret-key` header, an `am_`-prefixed bearer token, or the configured flag + * when OAuth is disabled. + */ +export const getStaticApiKey = ({ + headers, + flags, +}: { + headers: Headers; + flags: MCPOAuthFlags; +}): string | undefined => { + const secretKey = headers.get("secret-key"); + if (secretKey) return secretKey; + + const authorization = headers.get("authorization"); + const bearer = authorization?.startsWith("Bearer ") + ? authorization.slice("Bearer ".length) + : undefined; + if (bearer?.startsWith("am_")) return bearer; + + return flags["oauth-enabled"] ? undefined : flags["secret-key"]; +}; diff --git a/packages/mcp/src/server/auth/utils/schemas.ts b/packages/mcp/src/server/auth/utils/schemas.ts new file mode 100644 index 000000000..add8d13e8 --- /dev/null +++ b/packages/mcp/src/server/auth/utils/schemas.ts @@ -0,0 +1,31 @@ +import * as z from "zod/v4"; +import { DEFAULT_API_VERSION } from "../../../constants.js"; +import type { MCPServerFlags } from "../../flags.js"; + +export const environmentSchema = z.enum(["sandbox", "live"]); +export type OAuthEnvironment = z.infer; + +export const xApiVersionSchema = z.string().default(DEFAULT_API_VERSION); + +export const failOpenSchema = z + .union([ + z.boolean(), + z.enum(["true", "false"]).transform((v) => v === "true"), + ]) + .default(true); + +export const secretKeySchema = z.string().min(1).optional(); + +export const tokenExchangeSchema = z.object({ + sandbox_key: z.string().optional(), + prod_key: z.string().optional(), + org_id: z.string().optional(), + user_id: z.string().optional(), + client_id: z.string().optional(), + scopes: z.array(z.string()).optional(), +}); + +export interface MCPOAuthFlags extends MCPServerFlags { + readonly "oauth-enabled"?: boolean | undefined; + readonly "oauth-environment"?: OAuthEnvironment | undefined; +} diff --git a/packages/mcp/src/server/auth/utils/urls.ts b/packages/mcp/src/server/auth/utils/urls.ts new file mode 100644 index 000000000..7cb4809b9 --- /dev/null +++ b/packages/mcp/src/server/auth/utils/urls.ts @@ -0,0 +1,63 @@ +import { DEFAULT_AUTUMN_API_URL } from "../../../constants.js"; +import { OAuthHttpError } from "./errors.js"; +import type { MCPOAuthFlags } from "./schemas.js"; + +const trimTrailingSlash = (url: string) => + url.endsWith("/") ? url.slice(0, -1) : url; + +/** Host the client reached us on, honouring Autumn's proxy forwarding headers. */ +const getForwardedHost = (headers: Headers) => + headers.get("x-autumn-forwarded-host") ?? + headers.get("x-forwarded-host") ?? + headers.get("host"); + +const getForwardedProto = (headers: Headers) => + headers.get("x-autumn-forwarded-proto") ?? + headers.get("x-forwarded-proto") ?? + "http"; + +/** Absolute URL of the MCP resource the current request is targeting. */ +export const getResourceUrl = ({ + headers, + resourcePath = "/mcp", +}: { + headers: Headers; + resourcePath?: string; +}): string => { + const host = getForwardedHost(headers); + if (!host) { + throw new OAuthHttpError(400, "Missing Host header", "invalid_request"); + } + + return new URL(resourcePath, `${getForwardedProto(headers)}://${host}`).href; +}; + +export const getProtectedResourceMetadataUrl = ( + resourceUrl: string, +): string => { + const url = new URL(resourceUrl); + const path = url.pathname === "/" ? "" : url.pathname; + return new URL(`/.well-known/oauth-protected-resource${path}`, url).href; +}; + +export const getIssuerUrl = (flags: MCPOAuthFlags): string => + trimTrailingSlash( + new URL("/api/auth", flags["server-url"] ?? DEFAULT_AUTUMN_API_URL).href, + ); + +export const getApiKeyUrl = (flags: MCPOAuthFlags): string => + new URL("/cli/api-keys", getIssuerUrl(flags)).href; + +export const getWWWAuthenticate = ({ + resourceUrl, + error, +}: { + resourceUrl: string; + error?: string; +}): string => { + const params = [ + `resource_metadata="${getProtectedResourceMetadataUrl(resourceUrl)}"`, + ]; + if (error) params.push(`error="${error}"`); + return `Bearer ${params.join(", ")}`; +}; diff --git a/packages/mcp/src/mcp-server/flags.ts b/packages/mcp/src/server/flags.ts similarity index 100% rename from packages/mcp/src/mcp-server/flags.ts rename to packages/mcp/src/server/flags.ts diff --git a/packages/mcp/src/server/server.ts b/packages/mcp/src/server/server.ts new file mode 100644 index 000000000..f0ec8e2c3 --- /dev/null +++ b/packages/mcp/src/server/server.ts @@ -0,0 +1,15 @@ +import { MCPServer } from "@mastra/mcp"; +import { autumnMcpResources } from "../resources/index.js"; +import { createRawAutumnOperationTools } from "../tools/index.js"; + +export const createAutumnOperationsMCPServer = () => + new MCPServer({ + id: "autumn-mcp", + name: "Autumn MCP", + version: "0.0.1", + description: "Operate on Autumn customers, plans, and billing.", + instructions: + "Use preview tools before billing writes. Write tools are destructive and should only be called after explicit user confirmation.", + tools: createRawAutumnOperationTools(), + resources: autumnMcpResources, + }); diff --git a/packages/mcp/src/tools/balances.ts b/packages/mcp/src/tools/balances.ts new file mode 100644 index 000000000..85aabe459 --- /dev/null +++ b/packages/mcp/src/tools/balances.ts @@ -0,0 +1,49 @@ +import { CreateBalanceParamsV0Schema } from "@autumn/shared/publicApiSchemas"; +import { createDomainTools } from "./utils/builders.js"; +import { epochMillisecondsSchema } from "./utils/dates.js"; +import type { ToolDomain } from "./utils/types.js"; + +const createBalanceMcpSchema = CreateBalanceParamsV0Schema.extend({ + expires_at: epochMillisecondsSchema.optional().meta({ + description: + "Expiry time as epoch milliseconds or an ISO date string. Date-only values use midnight UTC.", + }), +}); + +const endpoints = { + createBalance: "/v1/balances.create", +} as const; + +const schemas = { + previewCreateBalance: createBalanceMcpSchema, + createBalance: createBalanceMcpSchema, +} as const; + +const { operation, localPreview } = createDomainTools({ endpoints, schemas }); + +const domain = { + operations: [ + operation({ + id: "createBalance", + description: + "Create a standalone customer balance grant. Use when a user asks to give, add, grant, or provision credits/balance to a customer or entity. Destructive: preview first; use entity_id for entity-scoped credits, included_grant for the grant amount, expires_at for expiring grants, and omit reset when using expires_at. For relative expiries like '2 months', use calendar months, not a 30-day approximation. expires_at accepts epoch milliseconds or ISO/date strings.", + destructive: true, + }), + ], + localPreviews: [ + localPreview({ + id: "previewCreateBalance", + description: + "Preview a standalone balance grant before createBalance. Use when a user asks to give, add, grant, or provision credits/balance to a customer or entity. Use for one-time credit grants, referral/promotional credits, and entity-scoped credits. Does not mutate Autumn. For relative expiries like '2 months', use calendar months. expires_at accepts epoch milliseconds or ISO/date strings.", + writeToolName: "createBalance", + preview: (request) => ({ + action: "createBalance", + request, + impact: + "Creates a standalone balance grant. If entity_id is present, the balance is scoped to that entity. If expires_at is present, the grant expires at that timestamp.", + }), + }), + ], +} satisfies ToolDomain; + +export const balances = { endpoints, schemas, domain }; diff --git a/packages/mcp/src/tools/billing.ts b/packages/mcp/src/tools/billing.ts new file mode 100644 index 000000000..e2fd1b3a0 --- /dev/null +++ b/packages/mcp/src/tools/billing.ts @@ -0,0 +1,88 @@ +import { + AttachParamsV1Schema, + CreateScheduleParamsV0Schema, + CreateSchedulePhaseSchema, + UpdateSubscriptionV1ParamsSchema, +} from "@autumn/shared/publicApiSchemas"; +import * as z from "zod/v4"; +import { createDomainTools } from "./utils/builders.js"; +import { epochMillisecondsSchema } from "./utils/dates.js"; +import type { ToolDomain } from "./utils/types.js"; + +const createSchedulePhaseMcpSchema = CreateSchedulePhaseSchema.extend({ + starts_at: epochMillisecondsSchema.meta({ + description: + "Phase start time as epoch milliseconds or an ISO date string. Date-only values use midnight UTC.", + }), +}); + +const createScheduleMcpSchema = CreateScheduleParamsV0Schema.extend({ + phases: z + .tuple([createSchedulePhaseMcpSchema]) + .rest(createSchedulePhaseMcpSchema), +}); + +const endpoints = { + previewAttach: "/v1/billing.preview_attach", + attach: "/v1/billing.attach", + previewUpdateSubscription: "/v1/billing.preview_update", + updateSubscription: "/v1/billing.update", + previewCreateSchedule: "/v1/billing.preview_create_schedule", + createSchedule: "/v1/billing.create_schedule", +} as const; + +const schemas = { + previewAttach: AttachParamsV1Schema, + attach: AttachParamsV1Schema, + previewUpdateSubscription: UpdateSubscriptionV1ParamsSchema, + updateSubscription: UpdateSubscriptionV1ParamsSchema, + previewCreateSchedule: createScheduleMcpSchema, + createSchedule: createScheduleMcpSchema, +} as const; + +const { billingPreview, confirmedWrite } = createDomainTools({ + endpoints, + schemas, +}); + +const domain = { + billingPreviews: [ + billingPreview({ + id: "previewAttach", + description: + "Preview attaching a plan before attach. Include feature_quantities and custom items/prices; map recurring custom grants like 'per month/year' to reset.interval.", + writeToolName: "attach", + }), + billingPreview({ + id: "previewUpdateSubscription", + description: + "Preview updating a subscription before updateSubscription. Include quantity/custom item changes; recurring custom grants need reset.interval.", + writeToolName: "updateSubscription", + }), + billingPreview({ + id: "previewCreateSchedule", + description: + "Preview billing impact of a multi-phase schedule before createSchedule. starts_at accepts epoch milliseconds or ISO/date strings; preserve exact calendar dates from the user or contract. Use redirect_mode if_required unless the user explicitly asks to disable checkout/redirects. If changing an existing/customer contract schedule, inspect the customer first. For schedules, put phase-specific feature quantities and contract feature limits/overrides in plan.customize.items, not feature_quantities; map 'per month/year' to reset.interval month/year. If the user says year 1 is already paid or should have no billing changes, do not add a year-1 phase; start phases at the first future billing change.", + writeToolName: "createSchedule", + }), + ], + confirmedWrites: [ + confirmedWrite({ + id: "attach", + description: + "Attach a plan to a customer. Destructive: preview first; preserve feature_quantities, custom prices/items, reset intervals, discounts, and checkout behavior.", + }), + confirmedWrite({ + id: "updateSubscription", + description: + "Update a subscription. Destructive: preview first; preserve quantity/custom item changes and reset intervals from the previewed request.", + }), + confirmedWrite({ + id: "createSchedule", + description: + "Create a multi-phase billing schedule. Destructive: preview first; preserve phase starts_at and redirect_mode values from the previewed request. Use redirect_mode if_required unless the user explicitly asks to disable checkout/redirects. If changing an existing/customer contract schedule, inspect the customer first. For schedules, put phase-specific feature quantities and contract feature limits/overrides in plan.customize.items, not feature_quantities. If year 1 is already paid/no billing changes, do not add a year-1 phase; start at the first future billing change.", + }), + ], +} satisfies ToolDomain; + +export const billing = { endpoints, schemas, domain }; diff --git a/packages/mcp/src/tools/customers.ts b/packages/mcp/src/tools/customers.ts new file mode 100644 index 000000000..acc26db9d --- /dev/null +++ b/packages/mcp/src/tools/customers.ts @@ -0,0 +1,53 @@ +import { + CreateCustomerParamsV1Schema, + GetCustomerParamsV1Schema, + ListCustomersV2_3ParamsSchema, +} from "@autumn/shared/publicApiSchemas"; +import * as z from "zod/v4"; +import { createDomainTools } from "./utils/builders.js"; +import type { ToolDomain } from "./utils/types.js"; + +const listCustomersSchema = ListCustomersV2_3ParamsSchema.extend({ + limit: z + .preprocess( + (value) => (typeof value === "number" && value > 1000 ? 1000 : value), + z.number().int().positive().max(1000).optional(), + ) + .meta({ description: "Maximum customers per page. Max 1000." }), +}); + +const endpoints = { + listCustomers: "/v1/customers.list", + createCustomer: "/v1/customers.get_or_create", + getCustomer: "/v1/customers.get", +} as const; + +const schemas = { + listCustomers: listCustomersSchema, + createCustomer: CreateCustomerParamsV1Schema, + getCustomer: GetCustomerParamsV1Schema, +} as const; + +const { operation } = createDomainTools({ endpoints, schemas }); + +const domain = { + operations: [ + operation({ + id: "listCustomers", + description: + "List Autumn customers. Use search, plans, subscription_status, and processors filters for customer-heavy queries. limit max is 1000. For queued/upcoming plan version queries, use subscription_status scheduled and omit the earliest matching version unless the user asks for all historical versions (versions 1,2,3 -> filter 2,3). 'live', 'paying', and active subscribers usually mean subscription_status active. When a plan is named, include the plans filter instead of listing broad customer sets. If listPlans returned matching versions, pass only relevant versions in plans[].versions, never guessed versions. For every/all/complete requests, paginate by calling again with start_cursor set to the previous response's next_cursor until next_cursor is empty.", + }), + operation({ + id: "createCustomer", + description: + "Create an Autumn customer, or return the existing customer with the same id. Use when the user explicitly wants a customer record created.", + idempotent: true, + }), + operation({ + id: "getCustomer", + description: "Fetch one Autumn customer by id.", + }), + ], +} satisfies ToolDomain; + +export const customers = { endpoints, schemas, domain }; diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts new file mode 100644 index 000000000..d7b76401f --- /dev/null +++ b/packages/mcp/src/tools/index.ts @@ -0,0 +1,135 @@ +import { createTool } from "@mastra/core/tools"; +import * as z from "zod/v4"; +import { claimLatestPendingAction } from "../agent/pending-actions.js"; +import { instrumentToolsWithAnalytics } from "../analytics/index.js"; +import { type AutumnMcpAuth, getAutumnAuth } from "../server/auth/auth.js"; +import { balances } from "./balances.js"; +import { billing } from "./billing.js"; +import { customers } from "./customers.js"; +import { plans } from "./plans.js"; +import { callAutumn } from "./utils/client.js"; +import { dateToEpochMillisecondsTool } from "./utils/dates.js"; +import { logTool } from "./utils/debug.js"; +import { + agentBillingPreviewTool, + agentLocalPreviewTool, + agentPendingWriteTool, + operationTool, + rawLocalPreviewTool, + toTools, +} from "./utils/factories.js"; +import type { ConfirmedWriteToolName, ToolDomain } from "./utils/types.js"; + +export { dateToEpochMillisecondsTool } from "./utils/dates.js"; + +/** Endpoint each tool calls, keyed by tool id (preview tools use their preview path). */ +export const endpointByTool = { + ...customers.endpoints, + ...plans.endpoints, + ...billing.endpoints, + ...balances.endpoints, +} as const; + +/** Request schema each tool validates against, keyed by tool id. */ +export const schemaByTool = { + ...customers.schemas, + ...plans.schemas, + ...billing.schemas, + ...balances.schemas, +} as const satisfies Record< + keyof typeof endpointByTool | "previewCreateBalance", + z.ZodType +>; + +const domains: ToolDomain[] = [ + customers.domain, + plans.domain, + billing.domain, + balances.domain, +]; +const operations = domains.flatMap((domain) => domain.operations ?? []); +const billingPreviews = domains.flatMap( + (domain) => domain.billingPreviews ?? [], +); +const localPreviews = domains.flatMap((domain) => domain.localPreviews ?? []); +const confirmedWrites = domains.flatMap( + (domain) => domain.confirmedWrites ?? [], +); + +/** + * Public MCP toolset: previews call Autumn's preview endpoints directly and + * writes apply immediately (external clients gate destructive calls themselves). + */ +export const createRawAutumnOperationTools = () => + instrumentToolsWithAnalytics({ + tools: { + ...toTools(operations, operationTool), + ...toTools(billingPreviews, (config) => + operationTool({ ...config, endpoint: config.previewEndpoint }), + ), + ...toTools(localPreviews, rawLocalPreviewTool), + ...toTools(confirmedWrites, operationTool), + }, + surface: "mcp", + }); + +/** Applies a previously-staged billing write after the user confirms it. */ +export const executeConfirmedBillingAction = ({ + auth, + toolName, + request, +}: { + auth: AutumnMcpAuth; + toolName: ConfirmedWriteToolName; + request: unknown; +}) => + callAutumn({ + auth, + endpoint: endpointByTool[toolName], + request: schemaByTool[toolName].parse(request), + }); + +/** + * Agent toolset: destructive operations and billing writes are staged as pending + * actions (preview-first), then applied via `confirmBillingAction` once approved. + */ +const createAgentAutumnOperationToolset = () => ({ + ...toTools( + operations.filter(({ destructive }) => !destructive), + operationTool, + ), + ...toTools( + operations.filter(({ destructive }) => destructive), + agentPendingWriteTool, + ), + ...toTools(billingPreviews, agentBillingPreviewTool), + ...toTools(localPreviews, agentLocalPreviewTool), + dateToEpochMilliseconds: dateToEpochMillisecondsTool, + confirmBillingAction: createTool({ + id: "confirmBillingAction", + description: + "Apply the latest pending billing action after the user semantically confirms the preview.", + inputSchema: z.object({}).strict(), + execute: async (_input, context) => { + const auth = getAutumnAuth(context); + logTool("confirm-start", { env: auth.env }); + const action = await claimLatestPendingAction(auth); + logTool("confirm-claimed", { toolName: action.toolName }); + const result = await executeConfirmedBillingAction({ + auth, + toolName: action.toolName, + request: action.request, + }); + return { + message: `Confirmed and applied ${action.toolName}.`, + result, + }; + }, + }), +}); + +export const createAgentAutumnOperationTools = () => + instrumentToolsWithAnalytics({ + tools: createAgentAutumnOperationToolset(), + surface: "agent", + }); diff --git a/packages/mcp/src/tools/plans.ts b/packages/mcp/src/tools/plans.ts new file mode 100644 index 000000000..1eb37c1bb --- /dev/null +++ b/packages/mcp/src/tools/plans.ts @@ -0,0 +1,43 @@ +import { + CreatePlanParamsV2Schema, + GetPlanParamsV0Schema, + ListPlanParamsSchema, +} from "@autumn/shared/publicApiSchemas"; +import { createDomainTools } from "./utils/builders.js"; +import type { ToolDomain } from "./utils/types.js"; + +const endpoints = { + listPlans: "/v1/plans.list", + createPlan: "/v1/plans.create", + getPlan: "/v1/plans.get", +} as const; + +const schemas = { + listPlans: ListPlanParamsSchema, + createPlan: CreatePlanParamsV2Schema, + getPlan: GetPlanParamsV0Schema, +} as const; + +const { operation } = createDomainTools({ endpoints, schemas }); + +const domain = { + operations: [ + operation({ + id: "listPlans", + description: + "List Autumn plans. This is usually a cheap full scan; filter returned plans locally and use matching id/version pairs before customer queries based on plan attributes.", + }), + operation({ + id: "createPlan", + description: + "Create an Autumn plan. Destructive configuration write: gather plan_id, name, price, features/items, trials, and confirmation before running.", + destructive: true, + }), + operation({ + id: "getPlan", + description: "Fetch one Autumn plan by id and optional version.", + }), + ], +} satisfies ToolDomain; + +export const plans = { endpoints, schemas, domain }; diff --git a/packages/mcp/src/tools/utils/annotations.ts b/packages/mcp/src/tools/utils/annotations.ts new file mode 100644 index 000000000..0abf7af86 --- /dev/null +++ b/packages/mcp/src/tools/utils/annotations.ts @@ -0,0 +1,13 @@ +/** MCP tool hints describing the side effects of a tool call. */ +export const mcpAnnotations = ({ + destructive = false, + idempotent = false, +}: { + destructive?: boolean; + idempotent?: boolean; +} = {}) => ({ + readOnlyHint: !destructive && !idempotent, + destructiveHint: destructive, + idempotentHint: idempotent, + openWorldHint: false, +}); diff --git a/packages/mcp/src/tools/utils/builders.ts b/packages/mcp/src/tools/utils/builders.ts new file mode 100644 index 000000000..07663dbaa --- /dev/null +++ b/packages/mcp/src/tools/utils/builders.ts @@ -0,0 +1,100 @@ +import type * as z from "zod/v4"; +import type { + BillingPreviewToolConfig, + ConfirmedWriteToolName, + LocalPreviewToolConfig, + OperationToolConfig, +} from "./types.js"; + +/** + * Domain-scoped config composers bound to a domain's `endpoints` and `schemas` + * maps. A tool's `id` keys into both maps, so each tool declares its id, + * description, and semantics once — the schema and endpoint are looked up rather + * than repeated. The `id` is type-checked against the relevant map keys. + */ +export const createDomainTools = < + E extends Record, + S extends Record, +>({ + endpoints, + schemas, +}: { + endpoints: E; + schemas: S; +}) => { + type EndpointId = Extract; + type SchemaId = Extract; + + /** A tool that calls its endpoint directly with the parsed request. */ + const operation = ({ + id, + description, + destructive = false, + idempotent = false, + }: { + id: EndpointId; + description: string; + destructive?: boolean; + idempotent?: boolean; + }): OperationToolConfig => ({ + id, + description, + schema: schemas[id], + endpoint: endpoints[id], + destructive, + idempotent, + }); + + /** A preview tool that stages a pending billing write via its preview endpoint. */ + const billingPreview = ({ + id, + description, + writeToolName, + }: { + id: EndpointId; + description: string; + writeToolName: ConfirmedWriteToolName; + }): BillingPreviewToolConfig => ({ + id, + description, + schema: schemas[id], + previewEndpoint: endpoints[id], + writeToolName, + }); + + /** A destructive write applied only after the user confirms a preview. */ + const confirmedWrite = ({ + id, + description, + }: { + id: EndpointId; + description: string; + }): OperationToolConfig => ({ + id, + description, + schema: schemas[id], + endpoint: endpoints[id], + destructive: true, + }); + + /** A preview computed locally (no Autumn call) before a billing write. */ + const localPreview = ({ + id, + description, + writeToolName, + preview, + }: { + id: SchemaId; + description: string; + writeToolName: ConfirmedWriteToolName; + preview: (request: unknown) => unknown; + }): LocalPreviewToolConfig => ({ + id, + description, + schema: schemas[id], + writeToolName, + preview, + }); + + return { operation, billingPreview, confirmedWrite, localPreview }; +}; diff --git a/packages/mcp/src/tools/utils/client.ts b/packages/mcp/src/tools/utils/client.ts new file mode 100644 index 000000000..35f1db178 --- /dev/null +++ b/packages/mcp/src/tools/utils/client.ts @@ -0,0 +1,45 @@ +import { + type AutumnMcpAuth, + createAutumnClient, +} from "../../server/auth/auth.js"; + +const parseBody = (text: string): unknown => { + try { + return JSON.parse(text); + } catch { + return text; + } +}; + +/** POSTs a request to an Autumn endpoint using the caller's resolved auth. */ +export const callAutumn = async ({ + auth, + endpoint, + request, + signal, +}: { + auth: AutumnMcpAuth; + endpoint: string; + request: unknown; + signal?: AbortSignal | undefined; +}) => { + const client = createAutumnClient(auth); + const init: RequestInit = { + method: "POST", + headers: client.headers, + body: JSON.stringify(request), + }; + if (signal) init.signal = signal; + + const response = await fetch(new URL(endpoint, client.baseUrl), init); + const text = await response.text(); + const body = text ? parseBody(text) : null; + if (!response.ok) { + throw new Error( + `Autumn API request failed (${response.status}): ${ + typeof body === "string" ? body : JSON.stringify(body) + }`, + ); + } + return body; +}; diff --git a/packages/mcp/src/tools/utils/dates.ts b/packages/mcp/src/tools/utils/dates.ts new file mode 100644 index 000000000..66671b398 --- /dev/null +++ b/packages/mcp/src/tools/utils/dates.ts @@ -0,0 +1,53 @@ +import { createTool } from "@mastra/core/tools"; +import { isValid, parseISO } from "date-fns"; +import * as z from "zod/v4"; + +/** + * Parses an ISO date/timestamp string to UTC epoch milliseconds. Date-only + * values (`YYYY-MM-DD`) and zone-less timestamps are treated as UTC. Returns + * `null` when the input is not a valid date. + */ +const parseToEpochMilliseconds = (value: string): number | null => { + const normalized = /^\d{4}-\d{2}-\d{2}$/.test(value) + ? `${value}T00:00:00.000` + : value; + const hasExplicitZone = /(?:z|[+-]\d{2}:?\d{2})$/i.test(normalized); + const parsed = parseISO(hasExplicitZone ? normalized : `${normalized}Z`); + return isValid(parsed) ? parsed.getTime() : null; +}; + +/** Accepts epoch milliseconds or an ISO date/timestamp string; outputs epoch ms. */ +export const epochMillisecondsSchema = z + .union([z.number(), z.string()]) + .transform((value, context) => { + if (typeof value === "number") { + if (Number.isFinite(value)) return value; + } else { + const epoch = parseToEpochMilliseconds(value); + if (epoch !== null) return epoch; + } + + context.addIssue({ + code: "custom", + message: "Expected epoch milliseconds or an ISO date/timestamp string.", + }); + return z.NEVER; + }); + +const toEpochMilliseconds = (date: string): number => { + const epoch = parseToEpochMilliseconds(date); + if (epoch === null) throw new Error(`Invalid date: ${date}`); + return epoch; +}; + +export const dateToEpochMillisecondsTool = createTool({ + id: "dateToEpochMilliseconds", + description: + "Convert a calendar date or ISO timestamp to UTC epoch milliseconds for API timestamp fields. Date-only values default to midnight UTC; include an explicit offset in the date string when timezone matters.", + inputSchema: z + .object({ + date: z.string(), + }) + .strict(), + execute: async ({ date }) => toEpochMilliseconds(date), +}); diff --git a/packages/mcp/src/tools/utils/debug.ts b/packages/mcp/src/tools/utils/debug.ts new file mode 100644 index 000000000..9fb358454 --- /dev/null +++ b/packages/mcp/src/tools/utils/debug.ts @@ -0,0 +1,5 @@ +/** Opt-in tracing for the pending-action flow (set MCP_DEBUG_PENDING_ACTIONS=1). */ +export const logTool = (event: string, data: Record) => { + if (process.env.MCP_DEBUG_PENDING_ACTIONS !== "1") return; + console.log(`[mcp:agent-tools] ${event} ${JSON.stringify(data)}`); +}; diff --git a/packages/mcp/src/tools/utils/factories.ts b/packages/mcp/src/tools/utils/factories.ts new file mode 100644 index 000000000..2af613aff --- /dev/null +++ b/packages/mcp/src/tools/utils/factories.ts @@ -0,0 +1,164 @@ +import { createTool } from "@mastra/core/tools"; +import * as z from "zod/v4"; +import { createPendingAction } from "../../agent/pending-actions.js"; +import { getAutumnAuth } from "../../server/auth/auth.js"; +import { mcpAnnotations } from "./annotations.js"; +import { callAutumn } from "./client.js"; +import { logTool } from "./debug.js"; +import { + type BillingPreviewToolConfig, + isConfirmedWriteToolName, + type LocalPreviewToolConfig, + type OperationToolConfig, +} from "./types.js"; + +const PENDING_MESSAGE = + "Preview ready. Ask the user to explicitly apply or approve this exact change."; + +/** Reads the `request` payload out of a tool input without casting. */ +const getRequest = (input: unknown): unknown => + input && typeof input === "object" && "request" in input + ? input.request + : undefined; + +const signalOf = (context: { mcp?: { extra?: { signal?: AbortSignal } } }) => + context?.mcp?.extra?.signal; + +/** Builds a `{ id: tool }` record from a list of configs. */ +export const toTools = ( + configs: Config[], + create: (config: Config) => ReturnType, +) => Object.fromEntries(configs.map((config) => [config.id, create(config)])); + +/** Calls an Autumn endpoint directly with the parsed request. */ +export const operationTool = ({ + id, + description, + schema, + endpoint, + destructive = false, + idempotent = false, +}: OperationToolConfig) => + createTool({ + id, + description, + inputSchema: z.object({ request: schema }).strict(), + mcp: { annotations: mcpAnnotations({ destructive, idempotent }) }, + execute: (input, context) => + callAutumn({ + auth: getAutumnAuth(context), + endpoint, + request: schema.parse(getRequest(input)), + signal: signalOf(context), + }), + }); + +/** Agent variant: previews via Autumn, then stages a pending billing write. */ +export const agentBillingPreviewTool = ({ + id, + description, + schema, + previewEndpoint, + writeToolName, +}: BillingPreviewToolConfig) => + createTool({ + id, + description: `${description} Store the exact pending billing action for later confirmation.`, + inputSchema: z.object({ request: schema }).strict(), + mcp: { annotations: mcpAnnotations() }, + execute: async (input, context) => { + const parsedRequest = schema.parse(getRequest(input)); + const auth = getAutumnAuth(context); + logTool("preview-start", { previewTool: id, writeToolName }); + const preview = await callAutumn({ + auth, + endpoint: previewEndpoint, + request: parsedRequest, + signal: signalOf(context), + }); + await createPendingAction({ + auth, + toolName: writeToolName, + request: parsedRequest, + preview: JSON.stringify(preview), + }); + logTool("preview-stored", { previewTool: id, writeToolName }); + return { preview, pending: true, message: PENDING_MESSAGE }; + }, + }); + +/** Raw variant of a local preview: just returns the computed preview. */ +export const rawLocalPreviewTool = ({ + id, + description, + schema, + preview, +}: LocalPreviewToolConfig) => + createTool({ + id, + description, + inputSchema: z.object({ request: schema }).strict(), + mcp: { annotations: mcpAnnotations() }, + execute: async (input) => preview(schema.parse(getRequest(input))), + }); + +/** Agent variant of a local preview: stages a pending billing write. */ +export const agentLocalPreviewTool = ({ + id, + description, + schema, + writeToolName, + preview, +}: LocalPreviewToolConfig) => + createTool({ + id, + description: `${description} Store the exact pending billing action for later confirmation.`, + inputSchema: z.object({ request: schema }).strict(), + mcp: { annotations: mcpAnnotations() }, + execute: async (input, context) => { + const parsedRequest = schema.parse(getRequest(input)); + const previewResult = preview(parsedRequest); + await createPendingAction({ + auth: getAutumnAuth(context), + toolName: writeToolName, + request: parsedRequest, + preview: JSON.stringify(previewResult), + }); + return { + preview: previewResult, + pending: true, + message: PENDING_MESSAGE, + }; + }, + }); + +/** Agent variant of a destructive operation: stages the request instead of applying it. */ +export const agentPendingWriteTool = ({ + id, + description, + schema, +}: OperationToolConfig) => + createTool({ + id, + description: `${description} This internal agent tool stores the exact request for later confirmation instead of applying it immediately.`, + inputSchema: z.object({ request: schema }).strict(), + mcp: { annotations: mcpAnnotations() }, + execute: async (input, context) => { + if (!isConfirmedWriteToolName(id)) { + throw new Error(`Cannot stage a pending write for tool: ${id}`); + } + const parsedRequest = schema.parse(getRequest(input)); + await createPendingAction({ + auth: getAutumnAuth(context), + toolName: id, + request: parsedRequest, + preview: JSON.stringify(parsedRequest), + }); + return { + pending: true, + request: parsedRequest, + message: + "Request ready. Ask the user to explicitly apply or approve this exact change.", + }; + }, + }); diff --git a/packages/mcp/src/tools/utils/types.ts b/packages/mcp/src/tools/utils/types.ts new file mode 100644 index 000000000..e943f6694 --- /dev/null +++ b/packages/mcp/src/tools/utils/types.ts @@ -0,0 +1,61 @@ +import type * as z from "zod/v4"; + +/** + * Tool names that mutate billing state. These are the only tools that can be + * staged as a pending action and later applied via `confirmBillingAction`. + * Declared as a tuple so the union type and runtime guard stay in sync. + */ +export const CONFIRMED_WRITE_TOOL_NAMES = [ + "attach", + "updateSubscription", + "createPlan", + "createSchedule", + "createBalance", +] as const; + +export type ConfirmedWriteToolName = + (typeof CONFIRMED_WRITE_TOOL_NAMES)[number]; + +export const isConfirmedWriteToolName = ( + id: string, +): id is ConfirmedWriteToolName => + CONFIRMED_WRITE_TOOL_NAMES.some((name) => name === id); + +/** A tool that calls a single Autumn endpoint with the parsed request. */ +export type OperationToolConfig = { + id: string; + description: string; + schema: z.ZodType; + endpoint: string; + destructive?: boolean; + idempotent?: boolean; +}; + +/** A preview tool whose result is staged as a pending billing write. */ +export type BillingPreviewToolConfig = { + id: string; + description: string; + schema: z.ZodType; + previewEndpoint: string; + writeToolName: ConfirmedWriteToolName; +}; + +/** A preview tool computed locally (no Autumn call) before a billing write. */ +export type LocalPreviewToolConfig = { + id: string; + description: string; + schema: z.ZodType; + writeToolName: ConfirmedWriteToolName; + preview: (request: unknown) => unknown; +}; + +/** + * One business domain's tool declarations, grouped by behaviour. The top-level + * `index.ts` composes these into the raw (MCP) and agent toolsets. + */ +export type ToolDomain = { + operations?: OperationToolConfig[]; + billingPreviews?: BillingPreviewToolConfig[]; + localPreviews?: LocalPreviewToolConfig[]; + confirmedWrites?: OperationToolConfig[]; +}; diff --git a/packages/mcp/tests/evals/create-balance-evals.test.ts b/packages/mcp/tests/evals/create-balance-evals.test.ts index ad41fa4f1..b3891321a 100644 --- a/packages/mcp/tests/evals/create-balance-evals.test.ts +++ b/packages/mcp/tests/evals/create-balance-evals.test.ts @@ -45,10 +45,13 @@ test("previews and creates an entity-scoped expiring credit grant", async () => }, }); - await generate([ - "Looking to give entity ent_689d243e2c03da31e0ac90d0 on customer cus_687672c4c0d36fa5679f8c7a 50k credits on the credits feature that expire in 2 months. Can you set that up in Autumn?", - "These should not be permanent credits.", - ], 6); + await generate( + [ + "Looking to give entity ent_689d243e2c03da31e0ac90d0 on customer cus_687672c4c0d36fa5679f8c7a 50k credits on the credits feature that expire in 2 months. Can you set that up in Autumn?", + "These should not be permanent credits.", + ], + 6, + ); expectToolCall(toolCalls, "previewCreateBalance", expectedGrant); expectNoApiCall(api, "createBalance"); diff --git a/packages/mcp/tests/evals/list-customers-evals.test.ts b/packages/mcp/tests/evals/list-customers-evals.test.ts index d542c4c51..367991638 100644 --- a/packages/mcp/tests/evals/list-customers-evals.test.ts +++ b/packages/mcp/tests/evals/list-customers-evals.test.ts @@ -35,14 +35,18 @@ test("lists all matching customers with compound filters and cursor pagination", name: "Acme US", email: "billing@acme.example", processors: { stripe: { id: "cus_stripe_us" } }, - subscriptions: [{ planId: "pro", version: 3, status: "active" }], + subscriptions: [ + { planId: "pro", version: 3, status: "active" }, + ], }, { id: "cus_acme_eu", name: "Acme EU", email: "finance@acme.example", processors: { stripe: { id: "cus_stripe_eu" } }, - subscriptions: [{ planId: "pro", version: 2, status: "active" }], + subscriptions: [ + { planId: "pro", version: 2, status: "active" }, + ], }, ], next_cursor: "cursor_acme_2", @@ -175,7 +179,9 @@ test("resolves plan attributes before listing scheduled Vercel customers", async return ( call.body.subscription_status === "scheduled" && call.body.processors?.includes("vercel") && - Array.from(new Set(versions ?? [])).sort().join(",") === "4,5" + Array.from(new Set(versions ?? [])) + .sort() + .join(",") === "4,5" ); }); expect( diff --git a/packages/mcp/tests/unit/mcp-server/agent/ask-autumn.test.ts b/packages/mcp/tests/unit/mcp-server/agent/ask-autumn.test.ts deleted file mode 100644 index e3639d404..000000000 --- a/packages/mcp/tests/unit/mcp-server/agent/ask-autumn.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { describe, expect, mock, test } from "bun:test"; -import type { AutumnMcpAuth } from "../../../../src/mcp-server/agent/auth.js"; -import { setPendingActionsRedis } from "../../../../src/mcp-server/agent/pending-actions.js"; -import { createTestRedis } from "../../../utils/test-redis.js"; - -const systemPrompts: string[] = []; -let agentConfirms = true; -let agentCalls = 0; - -mock.module("@mastra/core/agent", () => ({ - Agent: class { - private readonly tools: Record; - - constructor(config: { tools: Record }) { - this.tools = config.tools; - } - - async generate( - message: string, - options: { - requestContext: unknown; - context: { content: string }[]; - }, - ) { - agentCalls += 1; - const systemPrompt = options.context[0]?.content ?? ""; - systemPrompts.push(systemPrompt); - const context = { requestContext: options.requestContext }; - if (message.toLowerCase().includes("customers")) { - const result = await this.tools.listCustomers.execute?.( - { request: {} }, - context, - ); - return { text: JSON.stringify(result) }; - } - - if (agentConfirms && systemPrompt.includes("Pending billing action")) { - const result = await this.tools.confirmBillingAction.execute?.( - {}, - context, - ); - return { text: JSON.stringify(result) }; - } - if (systemPrompt.includes("Pending billing action")) { - return { text: "There is no pending billing action to confirm." }; - } - - const result = await this.tools.previewAttach.execute?.( - { request: { customer_id: "cus_1", plan_id: "pro" } }, - context, - ); - return { text: JSON.stringify(result) }; - } - }, -})); - -const { createAskAutumnTool } = await import( - "../../../../src/mcp-server/agent/ask-autumn.js" -); - -const auth: AutumnMcpAuth = { - apiKey: "sk_test", - env: "sandbox", - principalId: "user_1", - resource: "http://localhost:2718/mcp", - scopes: ["billing:read", "billing:write"], - serverURL: "http://localhost:8080", -}; - -const mockFetch = (calls: { url: string; body: unknown }[]) => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async (url, init) => { - const body = JSON.parse(init?.body as string); - calls.push({ url: String(url), body }); - - if (String(url).endsWith("/v1/billing.preview_attach")) { - return Response.json({ total: 50 }); - } - - if (String(url).endsWith("/v1/billing.attach")) { - return Response.json({ applied: true }); - } - - if (String(url).endsWith("/v1/customers.list")) { - return Response.json({ customers: [] }); - } - - return Response.json({ error: "unexpected" }, { status: 500 }); - }) as typeof fetch; - return () => { - globalThis.fetch = originalFetch; - }; -}; - -describe("ask_autumn billing confirmation flow", () => { - test("confirms a pending attach across separate ask_autumn calls", async () => { - setPendingActionsRedis(createTestRedis()); - systemPrompts.length = 0; - agentConfirms = true; - agentCalls = 0; - const calls: { url: string; body: unknown }[] = []; - const restoreFetch = mockFetch(calls); - - try { - const tool = createAskAutumnTool(); - if (!tool.execute) throw new Error("ask_autumn is not executable"); - const context = { mcp: { extra: { authInfo: auth } } } as never; - - const preview = await tool.execute( - { message: "attach pro to cus_1" }, - context, - ); - expect(String(preview)).toContain("Preview ready"); - expect(systemPrompts.at(-1)).not.toContain("Pending billing action"); - expect(calls.map((call) => call.url)).toEqual([ - "http://localhost:8080/v1/billing.preview_attach", - ]); - - const confirm = await tool.execute({ message: "confirm" }, context); - expect(String(confirm)).toContain("Confirmed and applied attach."); - expect(calls).toEqual([ - { - url: "http://localhost:8080/v1/billing.preview_attach", - body: { - customer_id: "cus_1", - plan_id: "pro", - redirect_mode: "if_required", - }, - }, - { - url: "http://localhost:8080/v1/billing.attach", - body: { - customer_id: "cus_1", - plan_id: "pro", - redirect_mode: "if_required", - }, - }, - ]); - } finally { - restoreFetch(); - } - }); - - test("semantic confirmation gets the pending preview context", async () => { - setPendingActionsRedis(createTestRedis()); - systemPrompts.length = 0; - agentConfirms = true; - agentCalls = 0; - const calls: { url: string; body: unknown }[] = []; - const restoreFetch = mockFetch(calls); - - try { - const tool = createAskAutumnTool(); - if (!tool.execute) throw new Error("ask_autumn is not executable"); - const context = { mcp: { extra: { authInfo: auth } } } as never; - - await tool.execute({ message: "attach pro to cus_1" }, context); - expect(agentCalls).toBe(1); - - const confirm = await tool.execute( - { message: "that looks good, go ahead" }, - context, - ); - expect(String(confirm)).toContain("Confirmed and applied attach."); - expect(agentCalls).toBe(2); - expect(systemPrompts.at(-1)).toContain("Pending billing action:"); - expect(systemPrompts.at(-1)).toContain("Preview:"); - expect(systemPrompts.at(-1)).toContain('"total":50'); - expect(calls.map((call) => call.url)).toEqual([ - "http://localhost:8080/v1/billing.preview_attach", - "http://localhost:8080/v1/billing.attach", - ]); - } finally { - restoreFetch(); - } - }); - - test("question-like confirmation text does not bypass the agent", async () => { - setPendingActionsRedis(createTestRedis()); - systemPrompts.length = 0; - agentConfirms = false; - agentCalls = 0; - const calls: { url: string; body: unknown }[] = []; - const restoreFetch = mockFetch(calls); - - try { - const tool = createAskAutumnTool(); - if (!tool.execute) throw new Error("ask_autumn is not executable"); - const context = { mcp: { extra: { authInfo: auth } } } as never; - - await tool.execute({ message: "attach pro to cus_1" }, context); - const response = await tool.execute( - { message: "can you confirm what this changes?" }, - context, - ); - - expect(String(response)).toContain("no pending billing action"); - expect(agentCalls).toBe(2); - expect(systemPrompts.at(-1)).toContain("Pending billing action:"); - expect(calls.map((call) => call.url)).toEqual([ - "http://localhost:8080/v1/billing.preview_attach", - ]); - } finally { - restoreFetch(); - } - }); - - test("read requests continue when pending lookup fails", async () => { - setPendingActionsRedis({ - multi: () => { - throw new Error("unavailable"); - }, - get: async () => { - throw new Error("unavailable"); - }, - getdel: async () => { - throw new Error("unavailable"); - }, - del: async () => undefined, - keys: async () => [], - }); - systemPrompts.length = 0; - agentConfirms = true; - agentCalls = 0; - const calls: { url: string; body: unknown }[] = []; - const restoreFetch = mockFetch(calls); - - try { - const tool = createAskAutumnTool(); - if (!tool.execute) throw new Error("ask_autumn is not executable"); - const context = { mcp: { extra: { authInfo: auth } } } as never; - - const response = await tool.execute({ message: "list customers" }, context); - - expect(String(response)).toContain("customers"); - expect(agentCalls).toBe(1); - expect(calls.map((call) => call.url)).toEqual([ - "http://localhost:8080/v1/customers.list", - ]); - } finally { - restoreFetch(); - } - }); -}); diff --git a/packages/mcp/tests/unit/mcp-server/agent/axiom.test.ts b/packages/mcp/tests/unit/mcp-server/agent/axiom.test.ts index d5c9c2a4c..e94bbed88 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/axiom.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/axiom.test.ts @@ -1,7 +1,10 @@ import { describe, expect, test } from "bun:test"; import { Scopes } from "@autumn/shared/scopeDefinitions"; -import type { AutumnMcpAuth } from "../../../../src/mcp-server/agent/auth.js"; -import { prepareAxiomQuery, resolveAutumnOrgId } from "../../../../src/mcp-server/agent/axiom.js"; +import { + prepareAxiomQuery, + resolveAutumnOrgId, +} from "../../../../src/agent/axiom.js"; +import type { AutumnMcpAuth } from "../../../../src/server/auth/auth.js"; const auth: AutumnMcpAuth & { orgId: string } = { apiKey: "sk_test", diff --git a/packages/mcp/tests/unit/mcp-server/agent/pending-actions.test.ts b/packages/mcp/tests/unit/mcp-server/agent/pending-actions.test.ts index 2b1ccbf27..b872a0263 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/pending-actions.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/pending-actions.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from "bun:test"; -import type { AutumnMcpAuth } from "../../../../src/mcp-server/agent/auth.js"; import { claimLatestPendingAction, clearPendingActions, createPendingAction, setPendingActionsRedis, -} from "../../../../src/mcp-server/agent/pending-actions.js"; +} from "../../../../src/agent/pending-actions.js"; +import type { AutumnMcpAuth } from "../../../../src/server/auth/auth.js"; import { createTestRedis } from "../../../utils/test-redis.js"; setPendingActionsRedis(createTestRedis()); @@ -38,7 +38,9 @@ describe("pending billing actions", () => { plan_id: "pro", }, }); - await expect(claimLatestPendingAction(auth())).rejects.toThrow("No pending"); + await expect(claimLatestPendingAction(auth())).rejects.toThrow( + "No pending", + ); }); test("claims the latest matching action without exposing tokens", async () => { @@ -75,11 +77,11 @@ describe("pending billing actions", () => { claimLatestPendingAction(auth()), ]); - expect(results.filter((result) => result.status === "fulfilled")).toHaveLength( - 1, - ); - expect(results.filter((result) => result.status === "rejected")).toHaveLength( - 1, - ); + expect( + results.filter((result) => result.status === "fulfilled"), + ).toHaveLength(1); + expect( + results.filter((result) => result.status === "rejected"), + ).toHaveLength(1); }); }); diff --git a/packages/mcp/tests/unit/mcp-server/agent/server.test.ts b/packages/mcp/tests/unit/mcp-server/agent/server.test.ts index 996ae2c1b..b7b0d9245 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/server.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/server.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { - createAskAutumnMCPServer, - createAutumnOperationsMCPServer, -} from "../../../../src/mcp-server/agent/server.js"; -import { autumnMcpResourceUris } from "../../../../src/mcp-server/agent/resources.js"; +import { autumnMcpResourceUris } from "../../../../src/resources/index.js"; +import { createAutumnOperationsMCPServer } from "../../../../src/server/server.js"; describe("Autumn MCP server", () => { test("public server advertises raw operation tools", async () => { @@ -15,8 +12,8 @@ describe("Autumn MCP server", () => { "getCustomer", "listPlans", "createPlan", - "createBalance", "getPlan", + "createBalance", "previewAttach", "previewUpdateSubscription", "previewCreateSchedule", @@ -31,14 +28,6 @@ describe("Autumn MCP server", () => { ); }); - test("internal server advertises only ask_autumn", async () => { - const tools = await createAskAutumnMCPServer().getToolListInfo(); - - expect(tools.tools.map((tool) => tool.name)).toEqual(["ask_autumn"]); - expect(tools.tools.map((tool) => tool.name)).not.toContain("attach"); - expect(tools.tools.map((tool) => tool.name)).not.toContain("listCustomers"); - }); - test("billing tool schemas avoid legacy JSON Schema ids", async () => { const tools = await createAutumnOperationsMCPServer().getToolListInfo(); @@ -53,11 +42,8 @@ describe("Autumn MCP server", () => { } }); - test.each([ - ["public", createAutumnOperationsMCPServer], - ["internal", createAskAutumnMCPServer], - ])("%s server exposes Autumn composition docs", async (_name, createServer) => { - const server = createServer(); + test("public server exposes Autumn composition docs", async () => { + const server = createAutumnOperationsMCPServer(); const resources = await server.listResources(); expect(resources.resources.map((resource) => resource.uri)).toEqual( diff --git a/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts b/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts index 452ddd0df..07bf3f28e 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts @@ -1,17 +1,17 @@ import { describe, expect, test } from "bun:test"; -import type { AutumnMcpAuth } from "../../../../src/mcp-server/agent/auth.js"; import { - clearPendingActions, claimLatestPendingAction, + clearPendingActions, createPendingAction, setPendingActionsRedis, -} from "../../../../src/mcp-server/agent/pending-actions.js"; -import { createTestRedis } from "../../../utils/test-redis.js"; +} from "../../../../src/agent/pending-actions.js"; +import type { AutumnMcpAuth } from "../../../../src/server/auth/auth.js"; import { createAgentAutumnOperationTools, createRawAutumnOperationTools, dateToEpochMillisecondsTool, -} from "../../../../src/mcp-server/agent/tools.js"; +} from "../../../../src/tools/index.js"; +import { createTestRedis } from "../../../utils/test-redis.js"; setPendingActionsRedis(createTestRedis()); @@ -33,7 +33,9 @@ describe("Autumn operation tools", () => { const tools = createRawAutumnOperationTools(); expect(tools.listPlans.description).toContain("cheap full scan"); - expect(tools.listPlans.description).toContain("filter returned plans locally"); + expect(tools.listPlans.description).toContain( + "filter returned plans locally", + ); expect(tools.listCustomers.description).toContain("plans"); expect(tools.listCustomers.description).toContain("paginate"); expect(tools.createPlan.description).toContain("confirmation"); @@ -72,7 +74,8 @@ describe("Autumn operation tools", () => { test("dateToEpochMilliseconds converts UTC dates and offsets", async () => { const tool = dateToEpochMillisecondsTool as ExecutableTool; - if (!tool.execute) throw new Error("dateToEpochMilliseconds is not executable"); + if (!tool.execute) + throw new Error("dateToEpochMilliseconds is not executable"); await expect(tool.execute({ date: "2027-01-01" }, {})).resolves.toBe( Date.UTC(2027, 0, 1), @@ -85,7 +88,9 @@ describe("Autumn operation tools", () => { test("raw createCustomer calls the get-or-create endpoint", async () => { const originalFetch = globalThis.fetch; globalThis.fetch = (async (url, init) => { - expect(String(url)).toBe("http://localhost:8080/v1/customers.get_or_create"); + expect(String(url)).toBe( + "http://localhost:8080/v1/customers.get_or_create", + ); expect(JSON.parse(init?.body as string)).toMatchObject({ customer_id: "cus_1", email: "charlie@example.com", @@ -124,10 +129,9 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("createPlan is not executable"); await expect( - tool.execute( - { request: { plan_id: "pro", name: "Pro" } }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request: { plan_id: "pro", name: "Pro" } }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toEqual({ id: "pro" }); } finally { globalThis.fetch = originalFetch; @@ -137,7 +141,9 @@ describe("Autumn operation tools", () => { test("raw createSchedule calls the create schedule endpoint", async () => { const originalFetch = globalThis.fetch; globalThis.fetch = (async (url, init) => { - expect(String(url)).toBe("http://localhost:8080/v1/billing.create_schedule"); + expect(String(url)).toBe( + "http://localhost:8080/v1/billing.create_schedule", + ); expect(JSON.parse(init?.body as string)).toMatchObject({ customer_id: "cus_1", }); @@ -153,9 +159,7 @@ describe("Autumn operation tools", () => { { request: { customer_id: "cus_1", - phases: [ - { starts_at: Date.now(), plans: [{ plan_id: "pro" }] }, - ], + phases: [{ starts_at: Date.now(), plans: [{ plan_id: "pro" }] }], }, }, { mcp: { extra: { authInfo: auth } } } as never, @@ -181,13 +185,13 @@ describe("Autumn operation tools", () => { try { const tool = createRawAutumnOperationTools().previewCreateBalance; - if (!tool.execute) throw new Error("previewCreateBalance is not executable"); + if (!tool.execute) + throw new Error("previewCreateBalance is not executable"); await expect( - tool.execute( - { request }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toMatchObject({ action: "createBalance", request, @@ -257,9 +261,7 @@ describe("Autumn operation tools", () => { { request: { customer_id: "cus_1", - phases: [ - { starts_at: Date.now(), plans: [{ plan_id: "pro" }] }, - ], + phases: [{ starts_at: Date.now(), plans: [{ plan_id: "pro" }] }], }, }, { mcp: { extra: { authInfo: auth } } } as never, @@ -286,10 +288,9 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("listCustomers is not executable"); await expect( - tool.execute( - { request: { limit: 5000, search: "charlie" } }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request: { limit: 5000, search: "charlie" } }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toEqual({ customers: [] }); } finally { globalThis.fetch = originalFetch; @@ -300,7 +301,9 @@ describe("Autumn operation tools", () => { await clearPendingActions(); const originalFetch = globalThis.fetch; globalThis.fetch = (async (url, init) => { - expect(String(url)).toBe("http://localhost:8080/v1/billing.preview_attach"); + expect(String(url)).toBe( + "http://localhost:8080/v1/billing.preview_attach", + ); expect(JSON.parse(init?.body as string)).toEqual({ customer_id: "cus_1", plan_id: "pro", @@ -314,12 +317,13 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("previewAttach is not executable"); await expect( - tool.execute( - { request: { customer_id: "cus_1", plan_id: "pro" } }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request: { customer_id: "cus_1", plan_id: "pro" } }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toEqual({ total: 50 }); - await expect(claimLatestPendingAction(auth)).rejects.toThrow("No pending"); + await expect(claimLatestPendingAction(auth)).rejects.toThrow( + "No pending", + ); } finally { globalThis.fetch = originalFetch; } @@ -342,10 +346,9 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("attach is not executable"); await expect( - tool.execute( - { request: { customer_id: "cus_1", plan_id: "pro" } }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request: { customer_id: "cus_1", plan_id: "pro" } }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toEqual({ ok: true }); } finally { globalThis.fetch = originalFetch; @@ -356,7 +359,9 @@ describe("Autumn operation tools", () => { await clearPendingActions(); const originalFetch = globalThis.fetch; globalThis.fetch = (async (url, init) => { - expect(String(url)).toBe("http://localhost:8080/v1/billing.preview_attach"); + expect(String(url)).toBe( + "http://localhost:8080/v1/billing.preview_attach", + ); expect(JSON.parse(init?.body as string)).toEqual({ customer_id: "cus_1", plan_id: "pro", @@ -376,10 +381,9 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("previewAttach is not executable"); await expect( - tool.execute( - { request: { customer_id: "cus_1", plan_id: "pro" } }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request: { customer_id: "cus_1", plan_id: "pro" } }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toMatchObject({ pending: true, preview: { total: 50 } }); await expect(claimLatestPendingAction(auth)).resolves.toMatchObject({ @@ -411,10 +415,9 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("createPlan is not executable"); await expect( - tool.execute( - { request: { plan_id: "pro", name: "Pro" } }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request: { plan_id: "pro", name: "Pro" } }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toMatchObject({ pending: true }); await expect(claimLatestPendingAction(auth)).resolves.toMatchObject({ @@ -451,10 +454,9 @@ describe("Autumn operation tools", () => { } await expect( - tool.execute( - { request }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toMatchObject({ pending: true }); await expect(claimLatestPendingAction(auth)).resolves.toMatchObject({ @@ -491,10 +493,9 @@ describe("Autumn operation tools", () => { } await expect( - tool.execute( - { request }, - { mcp: { extra: { authInfo: auth } } } as never, - ), + tool.execute({ request }, { + mcp: { extra: { authInfo: auth } }, + } as never), ).resolves.toMatchObject({ pending: true }); await expect(claimLatestPendingAction(auth)).resolves.toMatchObject({ @@ -527,7 +528,8 @@ describe("Autumn operation tools", () => { try { const tool = createAgentAutumnOperationTools().confirmBillingAction; - if (!tool.execute) throw new Error("confirmBillingAction is not executable"); + if (!tool.execute) + throw new Error("confirmBillingAction is not executable"); await expect( tool.execute({}, { mcp: { extra: { authInfo: auth } } } as never), @@ -535,7 +537,9 @@ describe("Autumn operation tools", () => { message: "Confirmed and applied attach.", result: { ok: true }, }); - await expect(claimLatestPendingAction(auth)).rejects.toThrow("No pending"); + await expect(claimLatestPendingAction(auth)).rejects.toThrow( + "No pending", + ); } finally { globalThis.fetch = originalFetch; } @@ -565,7 +569,8 @@ describe("Autumn operation tools", () => { try { const tool = createAgentAutumnOperationTools().confirmBillingAction; - if (!tool.execute) throw new Error("confirmBillingAction is not executable"); + if (!tool.execute) + throw new Error("confirmBillingAction is not executable"); await expect( tool.execute({}, { mcp: { extra: { authInfo: auth } } } as never), diff --git a/packages/mcp/tests/unit/mcp-server/oauth.test.ts b/packages/mcp/tests/unit/mcp-server/oauth.test.ts index 9f24b9e03..b29878dfc 100644 --- a/packages/mcp/tests/unit/mcp-server/oauth.test.ts +++ b/packages/mcp/tests/unit/mcp-server/oauth.test.ts @@ -1,12 +1,12 @@ -import { Scopes } from "@autumn/shared/scopeDefinitions"; import { describe, expect, test } from "bun:test"; +import { Scopes } from "@autumn/shared/scopeDefinitions"; import { buildAuthForRequest, getProtectedResourceMetadata, MCP_OAUTH_SCOPES, - OAuthHttpError, type MCPOAuthFlags, -} from "../../../src/mcp-server/oauth.js"; + type OAuthHttpError, +} from "../../../src/server/auth/oauth.js"; const flags = { "oauth-enabled": true, diff --git a/packages/mcp/tests/utils/eval-test-utils.ts b/packages/mcp/tests/utils/eval-test-utils.ts index 9436668fb..571c5f866 100644 --- a/packages/mcp/tests/utils/eval-test-utils.ts +++ b/packages/mcp/tests/utils/eval-test-utils.ts @@ -1,5 +1,5 @@ -import { createServer, type IncomingMessage, type Server } from "node:http"; import { afterEach, expect } from "bun:test"; +import { createServer, type IncomingMessage, type Server } from "node:http"; import { Agent } from "@mastra/core/agent"; import type { MessageListItem } from "@mastra/core/agent/message-list"; import { Mastra } from "@mastra/core/mastra"; @@ -9,12 +9,9 @@ import type * as z from "zod/v4"; import { type AutumnMcpAuth, createRequestContext, -} from "../../src/mcp-server/agent/auth.js"; -import { createAutumnOperationsMCPServer } from "../../src/mcp-server/agent/server.js"; -import { - endpointByTool, - schemaByTool, -} from "../../src/mcp-server/agent/tools.js"; +} from "../../src/server/auth/auth.js"; +import { createAutumnOperationsMCPServer } from "../../src/server/server.js"; +import { endpointByTool, schemaByTool } from "../../src/tools/index.js"; type ToolName = keyof typeof schemaByTool; type EndpointToolName = keyof typeof endpointByTool; diff --git a/packages/mcp/tests/utils/test-redis.ts b/packages/mcp/tests/utils/test-redis.ts index 6d69c3e80..a97f52c61 100644 --- a/packages/mcp/tests/utils/test-redis.ts +++ b/packages/mcp/tests/utils/test-redis.ts @@ -1,7 +1,7 @@ import type { PendingActionRedis, PendingActionRedisMulti, -} from "../../src/mcp-server/agent/pending-actions.js"; +} from "../../src/agent/pending-actions.js"; export const createTestRedis = (): PendingActionRedis => { const store = new Map(); diff --git a/scripts/axiom/cli.ts b/scripts/axiom/cli.ts new file mode 100644 index 000000000..4e274a1f0 --- /dev/null +++ b/scripts/axiom/cli.ts @@ -0,0 +1,45 @@ +/** + * Axiom provisioning CLI. Secrets (AXIOM_ADMIN_TOKEN) are injected by infisical + * via the package.json scripts: + * + * bun axiom # dev (infisical --env=dev) + * bun axiom:prod # prod (infisical --env=prod) + * + * Add a new action by registering it in the `actions` map below. + */ +import "dotenv/config"; +import { createLeafDataset } from "./createLeafDataset.js"; + +const actions = { + "create-leaf": createLeafDataset, +} satisfies Record Promise>; + +type Action = keyof typeof actions; + +const isAction = (value: string | undefined): value is Action => + value !== undefined && Object.hasOwn(actions, value); + +const usage = () => + [ + "Usage: bun axiom (or bun axiom:prod )", + "", + "Actions:", + ...Object.keys(actions).map((action) => ` - ${action}`), + ].join("\n"); + +const main = async () => { + const action = process.argv[2]; + if (!isAction(action)) { + console.error(usage()); + process.exit(1); + } + + try { + await actions[action](); + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); + } +}; + +await main(); diff --git a/scripts/axiom/createLeafDataset.ts b/scripts/axiom/createLeafDataset.ts new file mode 100644 index 000000000..f5f88ab4c --- /dev/null +++ b/scripts/axiom/createLeafDataset.ts @@ -0,0 +1,105 @@ +/** + * Idempotently provisions the Axiom `leaf` dataset used for MCP usage + * analytics (events emitted from packages/mcp `tool.execute`), and configures + * its map fields. + * + * Map fields ("vacuum" the unpredictable nested payloads into a single column): + * MCP tool `input`/`output` payloads have an open-ended shape — every distinct + * arg key would otherwise become its own mapped field and quickly blow Axiom's + * per-dataset field limit. Declaring `input` and `output` as map fields stores + * their nested keys inside one field each, so they never count toward the limit + * while staying queryable (e.g. `where input.customer_id == '...'`). + * + * Run via the Axiom CLI (resolves AXIOM_ADMIN_TOKEN from infisical): + * bun axiom create-leaf # dev + * bun axiom:prod create-leaf # prod + * + * Notes: + * - AXIOM_ADMIN_TOKEN must be a personal API token with dataset create/update + * scope, NOT the `xaat-` ingest token used at runtime. + * - Safe to re-run: dataset creation tolerates "already exists", and the map + * field list is declared via PUT (full replace), so re-running converges. + */ + +const AXIOM_BASE = "https://api.axiom.co/v2"; +const DATASET = "leaf"; +const DATASET_DESCRIPTION = "Leaf app MCP usage analytics (per tool.execute)"; + +// Nested, open-ended payloads stored as map fields to stay under the field +// limit. Keep this list minimal — only genuinely high-cardinality objects. +const MAP_FIELDS = ["input", "output"]; + +const authHeaders = (token: string) => ({ + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", +}); + +const createDataset = async (token: string) => { + const res = await fetch(`${AXIOM_BASE}/datasets`, { + method: "POST", + headers: authHeaders(token), + body: JSON.stringify({ + name: DATASET, + description: DATASET_DESCRIPTION, + }), + }); + + if (res.ok) { + console.log(` + created dataset \`${DATASET}\``); + return; + } + + // 409 (or a 400 mentioning existence) means it's already there — fine. + const text = await res.text(); + if (res.status === 409 || /exist/i.test(text)) { + console.log(` = dataset \`${DATASET}\` already exists`); + return; + } + + throw new Error(`Failed to create dataset: ${res.status} ${text}`); +}; + +const setMapField = async (token: string, name: string) => { + const res = await fetch( + `${AXIOM_BASE}/datasets/${encodeURIComponent(DATASET)}/mapfields`, + { + method: "POST", + headers: authHeaders(token), + body: JSON.stringify({ name }), + }, + ); + + // Re-declaring an existing map field returns a 4xx mentioning existence. + const text = await res.text(); + if (res.ok) { + console.log(` + map field: ${name}`); + return; + } + if (/exist/i.test(text)) { + console.log(` = map field: ${name} (already set)`); + return; + } + + throw new Error(`Failed to set map field "${name}": ${res.status} ${text}`); +}; + +const setMapFields = async (token: string) => { + for (const name of MAP_FIELDS) { + await setMapField(token, name); + } +}; + +/** Provisions the `leaf` dataset and its map fields. */ +export const createLeafDataset = async () => { + const token = process.env.AXIOM_ADMIN_TOKEN; + if (!token) { + throw new Error( + "AXIOM_ADMIN_TOKEN env var is required (personal API token, not xaat-* ingest token)", + ); + } + + console.log(`Provisioning Axiom dataset \`${DATASET}\`...`); + await createDataset(token); + await setMapFields(token); + console.log("\nDone."); +}; diff --git a/scripts/axiom/setOtelVirtualFields.ts b/scripts/axiom/setOtelVirtualFields.ts index 051a4ed5e..8c4334f7e 100644 --- a/scripts/axiom/setOtelVirtualFields.ts +++ b/scripts/axiom/setOtelVirtualFields.ts @@ -4,10 +4,10 @@ * (`req.url`, `context.org_slug`, `statusCode`, etc.). * * Usage: - * AXIOM_API_TOKEN= bun scripts/axiom/setOtelVirtualFields.ts + * AXIOM_ADMIN_TOKEN= bun scripts/axiom/setOtelVirtualFields.ts * * Notes: - * - AXIOM_API_TOKEN must be a personal API token with dataset-write scope, + * - AXIOM_ADMIN_TOKEN must be a personal API token with dataset-write scope, * NOT the `xaat-` ingest token used by the server. * - Safe to re-run; existing fields with matching names are updated in place. */ @@ -149,10 +149,10 @@ type ExistingVField = { dataset: string; }; -const token = process.env.AXIOM_API_TOKEN; +const token = process.env.AXIOM_ADMIN_TOKEN; if (!token) { console.error( - "AXIOM_API_TOKEN env var is required (personal API token, not xaat-* ingest token)", + "AXIOM_ADMIN_TOKEN env var is required (personal API token, not xaat-* ingest token)", ); process.exit(1); } diff --git a/server/src/initHono.ts b/server/src/initHono.ts index eaefbfd41..c3b29ea36 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -24,7 +24,6 @@ import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/han import { apiRouter } from "./routers/apiRouter.js"; import { createChatProxyRouter } from "./routers/chatProxyRouter.js"; import { internalRouter } from "./routers/internalRouter.js"; -import { mcpProxyRouter } from "./routers/mcpProxyRouter.js"; import { publicRouter } from "./routers/publicRouter.js"; import { auth } from "./utils/auth.js"; import { isAllowedOrigin } from "./utils/corsOrigins.js"; @@ -101,8 +100,6 @@ export const createHonoApp = () => { app.get("/ready/:token", handleReadyCheck); app.get("/", handleHealthCheck); - app.route("", mcpProxyRouter); - // Step 1: OTel HTTP span + base middleware + span enrichment app.use( "*", diff --git a/server/src/routers/mcpProxyRouter.ts b/server/src/routers/mcpProxyRouter.ts deleted file mode 100644 index bffcecc49..000000000 --- a/server/src/routers/mcpProxyRouter.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Hono } from "hono"; -import type { Context } from "hono"; -import type { HonoEnv } from "../honoUtils/HonoEnv.js"; - -const hopByHopHeaders = [ - "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "te", - "trailer", - "transfer-encoding", - "upgrade", -]; - -const getMcpUpstream = () => { - const upstream = process.env.MCP_UPSTREAM_URL; - if (!upstream) return null; - - try { - return new URL(upstream); - } catch { - return null; - } -}; - -const proxyMcp = async (c: Context) => { - const upstream = getMcpUpstream(); - if (!upstream) { - return c.json({ error: "MCP upstream not configured" }, 503); - } - - const incomingUrl = new URL(c.req.url); - const targetUrl = new URL(incomingUrl.pathname + incomingUrl.search, upstream); - const headers = new Headers(c.req.raw.headers); - const forwardedHost = - headers.get("x-forwarded-host") ?? headers.get("host") ?? incomingUrl.host; - const forwardedProto = - headers.get("x-forwarded-proto") ?? incomingUrl.protocol.replace(":", ""); - - for (const header of hopByHopHeaders) headers.delete(header); - - headers.delete("host"); - headers.set("x-autumn-forwarded-host", forwardedHost); - headers.set("x-autumn-forwarded-proto", forwardedProto); - headers.set("x-forwarded-host", forwardedHost); - headers.set("x-forwarded-proto", forwardedProto); - - const hasBody = c.req.method !== "GET" && c.req.method !== "HEAD"; - const response = await fetch(targetUrl, { - method: c.req.method, - headers, - body: hasBody ? c.req.raw.body : undefined, - duplex: hasBody ? "half" : undefined, - } as RequestInit & { duplex?: "half" }); - - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); -}; - -export const mcpProxyRouter = new Hono(); - -mcpProxyRouter.all("/mcp", proxyMcp); -mcpProxyRouter.all("/mcp/*", proxyMcp); -mcpProxyRouter.all("/internal/mcp", proxyMcp); -mcpProxyRouter.all("/internal/mcp/*", proxyMcp); -mcpProxyRouter.all("/.well-known/oauth-protected-resource/mcp", proxyMcp); -mcpProxyRouter.all("/.well-known/oauth-protected-resource/internal/mcp", proxyMcp); diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 2a8f7a97f..c4a51e66f 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -91,7 +91,7 @@ const chatServerUrl = process.env.CHAT_SERVER_URL ?? (isProductionAuth ? "https://chat.useautumn.com" : "http://localhost:3099"); -const mcpResourcePaths = ["/mcp", "/internal/mcp"]; +const mcpResourcePaths = ["/mcp"]; const mcpResourceBases = [ process.env.BETTER_AUTH_URL, mcpServerUrl, diff --git a/shared/api/billing/createSchedule/createScheduleParamsV0.ts b/shared/api/billing/createSchedule/createScheduleParamsV0.ts index 9258870fe..b47caa563 100644 --- a/shared/api/billing/createSchedule/createScheduleParamsV0.ts +++ b/shared/api/billing/createSchedule/createScheduleParamsV0.ts @@ -4,9 +4,9 @@ import { RedirectModeSchema } from "@api/billing/common/redirectMode"; import { BasePriceParamsSchema } from "@api/products/components/basePrice/basePrice"; import { CreatePlanItemParamsV1Schema } from "@api/products/items/crud/createPlanItemParamsV1"; import { z } from "zod/v4"; +import { AttachDiscountSchema } from "../attachV2/attachDiscount"; import { BillingBehaviorSchema } from "../common/billingBehavior"; import { BillingCycleAnchorSchema } from "../common/billingCycleAnchor"; -import { AttachDiscountSchema } from "../attachV2/attachDiscount"; const CreateScheduleCustomizePlanSchema = z .object({ @@ -27,26 +27,25 @@ const CreateScheduleCustomizePlanSchema = z }, ); -export const CreateSchedulePlanSchema = z - .object({ - plan_id: z.string().meta({ - description: "The ID of the plan to schedule in this phase.", - }), - feature_quantities: z.array(FeatureQuantityParamsV0Schema).optional().meta({ - description: "Optional prepaid feature quantities for this phase's plan.", - }), - version: z.number().optional().meta({ - description: "Optional explicit plan version to schedule.", - }), - customize: CreateScheduleCustomizePlanSchema.optional().meta({ - description: - "Customize the plan to schedule. Can override the price, items, or both.", - }), - subscription_id: z.string().optional().meta({ - description: - "A unique ID to identify this subscription. Useful when scheduling the same plan multiple times.", - }), - }); +export const CreateSchedulePlanSchema = z.object({ + plan_id: z.string().meta({ + description: "The ID of the plan to schedule in this phase.", + }), + feature_quantities: z.array(FeatureQuantityParamsV0Schema).optional().meta({ + description: "Optional prepaid feature quantities for this phase's plan.", + }), + version: z.number().optional().meta({ + description: "Optional explicit plan version to schedule.", + }), + customize: CreateScheduleCustomizePlanSchema.optional().meta({ + description: + "Customize the plan to schedule. Can override the price, items, or both.", + }), + subscription_id: z.string().optional().meta({ + description: + "A unique ID to identify this subscription. Useful when scheduling the same plan multiple times.", + }), +}); export const CreateSchedulePhaseSchema = z.object({ starts_at: z.number().meta({ From 11163c4ddb303044396666ef9dbd34d9811c9293 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Thu, 4 Jun 2026 14:09:18 +0100 Subject: [PATCH 02/12] chore: sheet improvements --- .../InlinePlanEditor.tsx | 3 +- .../components/v2/sheets/InlineSheetPanel.tsx | 73 +++++++++++++++++++ vite/src/components/v2/sheets/Sheet.tsx | 18 ++--- .../components/v2/sheets/SheetBackdrop.tsx | 36 +++++++++ .../views/auth/components/AuthBackground.tsx | 2 +- .../customers2/customer/CustomerSheets.tsx | 27 +------ .../customers2/customer/CustomerView2.tsx | 28 ++----- .../migrations/migration/MigrationView.tsx | 55 ++++---------- .../src/views/products/plan/ProductSheets.tsx | 30 ++------ .../products/plan/components/PlanEditor.tsx | 3 +- 10 files changed, 157 insertions(+), 118 deletions(-) create mode 100644 vite/src/components/v2/sheets/InlineSheetPanel.tsx create mode 100644 vite/src/components/v2/sheets/SheetBackdrop.tsx diff --git a/vite/src/components/v2/inline-custom-plan-editor/InlinePlanEditor.tsx b/vite/src/components/v2/inline-custom-plan-editor/InlinePlanEditor.tsx index cea22b142..58c599a7c 100644 --- a/vite/src/components/v2/inline-custom-plan-editor/InlinePlanEditor.tsx +++ b/vite/src/components/v2/inline-custom-plan-editor/InlinePlanEditor.tsx @@ -111,9 +111,10 @@ function InlinePlanEditorContent({ )} - + + diff --git a/vite/src/components/v2/sheets/InlineSheetPanel.tsx b/vite/src/components/v2/sheets/InlineSheetPanel.tsx new file mode 100644 index 000000000..22eb52ed9 --- /dev/null +++ b/vite/src/components/v2/sheets/InlineSheetPanel.tsx @@ -0,0 +1,73 @@ +import { AnimatePresence, motion } from "motion/react"; +import type { ReactNode } from "react"; +import { SheetContainer } from "@/components/v2/sheets/InlineSheet"; +import { SheetCloseButton } from "@/components/v2/sheets/SheetCloseButton"; +import { useIsMobile } from "@/hooks/useIsMobile"; +import { cn } from "@/lib/utils"; + +const SHEET_PANEL_WIDTH = "28rem"; +const SHEET_PANEL_Z_INDEX = 100; +const SHEET_PANEL_TRANSITION = { + duration: 0.3, + ease: [0.32, 0.72, 0, 1] as const, +} as const; + +interface InlineSheetPanelProps { + isOpen: boolean; + onClose: () => void; + children: ReactNode; + className?: string; + width?: string; + zIndex?: number; + transition?: { + duration: number; + ease: readonly [number, number, number, number]; + }; +} + +/** + * Shared right-hand sheet panel used across the app's inline sheet orchestrators. + * Renders a slide-in, rounded, inset panel that floats over the (separately + * rendered) backdrop so the surrounding area reads as dimmed on every side. + */ +export function InlineSheetPanel({ + isOpen, + onClose, + children, + className, + width = SHEET_PANEL_WIDTH, + zIndex = SHEET_PANEL_Z_INDEX, + transition = SHEET_PANEL_TRANSITION, +}: InlineSheetPanelProps) { + const isMobile = useIsMobile(); + return ( + + {isOpen && ( + + + + {children} + + + )} + + ); +} diff --git a/vite/src/components/v2/sheets/Sheet.tsx b/vite/src/components/v2/sheets/Sheet.tsx index 6784a688e..473d4c129 100644 --- a/vite/src/components/v2/sheets/Sheet.tsx +++ b/vite/src/components/v2/sheets/Sheet.tsx @@ -69,10 +69,7 @@ function SheetPortal({ ); } -function SheetOverlay({ - className, - ...props -}: SheetPrimitive.Backdrop.Props) { +function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) { return ( ) { ); } -function SheetTitle({ - className, - ...props -}: SheetPrimitive.Title.Props) { +function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) { return ( void; + zIndex?: number; +} + +/** + * Full-viewport dimming backdrop for inline sheets, portaled to the document body + * so it covers everything behind the floating sheet panel uniformly. + */ +export function SheetBackdrop({ + isOpen, + onClose, + zIndex = SHEET_BACKDROP_Z_INDEX, +}: SheetBackdropProps) { + return createPortal( + + {isOpen && ( + + )} + , + document.body, + ); +} diff --git a/vite/src/views/auth/components/AuthBackground.tsx b/vite/src/views/auth/components/AuthBackground.tsx index ff8432cb2..c481a6e24 100644 --- a/vite/src/views/auth/components/AuthBackground.tsx +++ b/vite/src/views/auth/components/AuthBackground.tsx @@ -20,7 +20,7 @@ export function AuthBackground({ children }: AuthBackgroundProps) { aria-hidden="true" className="absolute inset-0 w-full h-full object-cover" /> - - {!isMobile && - createPortal( - - {sheetType && !isInlineEditorOpen && ( - { - closeProductSheet(); - }} - /> - )} - , - document.body, - )} + {!isMobile && ( + + )} diff --git a/vite/src/views/migrations/migration/MigrationView.tsx b/vite/src/views/migrations/migration/MigrationView.tsx index a14d001fe..a7f42e11f 100644 --- a/vite/src/views/migrations/migration/MigrationView.tsx +++ b/vite/src/views/migrations/migration/MigrationView.tsx @@ -1,6 +1,5 @@ -import { AnimatePresence, motion } from "motion/react"; +import { motion } from "motion/react"; import { useCallback, useEffect } from "react"; -import { createPortal } from "react-dom"; import { useHotkeys } from "react-hotkeys-hook"; import { useNavigate, useParams } from "react-router"; import { @@ -9,8 +8,8 @@ import { BreadcrumbList, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { SheetContainer } from "@/components/v2/sheets/InlineSheet"; -import { SheetCloseButton } from "@/components/v2/sheets/SheetCloseButton"; +import { InlineSheetPanel } from "@/components/v2/sheets/InlineSheetPanel"; +import { SheetBackdrop } from "@/components/v2/sheets/SheetBackdrop"; import { useMigrationsQuery } from "@/hooks/queries/useMigrationsQuery"; import { navigateTo } from "@/utils/genUtils"; import { SHEET_ANIMATION } from "@/views/customers2/customer/customerAnimations"; @@ -84,44 +83,22 @@ export function MigrationView() { - {createPortal( - - {selectedCustomer && ( - - )} - , - document.body, - )} + - + {selectedCustomer && ( - - - - - - + )} - + ); } diff --git a/vite/src/views/products/plan/ProductSheets.tsx b/vite/src/views/products/plan/ProductSheets.tsx index 52ad52d74..44c8d4413 100644 --- a/vite/src/views/products/plan/ProductSheets.tsx +++ b/vite/src/views/products/plan/ProductSheets.tsx @@ -1,14 +1,11 @@ import { type ProductItem, productV2ToFeatureItems } from "@autumn/shared"; -import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef } from "react"; import { useDiscardItemAndClose, useProduct, useSheet, } from "@/components/v2/inline-custom-plan-editor/PlanEditorContext"; -import { SheetContainer } from "@/components/v2/sheets/InlineSheet"; -import { SheetCloseButton } from "@/components/v2/sheets/SheetCloseButton"; -import { useIsMobile } from "@/hooks/useIsMobile"; +import { InlineSheetPanel } from "@/components/v2/sheets/InlineSheetPanel"; import { getItemId } from "@/utils/product/productItemUtils"; import { ProductItemContext } from "../product/product-item/ProductItemContext"; @@ -20,7 +17,6 @@ import { SelectFeatureSheet } from "./components/SelectFeatureSheet"; import { SHEET_ANIMATION } from "./planAnimations"; export const ProductSheets = () => { - const isMobile = useIsMobile(); const { product, setProduct } = useProduct(); const { sheetType, @@ -162,22 +158,12 @@ export const ProductSheets = () => { }; return ( - - {sheetType && ( - - - - {renderSheet()} - - - )} - + + {renderSheet()} + ); }; diff --git a/vite/src/views/products/plan/components/PlanEditor.tsx b/vite/src/views/products/plan/components/PlanEditor.tsx index bfc67e404..7f9987070 100644 --- a/vite/src/views/products/plan/components/PlanEditor.tsx +++ b/vite/src/views/products/plan/components/PlanEditor.tsx @@ -45,9 +45,10 @@ export const PlanEditor = () => { )} - + + ); From 3443ff94ab0fb840cac26d6e473ccbcae5d04f2a Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Thu, 4 Jun 2026 14:24:29 +0100 Subject: [PATCH 03/12] chore: fix sheet bugs --- .../src/views/products/plan/ProductSheets.tsx | 42 +++++++++++++------ .../EditPlanFeatureSheet.tsx | 14 ++++--- .../components/plan-card/PlanFeatureList.tsx | 2 +- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/vite/src/views/products/plan/ProductSheets.tsx b/vite/src/views/products/plan/ProductSheets.tsx index 44c8d4413..784a521cd 100644 --- a/vite/src/views/products/plan/ProductSheets.tsx +++ b/vite/src/views/products/plan/ProductSheets.tsx @@ -39,13 +39,35 @@ export const ProductSheets = () => { const featureItems = productV2ToFeatureItems({ items: product.items }); - const isCurrentItem = (item: ProductItem) => { - const actualIndex = product.items?.indexOf(item) ?? -1; - const currentItemId = getItemId({ item, itemIndex: actualIndex }); - return itemId === currentItemId; - }; + const matchedItemIndex = product.items + ? product.items.findIndex( + (item, index) => + !!item && + featureItems.includes(item) && + getItemId({ item, itemIndex: index }) === itemId, + ) + : -1; - const currentItem = featureItems.find(isCurrentItem); + const editingIndexRef = useRef(null); + + useEffect(() => { + if (matchedItemIndex !== -1) { + editingIndexRef.current = matchedItemIndex; + } else if (itemId === null) { + editingIndexRef.current = null; + } + }, [matchedItemIndex, itemId]); + + const resolvedItemIndex = + matchedItemIndex !== -1 + ? matchedItemIndex + : editingIndexRef.current !== null && + editingIndexRef.current < (product.items?.length ?? 0) + ? editingIndexRef.current + : -1; + + const currentItem = + resolvedItemIndex !== -1 ? product.items?.[resolvedItemIndex] : undefined; const lastItemIdRef = useRef(null); @@ -97,14 +119,10 @@ export const ProductSheets = () => { return; } - if (!product || !product.items) return; - - const currentItemIndex = product.items.findIndex(isCurrentItem); - - if (currentItemIndex === -1) return; + if (!product || !product.items || resolvedItemIndex === -1) return; const updatedItems = [...product.items]; - updatedItems[currentItemIndex] = updatedItem; + updatedItems[resolvedItemIndex] = updatedItem; setProduct({ ...product, items: updatedItems }); }; diff --git a/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx b/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx index 95b2d427d..d3982aaf8 100644 --- a/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx +++ b/vite/src/views/products/plan/components/edit-plan-feature/EditPlanFeatureSheet.tsx @@ -134,12 +134,16 @@ export function EditPlanFeatureSheet({ + <> Define how customers on plan{" "} - {product.name} can - use feature{" "} - {feature?.name} -

+ + {product.name} + {" "} + can use feature{" "} + + {feature?.name} + + } action={ Date: Thu, 4 Jun 2026 14:36:36 +0100 Subject: [PATCH 04/12] fix: immediate invoice for schedules --- ai | 2 +- .../handleStripeInvoiceCreated.ts | 6 +- ...executeStripeSubscriptionScheduleAction.ts | 1 + .../buildStripePhasesUpdate.ts | 8 +- .../create-schedule-annual-proration.test.ts | 272 +++++++++++++----- .../phases/create-schedule-phases.test.ts | 239 +++++++++++++++ .../preview/create-schedule-preview.test.ts | 70 +++++ .../build-schedule-phases.spec.ts | 2 +- 8 files changed, 518 insertions(+), 82 deletions(-) diff --git a/ai b/ai index 0d561b174..0e52f71fb 160000 --- a/ai +++ b/ai @@ -1 +1 @@ -Subproject commit 0d561b1747e8f47190a01d7a9bff7d8fcb42c9dd +Subproject commit 0e52f71fbd69e7a4a58b63863a8c4929bfd9ebf8 diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/handleStripeInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/handleStripeInvoiceCreated.ts index 545b95061..8ec5d6ae3 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/handleStripeInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/handleStripeInvoiceCreated.ts @@ -36,13 +36,17 @@ export const handleStripeInvoiceCreated = async ({ await processPrepaidPricesForInvoiceCreated({ ctx, eventContext }); await processAllocatedPricesForInvoiceCreated({ ctx, eventContext }); + const shouldStoreScheduleProrationInvoice = + eventContext.stripeInvoice.billing_reason === "subscription_update" && + !!eventContext.stripeSubscription.schedule; + // Upsert Autumn invoice record const autumnInvoice = await upsertAutumnInvoice({ ctx, stripeInvoice: eventContext.stripeInvoice, stripeSubscription: eventContext.stripeSubscription, customerProducts: eventContext.customerProducts, - options: { skipNonCycleInvoices: true }, + options: { skipNonCycleInvoices: !shouldStoreScheduleProrationInvoice }, }); // Store invoice line items (async via SQS workflow) diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts index eab92010f..7c46d271f 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts @@ -21,6 +21,7 @@ const toCreatePhase = ( ...(item.metadata && { metadata: item.metadata }), })), end_date: typeof phase.end_date === "number" ? phase.end_date : undefined, + proration_behavior: phase.proration_behavior, discounts: phase.discounts as | Stripe.SubscriptionScheduleCreateParams.Phase.Discount[] | undefined, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts index 164fd8904..8b3d1f8e2 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts @@ -231,6 +231,10 @@ export const buildStripePhasesUpdate = ({ const phaseStartDateSeconds = msToSeconds(startMs); const isBillingCycleAnchorResetPhase = billingCycleAnchorResetAt === startMs; + const shouldInvoicePhaseTransition = + phaseIndex > 0 && phaseItems.length > 0; + const shouldAlwaysInvoice = + shouldInvoicePhaseTransition || isBillingCycleAnchorResetPhase; const phase: Stripe.SubscriptionScheduleUpdateParams.Phase = { items: phaseItems, start_date: phaseStartDateSeconds, @@ -239,9 +243,7 @@ export const buildStripePhasesUpdate = ({ billing_cycle_anchor: isBillingCycleAnchorResetPhase ? "phase_start" : undefined, - proration_behavior: isBillingCycleAnchorResetPhase - ? "always_invoice" - : undefined, + proration_behavior: shouldAlwaysInvoice ? "always_invoice" : undefined, discounts: stripeDiscountsToPhaseDiscounts({ stripeDiscounts: billingContext.stripeDiscounts, phaseStartDateSeconds, diff --git a/server/tests/integration/billing/create-schedule/create-schedule-annual-proration.test.ts b/server/tests/integration/billing/create-schedule/create-schedule-annual-proration.test.ts index a8d55379a..ee4f75d59 100644 --- a/server/tests/integration/billing/create-schedule/create-schedule-annual-proration.test.ts +++ b/server/tests/integration/billing/create-schedule/create-schedule-annual-proration.test.ts @@ -79,6 +79,46 @@ const pendingStripeInvoiceItems = async ({ }); }; +const stripeInvoicesForCustomer = async ({ + ctx, + customer, +}: { + ctx: Awaited>["ctx"]; + customer: ApiCustomerV3; +}) => { + if (!customer.stripe_id) + throw new Error("Expected customer to have stripe_id"); + + const invoices = await ctx.stripeCli.invoices.list({ + customer: customer.stripe_id, + limit: 100, + }); + + return await Promise.all( + invoices.data.map((invoice) => + ctx.stripeCli.invoices.retrieve(invoice.id!, { + expand: ["lines.data.price"], + }), + ), + ); +}; + +const stripeSchedulesForCustomer = async ({ + ctx, + customer, +}: { + ctx: Awaited>["ctx"]; + customer: ApiCustomerV3; +}) => { + if (!customer.stripe_id) + throw new Error("Expected customer to have stripe_id"); + + return await ctx.stripeCli.subscriptionSchedules.list({ + customer: customer.stripe_id, + limit: 10, + }); +}; + const periodDuration = (period: { start: number; end: number }) => (period.end - period.start) * 1000; @@ -178,6 +218,70 @@ const expectedAnnualProrationDiff = ({ .toDecimalPlaces(2) .toNumber(); +const expectAmountCloseTo = ({ + actual, + expected, +}: { + actual: Decimal | number; + expected: Decimal | number; +}) => { + const diff = new Decimal(actual).minus(expected).abs(); + expect( + diff.lte(0.01), + `Expected $${new Decimal(actual).toFixed(2)} to be within $0.01 of $${new Decimal(expected).toFixed(2)}`, + ).toBe(true); +}; + +const expectAutumnInvoiceWithTotal = ({ + invoices, + total, +}: { + invoices: NonNullable; + total: Decimal | number; +}) => { + const invoice = invoices.find((candidate) => + new Decimal(candidate.total).minus(total).abs().lte(0.01), + ); + + expect( + invoice, + `Expected Autumn invoice total $${new Decimal(total).toFixed(2)}`, + ).toBeDefined(); + return invoice!; +}; + +const expectStripeInvoiceWithIntervalTotals = ({ + invoices, + yearTotal, + monthTotal, +}: { + invoices: Stripe.Invoice[]; + yearTotal: number; + monthTotal: number; +}) => { + const invoice = invoices.find((candidate) => { + const candidateYearTotal = intervalLineTotal({ + invoice: candidate, + interval: "year", + }); + const candidateMonthTotal = intervalLineTotal({ + invoice: candidate, + interval: "month", + }); + + return ( + candidateYearTotal.minus(yearTotal).abs().lte(0.01) && + candidateMonthTotal.minus(monthTotal).abs().lte(0.01) + ); + }); + + expect( + invoice, + `Expected Stripe invoice with yearly total $${yearTotal} and monthly total $${monthTotal}`, + ).toBeDefined(); + return invoice!; +}; + test.concurrent( `${chalk.yellowBright("create-schedule: customized annual prepaid proration ignores removed monthly prepaid")}`, async () => { @@ -261,6 +365,14 @@ test.concurrent( ctx, customer: initialCustomer, }); + const initialSchedules = await stripeSchedulesForCustomer({ + ctx, + customer: initialCustomer, + }); + expect(initialSchedules.data[0]?.phases[1]?.proration_behavior).toBe( + "always_invoice", + ); + expect(initialSchedules.data[0]?.billing_mode?.type).toBe("flexible"); const annualPeriod = annualPeriodFromInitialInvoice({ invoice: initialInvoice, }); @@ -273,11 +385,11 @@ test.concurrent( const customerAfterTransition = await autumnV1.customers.get(id); - const transitionInvoice = await latestStripeInvoice({ + const pendingItems = await pendingStripeInvoiceItems({ ctx, customer: customerAfterTransition, }); - const pendingItems = await pendingStripeInvoiceItems({ + const stripeInvoices = await stripeInvoicesForCustomer({ ctx, customer: customerAfterTransition, }); @@ -287,10 +399,37 @@ test.concurrent( transitionAt, billingPeriod: annualPeriod, }); - await expectCustomerInvoiceCorrect({ - customer: customerAfterTransition, - count: 2, - latestTotal: 10, + expect(customerAfterTransition.invoices).toHaveLength(3); + expectAutumnInvoiceWithTotal({ + invoices: customerAfterTransition.invoices!, + total: 10, + }); + expectAutumnInvoiceWithTotal({ + invoices: customerAfterTransition.invoices!, + total: new Decimal(expectedProration).minus(10), + }); + const prorationInvoice = expectStripeInvoiceWithIntervalTotals({ + invoices: stripeInvoices, + yearTotal: expectedProration, + monthTotal: -10, + }); + expectAmountCloseTo({ + actual: intervalLineTotal({ + invoice: prorationInvoice, + interval: "year", + }), + expected: expectedProration, + }); + expectAmountCloseTo({ + actual: intervalLineTotal({ + invoice: prorationInvoice, + interval: "month", + }), + expected: -10, + }); + expectAmountCloseTo({ + actual: new Decimal(prorationInvoice.total).div(100), + expected: new Decimal(expectedProration).minus(10), }); expect( pendingItemIntervalTotal({ @@ -299,42 +438,15 @@ test.concurrent( }) .toDecimalPlaces(2) .toNumber(), - ).toBe(expectedProration); + ).toBe(0); expect( - intervalLineTotal({ invoice: transitionInvoice, interval: "month" }) - .plus( - pendingItemIntervalTotal({ - items: pendingItems.data, - interval: "month", - }), - ) + pendingItemIntervalTotal({ + items: pendingItems.data, + interval: "month", + }) .toDecimalPlaces(2) .toNumber(), ).toBe(0); - expect( - new Decimal(transitionInvoice.total) - .div(100) - .plus( - pendingItemIntervalTotal({ - items: pendingItems.data, - interval: "month", - }), - ) - .plus( - pendingItemIntervalTotal({ - items: pendingItems.data, - interval: "year", - }), - ) - .toDecimalPlaces(2) - .toNumber(), - ).toBe(expectedProration); - expect( - pendingItems.data.some( - (item) => item.amount < 0 && item.amount !== -1000, - ), - ).toBe(true); - expect(pendingItems.data.some((item) => item.amount > 0)).toBe(true); }, ); @@ -415,6 +527,14 @@ test.concurrent( ctx, customer: initialCustomer, }); + const initialSchedules = await stripeSchedulesForCustomer({ + ctx, + customer: initialCustomer, + }); + expect(initialSchedules.data[0]?.phases[1]?.proration_behavior).toBe( + "always_invoice", + ); + expect(initialSchedules.data[0]?.billing_mode?.type).toBe("flexible"); const annualPeriod = annualPeriodFromInitialInvoice({ invoice: initialInvoice, }); @@ -427,11 +547,11 @@ test.concurrent( const customerAfterTransition = await autumnV1.customers.get(id); - const transitionInvoice = await latestStripeInvoice({ + const pendingItems = await pendingStripeInvoiceItems({ ctx, customer: customerAfterTransition, }); - const pendingItems = await pendingStripeInvoiceItems({ + const stripeInvoices = await stripeInvoicesForCustomer({ ctx, customer: customerAfterTransition, }); @@ -441,10 +561,37 @@ test.concurrent( transitionAt, billingPeriod: annualPeriod, }); - await expectCustomerInvoiceCorrect({ - customer: customerAfterTransition, - count: 2, - latestTotal: 10, + expect(customerAfterTransition.invoices).toHaveLength(3); + expectAutumnInvoiceWithTotal({ + invoices: customerAfterTransition.invoices!, + total: 10, + }); + expectAutumnInvoiceWithTotal({ + invoices: customerAfterTransition.invoices!, + total: new Decimal(expectedProration).minus(10), + }); + const prorationInvoice = expectStripeInvoiceWithIntervalTotals({ + invoices: stripeInvoices, + yearTotal: expectedProration, + monthTotal: -10, + }); + expectAmountCloseTo({ + actual: intervalLineTotal({ + invoice: prorationInvoice, + interval: "year", + }), + expected: expectedProration, + }); + expectAmountCloseTo({ + actual: intervalLineTotal({ + invoice: prorationInvoice, + interval: "month", + }), + expected: -10, + }); + expectAmountCloseTo({ + actual: new Decimal(prorationInvoice.total).div(100), + expected: new Decimal(expectedProration).minus(10), }); expect( pendingItemIntervalTotal({ @@ -453,41 +600,14 @@ test.concurrent( }) .toDecimalPlaces(2) .toNumber(), - ).toBe(expectedProration); + ).toBe(0); expect( - intervalLineTotal({ invoice: transitionInvoice, interval: "month" }) - .plus( - pendingItemIntervalTotal({ - items: pendingItems.data, - interval: "month", - }), - ) + pendingItemIntervalTotal({ + items: pendingItems.data, + interval: "month", + }) .toDecimalPlaces(2) .toNumber(), ).toBe(0); - expect( - new Decimal(transitionInvoice.total) - .div(100) - .plus( - pendingItemIntervalTotal({ - items: pendingItems.data, - interval: "month", - }), - ) - .plus( - pendingItemIntervalTotal({ - items: pendingItems.data, - interval: "year", - }), - ) - .toDecimalPlaces(2) - .toNumber(), - ).toBe(expectedProration); - expect( - pendingItems.data.some( - (item) => item.amount < 0 && item.amount !== -1000, - ), - ).toBe(true); - expect(pendingItems.data.some((item) => item.amount > 0)).toBe(true); }, ); diff --git a/server/tests/integration/billing/create-schedule/phases/create-schedule-phases.test.ts b/server/tests/integration/billing/create-schedule/phases/create-schedule-phases.test.ts index 294e9ef25..d4423e972 100644 --- a/server/tests/integration/billing/create-schedule/phases/create-schedule-phases.test.ts +++ b/server/tests/integration/billing/create-schedule/phases/create-schedule-phases.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "bun:test"; import { type ApiCustomerV3, + applyProration, type CreateScheduleParamsV0Input, CusProductStatus, customerProducts, @@ -15,12 +16,133 @@ import { products } from "@tests/utils/fixtures/products"; import { advanceTestClock } from "@tests/utils/stripeUtils"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; +import { Decimal } from "decimal.js"; import { eq, inArray } from "drizzle-orm"; +import type Stripe from "stripe"; import { getCustomerProductRows, getRequiredScheduleId, } from "../utils/createScheduleTestHelpers"; +const latestStripeInvoice = async ({ + ctx, + customer, +}: { + ctx: Awaited>["ctx"]; + customer: ApiCustomerV3; +}) => { + const stripeId = customer.invoices?.[0]?.stripe_id; + if (!stripeId) throw new Error("Expected latest invoice to have stripe_id"); + + return await ctx.stripeCli.invoices.retrieve(stripeId, { + expand: ["lines.data.price"], + }); +}; + +const pendingStripeInvoiceItems = async ({ + ctx, + customer, +}: { + ctx: Awaited>["ctx"]; + customer: ApiCustomerV3; +}) => { + if (!customer.stripe_id) + throw new Error("Expected customer to have stripe_id"); + + return await ctx.stripeCli.invoiceItems.list({ + customer: customer.stripe_id, + pending: true, + limit: 100, + }); +}; + +const stripeInvoicesForCustomer = async ({ + ctx, + customer, +}: { + ctx: Awaited>["ctx"]; + customer: ApiCustomerV3; +}) => { + if (!customer.stripe_id) + throw new Error("Expected customer to have stripe_id"); + + const invoices = await ctx.stripeCli.invoices.list({ + customer: customer.stripe_id, + limit: 100, + }); + + return await Promise.all( + invoices.data.map((invoice) => + ctx.stripeCli.invoices.retrieve(invoice.id!, { + expand: ["lines.data.price"], + }), + ), + ); +}; + +const lineAmountDollars = (line: Stripe.InvoiceLineItem) => + new Decimal(line.amount).div(100); + +const invoiceLineTotal = (invoice: Stripe.Invoice) => + invoice.lines.data.reduce( + (total, line) => total.plus(lineAmountDollars(line)), + new Decimal(0), + ); + +const initialMonthlyPeriod = (invoice: Stripe.Invoice) => { + const monthlyLine = invoice.lines.data.find((line) => line.amount > 0); + if (!monthlyLine) throw new Error("Expected a positive monthly invoice line"); + + return { + start: monthlyLine.period.start * 1000, + end: monthlyLine.period.end * 1000, + }; +}; + +const expectedMonthlyProrationDiff = ({ + oldAmount, + newAmount, + transitionAt, + billingPeriod, +}: { + oldAmount: number; + newAmount: number; + transitionAt: number; + billingPeriod: { start: number; end: number }; +}) => + new Decimal( + applyProration({ + now: transitionAt, + billingPeriod, + amount: newAmount, + }), + ) + .minus( + applyProration({ + now: transitionAt, + billingPeriod, + amount: oldAmount, + }), + ) + .toDecimalPlaces(2) + .toNumber(); + +const expectStripeInvoiceWithTotal = ({ + invoices, + total, +}: { + invoices: Stripe.Invoice[]; + total: number; +}) => { + const invoice = invoices.find((candidate) => { + const candidateTotal = new Decimal(candidate.total).div(100); + return candidateTotal.minus(total).abs().lte(0.01); + }); + + expect(invoice, `Expected Stripe invoice total $${total}`).toBeDefined(); + return invoice!; +}; + test.concurrent( `${chalk.yellowBright("create-schedule: bills the first phase immediately and stores later phases as scheduled")}`, async () => { @@ -403,3 +525,120 @@ test.concurrent( }); }, ); + +test.concurrent( + `${chalk.yellowBright("create-schedule: phase transition invoices monthly upgrade proration immediately")}`, + async () => { + const pro = products.pro({ + id: "create-schedule-transition-invoice-pro", + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + const premium = products.premium({ + id: "create-schedule-transition-invoice-premium", + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + + const { customerId, autumnV1, ctx, testClockId, advancedTo } = + await initScenario({ + customerId: "create-schedule-transition-invoice", + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium] }), + ], + actions: [], + }); + + const now = advancedTo; + const transitionAt = now + ms.days(15); + await autumnV1.billing.createSchedule({ + customer_id: customerId, + phases: [ + { + starts_at: now, + plans: [{ plan_id: pro.id }], + }, + { + starts_at: transitionAt, + plans: [{ plan_id: premium.id }], + }, + ], + }); + + const initialCustomer = + await autumnV1.customers.get(customerId); + await expectCustomerInvoiceCorrect({ + customer: initialCustomer, + count: 1, + latestTotal: 20, + }); + + const initialInvoice = await latestStripeInvoice({ + ctx, + customer: initialCustomer, + }); + const billingPeriod = initialMonthlyPeriod(initialInvoice); + const expectedProration = expectedMonthlyProrationDiff({ + oldAmount: 20, + newAmount: 50, + transitionAt, + billingPeriod, + }); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + advanceTo: transitionAt, + waitForSeconds: 30, + }); + + const customerAfterTransition = + await autumnV1.customers.get(customerId); + await expectCustomerProducts({ + customer: customerAfterTransition, + active: [premium.id], + notPresent: [pro.id], + }); + await expectCustomerInvoiceCorrect({ + customer: customerAfterTransition, + count: 2, + latestTotal: expectedProration, + }); + const stripeInvoices = await stripeInvoicesForCustomer({ + ctx, + customer: customerAfterTransition, + }); + const transitionInvoice = expectStripeInvoiceWithTotal({ + invoices: stripeInvoices, + total: expectedProration, + }); + expect( + invoiceLineTotal(transitionInvoice).toDecimalPlaces(2).toNumber(), + ).toBe(expectedProration); + + const pendingItems = await pendingStripeInvoiceItems({ + ctx, + customer: customerAfterTransition, + }); + expect(pendingItems.data).toHaveLength(0); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + advanceTo: billingPeriod.end, + waitForSeconds: 30, + }); + + const customerAfterRenewal = + await autumnV1.customers.get(customerId); + await expectCustomerProducts({ + customer: customerAfterRenewal, + active: [premium.id], + notPresent: [pro.id], + }); + await expectCustomerInvoiceCorrect({ + customer: customerAfterRenewal, + count: 3, + latestTotal: 50, + }); + }, +); diff --git a/server/tests/integration/billing/create-schedule/preview/create-schedule-preview.test.ts b/server/tests/integration/billing/create-schedule/preview/create-schedule-preview.test.ts index ff92c485c..38f69b45c 100644 --- a/server/tests/integration/billing/create-schedule/preview/create-schedule-preview.test.ts +++ b/server/tests/integration/billing/create-schedule/preview/create-schedule-preview.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; import { + type ApiCustomerV3, type AttachPreviewResponse, applyProration, BillingInterval, @@ -885,3 +886,72 @@ test.concurrent( }); }, ); + +test.concurrent( + `${chalk.yellowBright("create-schedule preview 15: invoice excludes unrelated pending Stripe invoice items")}`, + async () => { + const pro = products.pro({ + id: "preview-pending-items-pro", + items: [items.monthlyMessages({ includedUsage: 100 })], + }); + const premium = products.premium({ + id: "preview-pending-items-premium", + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + + const { customerId, autumnV1, ctx, advancedTo } = await initScenario({ + customerId: "create-schedule-preview-pending-items", + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, premium] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + const customer = await autumnV1.customers.get(customerId); + const stripeSubscriptions = await ctx.stripeCli.subscriptions.list({ + customer: customer.stripe_id!, + limit: 1, + }); + const stripeSubscription = stripeSubscriptions.data[0]; + expect(stripeSubscription).toBeDefined(); + + await ctx.stripeCli.invoiceItems.create({ + customer: customer.stripe_id!, + subscription: stripeSubscription.id, + amount: 12345, + currency: "usd", + description: "Unrelated pending Stripe invoice item", + }); + + const params: CreateScheduleParamsV0Input = { + customer_id: customerId, + invoice_mode: { + enabled: true, + finalize: false, + enable_plan_immediately: true, + }, + phases: [ + { + starts_at: advancedTo, + plans: [{ plan_id: premium.id }], + }, + ], + }; + const preview = await previewCreateSchedule({ autumnV1, params }); + const response = await autumnV1.billing.createSchedule(params); + + expect(response.status).toBe("created"); + expect(response.invoice?.total).toBe(preview.total); + + const stripeInvoice = await ctx.stripeCli.invoices.retrieve( + response.invoice!.stripe_id!, + { expand: ["lines"] }, + ); + expect( + stripeInvoice.lines.data.some( + (line) => line.description === "Unrelated pending Stripe invoice item", + ), + ).toBe(false); + }, +); diff --git a/server/tests/unit/billing/stripe/subscription-schedules/build-schedule-phases.spec.ts b/server/tests/unit/billing/stripe/subscription-schedules/build-schedule-phases.spec.ts index bf7816190..03ff3d986 100644 --- a/server/tests/unit/billing/stripe/subscription-schedules/build-schedule-phases.spec.ts +++ b/server/tests/unit/billing/stripe/subscription-schedules/build-schedule-phases.spec.ts @@ -144,7 +144,7 @@ describe( // Phase 2: Pro expect(phases[1].start_date).toBe(msToSeconds(proStartMs)); expect(phases[1].end_date).toBeUndefined(); - expect(phases[1].proration_behavior).toBeUndefined(); + expect(phases[1].proration_behavior).toBe("always_invoice"); expectPhaseItems(phases[1].items!, getStripePriceIds(pro)); }); From 1707a7773b2d87ca67b496ff37e3f48971a3587c Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 15:06:56 +0100 Subject: [PATCH 05/12] analytics --- ai | 2 +- apps/leaf/package.json | 1 + apps/leaf/src/agent/agent.ts | 55 +++++-- apps/leaf/src/agent/mcp.ts | 31 +++- apps/leaf/src/agent/messages.ts | 14 +- apps/leaf/src/approvals/flow.ts | 60 +++++-- apps/leaf/src/bot.ts | 57 ++++++- apps/leaf/src/lib/logger.ts | 60 +++++++ apps/leaf/src/main.ts | 13 +- apps/leaf/src/mcp/http.ts | 13 +- apps/leaf/src/providers/slack/routes.ts | 24 ++- apps/leaf/src/types.ts | 2 + apps/leaf/tests/unit/lib/logger.test.ts | 32 ++++ apps/leaf/tsconfig.json | 1 + bun.lock | 23 +++ docker/Dockerfile | 1 + docker/dev.dockerfile | 45 ------ package.json | 2 + packages/logging/package.json | 30 ++++ .../logging/src/context/addContextToLogs.ts | 38 +++++ packages/logging/src/context/types.ts | 35 +++++ packages/logging/src/ids/createSessionId.ts | 21 +++ packages/logging/src/ids/createTraceId.ts | 3 + packages/logging/src/index.ts | 40 +++++ packages/logging/src/logger/autumnLogger.ts | 69 ++++++++ packages/logging/src/logger/consoleLogger.ts | 28 ++++ packages/logging/src/logger/createLogger.ts | 60 +++++++ packages/logging/src/logger/loggerWrappers.ts | 71 +++++++++ .../src/logger/resolveLoggerOptions.ts | 67 ++++++++ packages/logging/src/payload/asAxiomMap.ts | 8 + .../logging/src/payload/guardLogPayload.ts | 147 ++++++++++++++++++ .../logging/src/streams/consoleJsonStream.ts | 9 ++ .../logging/src/streams/prettyLogStream.ts | 116 ++++++++++++++ packages/logging/src/types.ts | 56 +++++++ packages/logging/tsconfig.json | 34 ++++ packages/mcp/package.json | 1 + packages/mcp/src/agent/axiom.ts | 33 +++- packages/mcp/src/analytics/analyticsSink.ts | 4 +- packages/mcp/src/analytics/analyticsTypes.ts | 26 +++- packages/mcp/src/analytics/axiomSink.ts | 72 --------- packages/mcp/src/analytics/emitToolEvent.ts | 43 +++-- packages/mcp/src/analytics/index.ts | 5 +- packages/mcp/src/analytics/instrumentTools.ts | 33 +++- packages/mcp/src/analytics/loggerSink.ts | 60 +++++++ packages/mcp/src/analytics/sessionId.ts | 7 +- packages/mcp/src/server/auth/oauth.ts | 7 +- packages/mcp/src/tools/index.ts | 7 +- packages/mcp/src/tools/utils/intent.ts | 47 ++++++ .../tests/unit/mcp-server/agent/tools.test.ts | 55 +++++-- .../tests/unit/mcp-server/analytics.test.ts | 130 ++++++++++++++++ packages/mcp/tsconfig.json | 1 + scripts/axiom/createLeafDataset.ts | 74 +++++++-- scripts/mcp/addMcp.ts | 91 +++++++++++ 53 files changed, 1726 insertions(+), 238 deletions(-) create mode 100644 apps/leaf/src/lib/logger.ts create mode 100644 apps/leaf/tests/unit/lib/logger.test.ts delete mode 100644 docker/dev.dockerfile create mode 100644 packages/logging/package.json create mode 100644 packages/logging/src/context/addContextToLogs.ts create mode 100644 packages/logging/src/context/types.ts create mode 100644 packages/logging/src/ids/createSessionId.ts create mode 100644 packages/logging/src/ids/createTraceId.ts create mode 100644 packages/logging/src/index.ts create mode 100644 packages/logging/src/logger/autumnLogger.ts create mode 100644 packages/logging/src/logger/consoleLogger.ts create mode 100644 packages/logging/src/logger/createLogger.ts create mode 100644 packages/logging/src/logger/loggerWrappers.ts create mode 100644 packages/logging/src/logger/resolveLoggerOptions.ts create mode 100644 packages/logging/src/payload/asAxiomMap.ts create mode 100644 packages/logging/src/payload/guardLogPayload.ts create mode 100644 packages/logging/src/streams/consoleJsonStream.ts create mode 100644 packages/logging/src/streams/prettyLogStream.ts create mode 100644 packages/logging/src/types.ts create mode 100644 packages/logging/tsconfig.json delete mode 100644 packages/mcp/src/analytics/axiomSink.ts create mode 100644 packages/mcp/src/analytics/loggerSink.ts create mode 100644 packages/mcp/src/tools/utils/intent.ts create mode 100644 packages/mcp/tests/unit/mcp-server/analytics.test.ts create mode 100644 scripts/mcp/addMcp.ts diff --git a/ai b/ai index a84bc7418..0e52f71fb 160000 --- a/ai +++ b/ai @@ -1 +1 @@ -Subproject commit a84bc7418cd985f3b831b24a5e49f447b0eff0d4 +Subproject commit 0e52f71fbd69e7a4a58b63863a8c4929bfd9ebf8 diff --git a/apps/leaf/package.json b/apps/leaf/package.json index b1cd01d9d..8d0684771 100644 --- a/apps/leaf/package.json +++ b/apps/leaf/package.json @@ -10,6 +10,7 @@ "ts": "tsc --noEmit" }, "dependencies": { + "@autumn/logging": "workspace:*", "@autumn/mcp": "workspace:*", "@autumn/shared": "workspace:*", "@chat-adapter/slack": "^4.29.0", diff --git a/apps/leaf/src/agent/agent.ts b/apps/leaf/src/agent/agent.ts index 5143b1a58..3bf3a837d 100644 --- a/apps/leaf/src/agent/agent.ts +++ b/apps/leaf/src/agent/agent.ts @@ -1,13 +1,12 @@ +import type { AutumnLogger } from "@autumn/logging"; import { AppEnv } from "@autumn/shared"; import { Agent } from "@mastra/core/agent"; import { z } from "zod"; -import { - createAutumnMcpClient, - getAutumnMcpTools, -} from "./mcp.js"; -import { createFirecrawlTools } from "./firecrawl.js"; import { env as chatEnv } from "../lib/env.js"; +import { logger as rootLogger } from "../lib/logger.js"; import type { ChatContextMessage } from "../types.js"; +import { createFirecrawlTools } from "./firecrawl.js"; +import { createAutumnMcpClient, getAutumnMcpTools } from "./mcp.js"; const docs = [ "autumn://docs/tool-composition", @@ -37,15 +36,25 @@ const recentMessageContext = (messages: ChatContextMessage[] = []) => })); export const selectChatEnv = async ({ + logger = rootLogger, message, recentMessages, select, }: { + logger?: AutumnLogger; message: string; recentMessages?: ChatContextMessage[]; select?: () => Promise | unknown; }) => { - if (select) return envSelectionSchema.parse(await select()).env; + if (select) { + const env = envSelectionSchema.parse(await select()).env; + logger.debug("Selected chat environment from override", { + event: "leaf.chat_env_selected", + context: { env }, + data: { source: "override" }, + }); + return env; + } const agent = new Agent({ id: "autumn-chat-env", @@ -61,9 +70,12 @@ export const selectChatEnv = async ({ instructions: "Return live unless the latest user request clearly asks to use sandbox or test mode.", }, - context: [ - ...recentMessageContext(recentMessages), - ], + context: [...recentMessageContext(recentMessages)], + }); + logger.debug("Selected chat environment from model", { + event: "leaf.chat_env_selected", + context: { env: output.object.env }, + data: { source: "model" }, }); return output.object.env; }; @@ -86,6 +98,7 @@ const readDocs = async (mcp: ReturnType) => { export const runChatAgent = async ({ apiKey, env, + logger = rootLogger, message, threadId, resourceId, @@ -95,6 +108,7 @@ export const runChatAgent = async ({ }: { apiKey: string; env: AppEnv; + logger?: AutumnLogger; message: string; onAction?: (message: string) => Promise | void; threadId: string; @@ -111,10 +125,22 @@ export const runChatAgent = async ({ } | undefined; try { + logger.info("Starting chat agent", { + event: "leaf.agent_started", + context: { + env, + org_id: resourceId, + provider, + }, + data: { + thread_id: threadId, + }, + }); await onAction?.("Loading Autumn tools and guidance"); const [tools, docsText] = await Promise.all([ getAutumnMcpTools(mcp, { applyApprovalPolicy: true, + logger, onToolCall: onAction, onPreview: (approval) => { previewApproval = approval; @@ -150,8 +176,19 @@ export const runChatAgent = async ({ ...recentMessageContext(recentMessages), ], }); + logger.info("Completed chat agent", { + event: "leaf.agent_completed", + context: { env }, + data: { + finish_reason: output.finishReason, + run_id: output.runId, + }, + }); return { ...output, env, previewApproval }; } finally { await mcp.disconnect(); + logger.debug("Disconnected Autumn MCP client", { + event: "leaf.mcp_client_disconnected", + }); } }; diff --git a/apps/leaf/src/agent/mcp.ts b/apps/leaf/src/agent/mcp.ts index 44b2dc2ec..1b4b8113d 100644 --- a/apps/leaf/src/agent/mcp.ts +++ b/apps/leaf/src/agent/mcp.ts @@ -1,6 +1,8 @@ +import type { AutumnLogger } from "@autumn/logging"; import { MCPClient } from "@mastra/mcp"; -import { getWriteToolForPreview, toolLabel } from "./toolPolicy.js"; import { env } from "../lib/env.js"; +import { logger as rootLogger } from "../lib/logger.js"; +import { getWriteToolForPreview, toolLabel } from "./toolPolicy.js"; type AutumnTool = { execute?: ( @@ -14,6 +16,7 @@ type AutumnTool = { type ToolOptions = { applyApprovalPolicy?: boolean; + logger?: AutumnLogger; onToolCall?: (message: string) => Promise | void; onPreview?: (approval: { toolName: string; @@ -77,12 +80,25 @@ export const getAutumnMcpTools = async ( mcp: MCPClient, options: ToolOptions = {}, ) => { + const logger = options.logger ?? rootLogger; const { toolsets, errors } = await mcp.listToolsetsWithErrors(); if (Object.keys(errors).length) { - throw new Error(`Could not load Autumn MCP tools: ${JSON.stringify(errors)}`); + logger.error("Could not load Autumn MCP tools", { + event: "leaf.mcp_tools_load_failed", + data: { errors }, + }); + throw new Error( + `Could not load Autumn MCP tools: ${JSON.stringify(errors)}`, + ); } const tools = (toolsets.autumn ?? {}) as Record; + logger.info("Loaded Autumn MCP tools", { + event: "leaf.mcp_tools_loaded", + data: { + tool_count: Object.keys(tools).length, + }, + }); for (const [toolName, tool] of Object.entries(tools)) { if (options.applyApprovalPolicy) { tool.requireApproval = tool.mcp?.annotations?.destructiveHint === true; @@ -91,10 +107,21 @@ export const getAutumnMcpTools = async ( if (tool.execute && (options.onToolCall || options.onPreview)) { const execute = tool.execute.bind(tool); tool.execute = async (args, ...rest) => { + logger.info("Calling Autumn MCP tool", { + event: "leaf.mcp_tool_called", + tool: toolName, + }); await options.onToolCall?.(formatToolAction(toolName, args)); const result = await execute(args, ...rest); const writeTool = getWriteToolForPreview(toolName); if (writeTool) { + logger.info("Captured Autumn MCP preview", { + event: "leaf.mcp_preview_captured", + tool: writeTool, + data: { + preview_tool: toolName, + }, + }); options.onPreview?.({ toolName: writeTool, toolArgs: args, diff --git a/apps/leaf/src/agent/messages.ts b/apps/leaf/src/agent/messages.ts index 395a410f1..c434a5d58 100644 --- a/apps/leaf/src/agent/messages.ts +++ b/apps/leaf/src/agent/messages.ts @@ -1,6 +1,7 @@ -import { runChatAgent, selectChatEnv } from "./agent.js"; +import { logger as rootLogger } from "../lib/logger.js"; import { getInstallationKey } from "../providers/slack/installations.js"; import { agentOutputSchema, type BotMessage } from "../types.js"; +import { runChatAgent, selectChatEnv } from "./agent.js"; const withTimeout = (promise: Promise, ms: number) => new Promise((resolve, reject) => { @@ -13,6 +14,7 @@ const withTimeout = (promise: Promise, ms: number) => export const runMessage = async ({ installation, + logger = rootLogger, onAction, recentMessages, text, @@ -23,11 +25,21 @@ export const runMessage = async ({ const env = await selectChatEnv({ message: text, recentMessages, + logger, + }); + logger.info("Selected chat environment", { + event: "leaf.chat_env_selected", + context: { + env, + org_id: installation.org_id, + provider: installation.provider, + }, }); return agentOutputSchema.parse( await runChatAgent({ apiKey: getInstallationKey(installation, env), env, + logger, message: text, onAction, threadId, diff --git a/apps/leaf/src/approvals/flow.ts b/apps/leaf/src/approvals/flow.ts index 3486125cd..f79e6930d 100644 --- a/apps/leaf/src/approvals/flow.ts +++ b/apps/leaf/src/approvals/flow.ts @@ -1,5 +1,16 @@ +import type { AutumnLogger } from "@autumn/logging"; import type { ChatApproval, ChatInstallation } from "@autumn/shared"; import type { ActionEvent } from "chat"; +import { toolLabel } from "../agent/toolPolicy.js"; +import { logger as rootLogger } from "../lib/logger.js"; +import type { AgentOutput } from "../types.js"; +import { approvalCard, approvalStatusCard } from "../ui/blocks.js"; +import { + finishLoading, + type LoadingState, + type ReplyTarget, +} from "../ui/progress.js"; +import { approvalRequestFromOutput } from "./request.js"; import { approveAndRun, cancelApproval, @@ -7,21 +18,13 @@ import { getApproval, isErrorResult, } from "./store.js"; -import { approvalRequestFromOutput } from "./request.js"; -import { approvalCard, approvalStatusCard } from "../ui/blocks.js"; -import { - finishLoading, - type LoadingState, - type ReplyTarget, -} from "../ui/progress.js"; -import { toolLabel } from "../agent/toolPolicy.js"; -import type { AgentOutput } from "../types.js"; export const postApprovalRequest = async ({ channelId, installation, loading, logAction, + logger = rootLogger, output, providerUserId, target, @@ -30,6 +33,7 @@ export const postApprovalRequest = async ({ installation: ChatInstallation; loading: LoadingState; logAction: (message: string) => Promise | void; + logger?: AutumnLogger; output: AgentOutput; providerUserId: string; target: ReplyTarget; @@ -47,6 +51,15 @@ export const postApprovalRequest = async ({ }); await logAction(`Waiting for approval: ${toolLabel(approval.toolName)}`); + logger.info("Created approval request", { + event: "leaf.approval_created", + context: { + env: approval.env, + org_id: installation.org_id, + }, + approval_id: approvalId, + tool: approval.toolName, + }); await finishLoading(target, loading, "Preview ready."); await target.post( approvalCard({ @@ -92,10 +105,22 @@ export const handleApprovalAction = async (event: ActionEvent) => { if (!event.value) return; try { + rootLogger.info("Received approval action", { + event: "leaf.approval_action_received", + approval_id: event.value, + action: event.actionId, + data: { + provider_user_id: event.user.userId, + }, + }); const details = await approvalDetails(event.value); if (event.actionId === "cancel_billing_action") { const cancelled = await cancelApproval(event.value, event.user.userId); if (!cancelled) { + rootLogger.warn("Approval cancellation ignored", { + event: "leaf.approval_cancel_ignored", + approval_id: event.value, + }); const current = await getApproval(event.value); await editActionMessage( event, @@ -110,6 +135,11 @@ export const handleApprovalAction = async (event: ActionEvent) => { event, approvalStatusCard({ status: "cancelled", ...details }), ); + rootLogger.info("Cancelled approval", { + event: "leaf.approval_cancelled", + approval_id: event.value, + tool: details.toolName, + }); return; } @@ -118,6 +148,12 @@ export const handleApprovalAction = async (event: ActionEvent) => { approvalStatusCard({ status: "running", ...details }), ); const result = await approveAndRun(event.value, event.user.userId); + rootLogger.info("Completed approval action", { + event: "leaf.approval_completed", + approval_id: event.value, + status: isErrorResult(result) ? "failed" : "approved", + tool: details.toolName, + }); await editActionMessage( event, approvalStatusCard({ @@ -127,7 +163,11 @@ export const handleApprovalAction = async (event: ActionEvent) => { }), ); } catch (error) { - console.error("[chat] Approval action failed", error); + rootLogger.error("[chat] Approval action failed", error, { + event: "leaf.approval_failed", + approval_id: event.value, + action: event.actionId, + }); const current = await getApproval(event.value); await editActionMessage( event, diff --git a/apps/leaf/src/bot.ts b/apps/leaf/src/bot.ts index 1e9db935c..800b9a43d 100644 --- a/apps/leaf/src/bot.ts +++ b/apps/leaf/src/bot.ts @@ -2,12 +2,19 @@ import { createSlackAdapter } from "@chat-adapter/slack"; import { createPostgresState } from "@chat-adapter/state-pg"; import type { Message, Thread } from "chat"; import { Chat } from "chat"; +import { runMessage } from "./agent/messages.js"; import { handleApprovalAction, postApprovalRequest } from "./approvals/flow.js"; -import { getSlackWorkspaceId } from "./providers/slack/context.js"; import { decrypt } from "./lib/crypto.js"; import { env } from "./lib/env.js"; +import { + addLeafContext, + createLeafSessionContext, + logger as rootLogger, +} from "./lib/logger.js"; +import { getSlackWorkspaceId } from "./providers/slack/context.js"; import { findInstallation } from "./providers/slack/installations.js"; -import { runMessage } from "./agent/messages.js"; +import { getRecentMessages } from "./providers/slack/threadContext.js"; +import type { ChatContextMessage } from "./types.js"; import { createActionLogger, finishLoading, @@ -15,8 +22,6 @@ import { type ReplyTarget, startLoading, } from "./ui/progress.js"; -import { getRecentMessages } from "./providers/slack/threadContext.js"; -import type { ChatContextMessage } from "./types.js"; export const chatAdapterNames = ["slack"]; @@ -66,15 +71,46 @@ const runAndReply = async ({ threadId: string; }) => { let loading: LoadingState = null; + let logger = rootLogger; try { const workspaceId = getSlackWorkspaceId(raw); + const session = createLeafSessionContext({ + channelId, + provider: "slack", + providerUserId, + threadId, + workspaceId, + }); + logger = addLeafContext(rootLogger, { + ...session.context, + agent_run_id: session.agentRunId, + }); + logger.info("Received Slack message", { + event: "leaf.slack_message_received", + data: { + text_length: text.length, + }, + }); const installation = await findInstallation("slack", workspaceId); - if (!installation || !text.trim()) return; + if (!installation) { + logger.warn("Slack installation not found", { + event: "leaf.slack_installation_missing", + }); + return; + } + if (!text.trim()) { + logger.info("Skipping empty Slack message", { + event: "leaf.slack_message_skipped", + data: { reason: "empty" }, + }); + return; + } loading = await startLoading(target); const logAction = createActionLogger(loading); const output = await runMessage({ installation, + logger, onAction: logAction, recentMessages, text, @@ -86,6 +122,7 @@ const runAndReply = async ({ installation, loading, logAction, + logger, output, providerUserId, target, @@ -94,8 +131,16 @@ const runAndReply = async ({ await finishLoading(target, loading, "Done."); await target.post({ markdown: output.text || "Done." }); + logger.info("Posted Slack response", { + event: "leaf.slack_response_posted", + data: { + has_text: Boolean(output.text), + }, + }); } catch (error) { - console.error("[chat] Message failed", error); + logger.error("[chat] Message failed", error, { + event: "leaf.slack_message_failed", + }); await finishLoading(target, loading, "Request failed."); await target.post({ markdown: "I could not complete that request. Please try again.", diff --git a/apps/leaf/src/lib/logger.ts b/apps/leaf/src/lib/logger.ts new file mode 100644 index 000000000..f831e6d16 --- /dev/null +++ b/apps/leaf/src/lib/logger.ts @@ -0,0 +1,60 @@ +import { + type AutumnLogger, + createAppLogger, + createSessionId, + createTraceId, +} from "@autumn/logging"; + +export const logger = createAppLogger({ + service: "leaf", + dataset: process.env.LEAF_LOG_DATASET ?? "leaf", + preset: "default", +}); + +export const createLeafSessionContext = ({ + channelId, + provider, + providerUserId, + threadId, + workspaceId, +}: { + channelId: string; + provider: string; + providerUserId: string; + threadId: string; + workspaceId: string; +}) => { + const traceId = createTraceId(); + const sessionId = createSessionId({ + parts: { + channelId, + provider, + threadId, + workspaceId, + }, + }); + return { + agentRunId: createTraceId(), + sessionId, + traceId, + context: { + provider, + provider_user_id: providerUserId, + session_id: sessionId, + trace_id: traceId, + slack_channel_id: channelId, + slack_thread_id: threadId, + slack_workspace_id: workspaceId, + }, + }; +}; + +export const addLeafContext = ( + baseLogger: AutumnLogger, + context: Record, +): AutumnLogger => + baseLogger.child({ + context: { + context, + }, + }); diff --git a/apps/leaf/src/main.ts b/apps/leaf/src/main.ts index 452cfacaa..ce9cfcc98 100644 --- a/apps/leaf/src/main.ts +++ b/apps/leaf/src/main.ts @@ -1,9 +1,9 @@ -import { createConsoleLogger } from "@autumn/mcp"; import type { HttpBindings } from "@hono/node-server"; import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { chatAdapterNames } from "./bot.js"; import { env } from "./lib/env.js"; +import { logger } from "./lib/logger.js"; import { registerMcpRoutes } from "./mcp/http.js"; import { slackRoutes } from "./providers/slack/routes.js"; @@ -22,7 +22,7 @@ registerMcpRoutes(app, { "oauth-enabled": true, "oauth-environment": env.MCP_OAUTH_ENVIRONMENT, "server-url": env.BETTER_AUTH_URL, - logger: createConsoleLogger("info"), + logger, }); app.route("/slack", slackRoutes); @@ -34,9 +34,12 @@ serve( port: env.PORT, }, ({ address, port }) => { - console.log("Chat listening", { - host: `${address}:${port}`, - adapters: chatAdapterNames, + logger.info("Chat listening", { + event: "leaf.server_started", + data: { + host: `${address}:${port}`, + adapters: chatAdapterNames, + }, }); }, ); diff --git a/apps/leaf/src/mcp/http.ts b/apps/leaf/src/mcp/http.ts index 9f5c38067..3186a630f 100644 --- a/apps/leaf/src/mcp/http.ts +++ b/apps/leaf/src/mcp/http.ts @@ -1,6 +1,7 @@ +import { randomUUID } from "node:crypto"; +import type { AutumnLogger } from "@autumn/logging"; import { buildAuthForRequest, - type ConsoleLogger, createAutumnOperationsMCPServer, getAuthorizationServerMetadata, getProtectedResourceMetadata, @@ -15,7 +16,7 @@ import type { Context, Hono } from "hono"; export interface McpRouteOptions extends MCPServerFlags { readonly "oauth-enabled": boolean; readonly "oauth-environment": OAuthEnvironment; - readonly logger: ConsoleLogger; + readonly logger: AutumnLogger; } type AppContext = Context<{ Bindings: HttpBindings }>; @@ -23,6 +24,8 @@ type McpPath = "/mcp"; type McpApp = Hono<{ Bindings: HttpBindings }>; export function registerMcpRoutes(app: McpApp, options: McpRouteOptions) { + const mcpServer = createAutumnOperationsMCPServer(); + app.get("/.well-known/oauth-protected-resource/mcp", (c) => c.json(getProtectedResourceMetadata(c.req.raw.headers, options, "/mcp")), ); @@ -64,14 +67,12 @@ export function registerMcpRoutes(app: McpApp, options: McpRouteOptions) { httpPath: path, req: c.env.incoming, res: c.env.outgoing, - options: { serverless: true }, + options: { sessionIdGenerator: randomUUID }, }); return RESPONSE_ALREADY_SENT; }; - app.all("/mcp", (c) => - handleMcp(c, "/mcp", createAutumnOperationsMCPServer()), - ); + app.all("/mcp", (c) => handleMcp(c, "/mcp", mcpServer)); return app; } diff --git a/apps/leaf/src/providers/slack/routes.ts b/apps/leaf/src/providers/slack/routes.ts index 748c5872b..f6e393036 100644 --- a/apps/leaf/src/providers/slack/routes.ts +++ b/apps/leaf/src/providers/slack/routes.ts @@ -2,6 +2,7 @@ import { verifyChatInstallState } from "@autumn/shared/utils/chatState"; import { Hono } from "hono"; import { z } from "zod"; import { bot } from "../../bot.js"; +import { logger } from "../../lib/logger.js"; import { getStateSecret, replaceInstallation } from "./installations.js"; import { exchangeSlackCode, slackErrorUrl, slackSuccessUrl } from "./oauth.js"; @@ -36,25 +37,38 @@ slackRoutes.get("/oauth/callback", async (c) => { .filter(Boolean), installedByProviderUserId: oauth.authed_user?.id, }); - console.info("[chat:slack] Installed", { - orgId: parsedState.orgId, - workspaceId: oauth.team.id, - workspaceName: oauth.team.name, + logger.info("[chat:slack] Installed", { + event: "leaf.slack_installed", + context: { + org_id: parsedState.orgId, + slack_workspace_id: oauth.team.id, + }, + data: { + workspace_name: oauth.team.name, + }, }); return c.redirect(slackSuccessUrl()); } catch (error) { - console.error("[chat:slack] OAuth callback failed", error); + logger.error("[chat:slack] OAuth callback failed", error, { + event: "leaf.slack_oauth_failed", + }); return c.redirect(slackErrorUrl("Slack install failed")); } }); slackRoutes.post("/events", (c) => { + logger.debug("Received Slack events request", { + event: "leaf.slack_events_request_received", + }); if (!bot.webhooks.slack) return c.text("Slack is not configured", 503); return bot.webhooks.slack(c.req.raw); }); slackRoutes.post("/interactions", (c) => { + logger.debug("Received Slack interactions request", { + event: "leaf.slack_interactions_request_received", + }); if (!bot.webhooks.slack) return c.text("Slack is not configured", 503); return bot.webhooks.slack(c.req.raw); }); diff --git a/apps/leaf/src/types.ts b/apps/leaf/src/types.ts index 92c3f937f..7a0dc1208 100644 --- a/apps/leaf/src/types.ts +++ b/apps/leaf/src/types.ts @@ -1,4 +1,5 @@ import { AppEnv, type ChatInstallation } from "@autumn/shared"; +import type { AutumnLogger } from "@autumn/logging"; import { z } from "zod"; export const agentOutputSchema = z.preprocess( @@ -62,6 +63,7 @@ export type SignatureArgs = { export type BotMessage = { installation: ChatInstallation; + logger?: AutumnLogger; onAction?: (message: string) => Promise | void; recentMessages?: ChatContextMessage[]; text: string; diff --git a/apps/leaf/tests/unit/lib/logger.test.ts b/apps/leaf/tests/unit/lib/logger.test.ts new file mode 100644 index 000000000..2e5bccf0d --- /dev/null +++ b/apps/leaf/tests/unit/lib/logger.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from "bun:test"; +import { createLeafSessionContext } from "../../../src/lib/logger.js"; + +describe("Leaf logger context", () => { + test("creates stable session ids and distinct trace ids", () => { + const first = createLeafSessionContext({ + channelId: "C1", + provider: "slack", + providerUserId: "U1", + threadId: "T1", + workspaceId: "W1", + }); + const second = createLeafSessionContext({ + channelId: "C1", + provider: "slack", + providerUserId: "U2", + threadId: "T1", + workspaceId: "W1", + }); + + expect(first.sessionId).toBe(second.sessionId); + expect(first.traceId).not.toBe(second.traceId); + expect(first.context).toMatchObject({ + provider: "slack", + session_id: first.sessionId, + trace_id: first.traceId, + slack_channel_id: "C1", + slack_thread_id: "T1", + slack_workspace_id: "W1", + }); + }); +}); diff --git a/apps/leaf/tsconfig.json b/apps/leaf/tsconfig.json index 9bd1abedd..39cf3a979 100644 --- a/apps/leaf/tsconfig.json +++ b/apps/leaf/tsconfig.json @@ -12,6 +12,7 @@ "paths": { "@autumn/shared": ["../../shared/index.ts"], "@autumn/shared/*": ["../../shared/*"], + "@autumn/logging": ["../../packages/logging/src/index.ts"], "@autumn/mcp/*": ["../../packages/mcp/*"], "@api/*": ["../../shared/api/*"], "@models/*": ["../../shared/models/*"], diff --git a/bun.lock b/bun.lock index 536fed665..c84d57504 100644 --- a/bun.lock +++ b/bun.lock @@ -88,6 +88,7 @@ "name": "@autumn/leaf", "version": "0.0.1", "dependencies": { + "@autumn/logging": "workspace:*", "@autumn/mcp": "workspace:*", "@autumn/shared": "workspace:*", "@chat-adapter/slack": "^4.29.0", @@ -284,10 +285,24 @@ "name": "@autumn/ksuid", "version": "1.0.0", }, + "packages/logging": { + "name": "@autumn/logging", + "version": "0.0.1", + "dependencies": { + "@axiomhq/pino": "^1.3.1", + "pino": "^9.6.0", + }, + "devDependencies": { + "@types/bun": "^1.2.13", + "@types/node": "^18.19.3", + "typescript": "~5.8.3", + }, + }, "packages/mcp": { "name": "@autumn/mcp", "version": "0.0.1", "dependencies": { + "@autumn/logging": "workspace:*", "@autumn/shared": "workspace:*", "@axiomhq/js": "^1.6.1", "@mastra/core": "^1.36.0", @@ -722,6 +737,8 @@ "@autumn/leaf": ["@autumn/leaf@workspace:apps/leaf"], + "@autumn/logging": ["@autumn/logging@workspace:packages/logging"], + "@autumn/mcp": ["@autumn/mcp@workspace:packages/mcp"], "@autumn/openapi": ["@autumn/openapi@workspace:packages/openapi"], @@ -6000,6 +6017,10 @@ "@autumn/leaf/@types/node": ["@types/node@25.8.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ=="], + "@autumn/logging/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@autumn/logging/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "@autumn/mcp/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@autumn/mcp/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], @@ -7770,6 +7791,8 @@ "@autumn/leaf/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "@autumn/logging/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@autumn/mcp/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@autumn/server/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], diff --git a/docker/Dockerfile b/docker/Dockerfile index 74a8aaeb7..5e8c83c38 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,6 +29,7 @@ COPY packages/atmn/package.json packages/atmn/ COPY packages/atmn-tests/package.json packages/atmn-tests/ COPY packages/autumn-js/package.json packages/autumn-js/ COPY packages/ksuid/package.json packages/ksuid/ +COPY packages/logging/package.json packages/logging/ COPY packages/mcp/package.json packages/mcp/ COPY packages/openapi/package.json packages/openapi/ COPY packages/sdk/package.json packages/sdk/ diff --git a/docker/dev.dockerfile b/docker/dev.dockerfile deleted file mode 100644 index 4fec00979..000000000 --- a/docker/dev.dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -# Multi-stage Dockerfile for Autumn development -FROM oven/bun:latest AS base - -WORKDIR /app - -# Skip Puppeteer Chromium download to speed up install -ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true -ENV PUPPETEER_SKIP_DOWNLOAD=true - -COPY package.json ./ -COPY bun.lock ./ -COPY shared/package*.json ./shared/ -COPY server/package*.json ./server/ -COPY vite/package*.json ./vite/ - -RUN bun install - -# Stage 1: /localtunnel -FROM base AS localtunnel -WORKDIR /app -COPY localtunnel-start.sh ./ -CMD ["sh", "localtunnel-start.sh"] - -# Stage 2: /vite -FROM base AS vite -COPY shared/ ./shared/ -WORKDIR /app/vite -COPY vite/ ./ -EXPOSE 3000 -CMD ["bun", "dev"] - -# Stage 3: /server -FROM base AS server -COPY shared/ ./shared/ -COPY server/ ./server/ -WORKDIR /app/server -EXPOSE 8080 -CMD ["bun", "dev"] - -# Stage 4: Workers -FROM base AS workers -COPY shared/ ./shared/ -COPY server/ ./server/ -WORKDIR /app/server -CMD ["bun", "workers:dev"] \ No newline at end of file diff --git a/package.json b/package.json index 31e81e561..f9c4ebdf1 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "apps/sdk-test", "packages/atmn", "packages/atmn-tests", + "packages/logging", "packages/mcp", "packages/sdk", "packages/autumn-js", @@ -115,6 +116,7 @@ "tb:prod-legacy": "bun scripts/tinybird/index.ts prod-legacy", "axiom": "ENV_FILE=.env infisical run --env=dev --recursive -- bun scripts/axiom/cli.ts", "axiom:prod": "ENV_FILE=.env.prod infisical run --env=prod --recursive -- bun scripts/axiom/cli.ts", + "add-mcp": "bun scripts/mcp/addMcp.ts", "trigger:deploy": "bunx trigger.dev deploy", "setupci": "node scripts/setup/setupci.js", "replicate": "bun scripts/db/replicate.ts", diff --git a/packages/logging/package.json b/packages/logging/package.json new file mode 100644 index 000000000..c4912dd34 --- /dev/null +++ b/packages/logging/package.json @@ -0,0 +1,30 @@ +{ + "name": "@autumn/logging", + "version": "0.0.1", + "author": "Autumn", + "type": "module", + "sideEffects": false, + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "README.md", + "src" + ], + "scripts": { + "build": "tsc", + "ts": "tsc --noEmit", + "test": "bun test tests/unit", + "prepack": "bun run build", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "@axiomhq/pino": "^1.3.1", + "pino": "^9.6.0" + }, + "devDependencies": { + "@types/bun": "^1.2.13", + "@types/node": "^18.19.3", + "typescript": "~5.8.3" + } +} diff --git a/packages/logging/src/context/addContextToLogs.ts b/packages/logging/src/context/addContextToLogs.ts new file mode 100644 index 000000000..45a7a63e9 --- /dev/null +++ b/packages/logging/src/context/addContextToLogs.ts @@ -0,0 +1,38 @@ +import type { AutumnLogger } from "../types.js"; +import type { + LogAppContext, + LogRequestContext, + LogTriggerContext, +} from "./types.js"; + +export const addRequestToLogs = ({ + logger, + requestContext, +}: { + logger: AutumnLogger; + requestContext: LogRequestContext; +}): AutumnLogger => logger.child({ context: { req: requestContext } }); + +export const addAppContextToLogs = ({ + logger, + appContext, +}: { + logger: AutumnLogger; + appContext: LogAppContext; +}): AutumnLogger => logger.child({ context: { context: appContext } }); + +export const addTriggerToLogs = ({ + logger, + triggerContext, +}: { + logger: AutumnLogger; + triggerContext: LogTriggerContext; +}): AutumnLogger => logger.child({ context: { trigger: triggerContext } }); + +export const addExtrasToLogs = ({ + logger, + extras, +}: { + logger: AutumnLogger; + extras: Record; +}): AutumnLogger => logger.child({ context: { extras } }); diff --git a/packages/logging/src/context/types.ts b/packages/logging/src/context/types.ts new file mode 100644 index 000000000..830a0a861 --- /dev/null +++ b/packages/logging/src/context/types.ts @@ -0,0 +1,35 @@ +export type LogRequestContext = { + id: string; + method: string; + url: string; + timestamp: number; + customer_id?: string; + entity_id?: string; + user_agent?: string; + ip_address?: string; + region?: string; + query: Record; + body: unknown; + name: string; +}; + +export type LogAppContext = { + org_id?: string; + org_slug?: string; + env?: string; + auth_type?: string; + customer_id?: string; + entity_id?: string; + user_id?: string; + user_email?: string; + api_version?: string; + scopes?: string[]; + full_subject_bucket?: number; + full_subject_rollout_enabled?: boolean; +}; + +export type LogTriggerContext = { + run_id: string; + task_id: string; + attempt_number?: number; +}; diff --git a/packages/logging/src/ids/createSessionId.ts b/packages/logging/src/ids/createSessionId.ts new file mode 100644 index 000000000..4cd3832a1 --- /dev/null +++ b/packages/logging/src/ids/createSessionId.ts @@ -0,0 +1,21 @@ +import { createHash } from "node:crypto"; + +const stableStringify = ({ value }: { value: unknown }): string => { + if (!value || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) + return `[${value.map((item) => stableStringify({ value: item })).join(",")}]`; + + return `{${Object.entries(value) + .sort(([a], [b]) => a.localeCompare(b)) + .map( + ([key, item]) => + `${JSON.stringify(key)}:${stableStringify({ value: item })}`, + ) + .join(",")}}`; +}; + +export const createSessionId = ({ parts }: { parts: unknown }): string => + createHash("sha256") + .update(stableStringify({ value: parts })) + .digest("hex") + .slice(0, 24); diff --git a/packages/logging/src/ids/createTraceId.ts b/packages/logging/src/ids/createTraceId.ts new file mode 100644 index 000000000..124381e7d --- /dev/null +++ b/packages/logging/src/ids/createTraceId.ts @@ -0,0 +1,3 @@ +import { randomUUID } from "node:crypto"; + +export const createTraceId = (): string => randomUUID(); diff --git a/packages/logging/src/index.ts b/packages/logging/src/index.ts new file mode 100644 index 000000000..fbf99ad76 --- /dev/null +++ b/packages/logging/src/index.ts @@ -0,0 +1,40 @@ +export { + addAppContextToLogs, + addExtrasToLogs, + addRequestToLogs, + addTriggerToLogs, +} from "./context/addContextToLogs.js"; +export type { + LogAppContext, + LogRequestContext, + LogTriggerContext, +} from "./context/types.js"; +export { createSessionId } from "./ids/createSessionId.js"; +export { createTraceId } from "./ids/createTraceId.js"; +export { + createAppLogger, + createAutumnLogger, +} from "./logger/autumnLogger.js"; +export { createConsoleLogger } from "./logger/consoleLogger.js"; +export { createLogger } from "./logger/createLogger.js"; +export { + mirrorLogger, + withLogPrefix, +} from "./logger/loggerWrappers.js"; +export { resolveLoggerOptions } from "./logger/resolveLoggerOptions.js"; +export { asAxiomMap } from "./payload/asAxiomMap.js"; +export { + type GuardLogPayloadOptions, + guardLogPayload, +} from "./payload/guardLogPayload.js"; +export type { + AutumnLogger, + ConsoleLogger, + ConsoleLoggerLevel, + CreateLoggerParams, + LoggerLevel, + LoggerOutput, + LoggerPreset, + PinoLogger, + ResolvedLoggerOptions, +} from "./types.js"; diff --git a/packages/logging/src/logger/autumnLogger.ts b/packages/logging/src/logger/autumnLogger.ts new file mode 100644 index 000000000..ccf946ac7 --- /dev/null +++ b/packages/logging/src/logger/autumnLogger.ts @@ -0,0 +1,69 @@ +import type pino from "pino"; +import type { + AutumnLogger, + ConsoleLoggerLevel, + CreateLoggerParams, + LogArgs, +} from "../types.js"; +import { createLogger } from "./createLogger.js"; + +const rewriteAppPath = (value: string): string => + value.replace("file:///app/", "./").replace(/\/app\//g, "./"); + +const errorToObject = (error: Error) => ({ + name: error.name, + message: error.message, + stack: error.stack ? rewriteAppPath(error.stack) : undefined, +}); + +const normalizeLogArgs = ({ args }: { args: LogArgs }) => { + const strings = args + .filter((arg): arg is string => typeof arg === "string") + .map(rewriteAppPath); + const objects = args + .filter( + (arg) => typeof arg !== "string" && arg !== null && arg !== undefined, + ) + .map((arg) => (arg instanceof Error ? { error: errorToObject(arg) } : arg)); + const error = args.find((arg): arg is Error => arg instanceof Error); + const message = + strings.at(-1) ?? + (error + ? rewriteAppPath(error.stack || error.message || "Error occurred") + : ""); + + return { + message, + merged: Object.assign({}, ...objects) as Record, + }; +}; + +const createLogMethod = + ({ method }: { method: pino.LogFn }) => + (...args: LogArgs) => { + const { message, merged } = normalizeLogArgs({ args }); + if (Object.keys(merged).length > 0) method(merged, message); + else method(message); + }; + +export const createAutumnLogger = ({ + logger, +}: { + logger: pino.Logger; +}): AutumnLogger => ({ + level: logger.level as ConsoleLoggerLevel, + debug: createLogMethod({ method: logger.debug.bind(logger) }), + info: createLogMethod({ method: logger.info.bind(logger) }), + warn: createLogMethod({ method: logger.warn.bind(logger) }), + warning: createLogMethod({ method: logger.warn.bind(logger) }), + error: createLogMethod({ method: logger.error.bind(logger) }), + child: ({ context, onlyProd = false }) => { + if (onlyProd && process.env.NODE_ENV !== "production") { + return createAutumnLogger({ logger }); + } + return createAutumnLogger({ logger: logger.child(context) }); + }, +}); + +export const createAppLogger = (params: CreateLoggerParams): AutumnLogger => + createAutumnLogger({ logger: createLogger(params) }); diff --git a/packages/logging/src/logger/consoleLogger.ts b/packages/logging/src/logger/consoleLogger.ts new file mode 100644 index 000000000..e6c5795b4 --- /dev/null +++ b/packages/logging/src/logger/consoleLogger.ts @@ -0,0 +1,28 @@ +import type { ConsoleLogger, ConsoleLoggerLevel, LogArgs } from "../types.js"; + +export const createConsoleLogger = ({ + level, +}: { + level: ConsoleLoggerLevel; +}): ConsoleLogger => { + const levels: ConsoleLoggerLevel[] = ["debug", "info", "warning", "error"]; + const min = levels.indexOf(level); + const noop = () => {}; + const log = + ({ method }: { method: "debug" | "info" | "warn" | "error" }) => + (...args: LogArgs) => { + console[method](...args); + }; + + const logger: ConsoleLogger = { + level, + debug: min <= 0 ? log({ method: "debug" }) : noop, + info: min <= 1 ? log({ method: "info" }) : noop, + warn: min <= 2 ? log({ method: "warn" }) : noop, + warning: min <= 2 ? log({ method: "warn" }) : noop, + error: min <= 3 ? log({ method: "error" }) : noop, + child: () => logger, + }; + + return logger; +}; diff --git a/packages/logging/src/logger/createLogger.ts b/packages/logging/src/logger/createLogger.ts new file mode 100644 index 000000000..ff4b08c20 --- /dev/null +++ b/packages/logging/src/logger/createLogger.ts @@ -0,0 +1,60 @@ +import pino from "pino"; +import { createConsoleJsonStream } from "../streams/consoleJsonStream.js"; +import { createPrettyLogStream } from "../streams/prettyLogStream.js"; +import type { CreateLoggerParams } from "../types.js"; +import { resolveLoggerOptions } from "./resolveLoggerOptions.js"; + +export const createLogger = (params: CreateLoggerParams): pino.Logger => { + const resolved = resolveLoggerOptions({ options: params }); + const axiomToken = params.axiomToken ?? process.env.AXIOM_TOKEN; + const axiomOrgId = params.axiomOrgId ?? process.env.AXIOM_ORG_ID; + const streams: pino.StreamEntry[] = []; + + for (const output of resolved.outputs) { + if (output === "console-pretty") { + streams.push({ + level: resolved.level, + stream: createPrettyLogStream({ + trailingNewline: resolved.preset !== "dual", + useConsoleLog: params.useConsoleLog ?? resolved.preset === "dual", + }), + }); + } + + if (output === "console-json") { + streams.push({ + level: resolved.level, + stream: createConsoleJsonStream(), + }); + } + + if (output === "axiom" && axiomToken) { + streams.push({ + level: resolved.level, + stream: pino.transport({ + target: "@axiomhq/pino", + options: { + dataset: resolved.dataset, + token: axiomToken, + orgId: axiomOrgId, + }, + }), + }); + } + } + + return pino( + { + level: resolved.level, + base: { + service: resolved.service, + ...(params.context ?? {}), + }, + mixin: params.mixin, + formatters: { + level: (label: string) => ({ level: label.toUpperCase() }), + }, + }, + pino.multistream(streams), + ); +}; diff --git a/packages/logging/src/logger/loggerWrappers.ts b/packages/logging/src/logger/loggerWrappers.ts new file mode 100644 index 000000000..08d51e1f4 --- /dev/null +++ b/packages/logging/src/logger/loggerWrappers.ts @@ -0,0 +1,71 @@ +import type { AutumnLogger, LogArgs } from "../types.js"; + +const logToStdout = ({ + level, + args, +}: { + level: "debug" | "info" | "warn" | "error"; + args: LogArgs; +}) => { + const method = + level === "debug" + ? console.debug + : level === "info" + ? console.info + : level === "warn" + ? console.warn + : console.error; + method(...args); +}; + +export const mirrorLogger = ({ + logger, +}: { + logger: AutumnLogger; +}): AutumnLogger => ({ + debug: (...args) => { + logger.debug(...args); + logToStdout({ level: "debug", args }); + }, + info: (...args) => { + logger.info(...args); + logToStdout({ level: "info", args }); + }, + warn: (...args) => { + logger.warn(...args); + logToStdout({ level: "warn", args }); + }, + warning: (...args) => { + logger.warn(...args); + logToStdout({ level: "warn", args }); + }, + error: (...args) => { + logger.error(...args); + logToStdout({ level: "error", args }); + }, + child: (params) => mirrorLogger({ logger: logger.child(params) }), +}); + +const prefixArgs = ({ prefix, args }: { prefix: string; args: LogArgs }) => { + if (typeof args[0] !== "string") return [prefix, ...args]; + if (args[0].startsWith(prefix)) return args; + return [`${prefix} ${args[0]}`, ...args.slice(1)]; +}; + +export const withLogPrefix = ({ + logger, + label, +}: { + logger: AutumnLogger; + label: string; +}): AutumnLogger => { + const prefix = `[${label}]`; + return { + debug: (...args) => logger.debug(...prefixArgs({ prefix, args })), + info: (...args) => logger.info(...prefixArgs({ prefix, args })), + warn: (...args) => logger.warn(...prefixArgs({ prefix, args })), + warning: (...args) => logger.warn(...prefixArgs({ prefix, args })), + error: (...args) => logger.error(...prefixArgs({ prefix, args })), + child: (params) => withLogPrefix({ logger: logger.child(params), label }), + }; +}; diff --git a/packages/logging/src/logger/resolveLoggerOptions.ts b/packages/logging/src/logger/resolveLoggerOptions.ts new file mode 100644 index 000000000..ddd79caa4 --- /dev/null +++ b/packages/logging/src/logger/resolveLoggerOptions.ts @@ -0,0 +1,67 @@ +import type { + CreateLoggerParams, + LoggerLevel, + LoggerOutput, + ResolvedLoggerOptions, +} from "../types.js"; + +const parseOutputs = ( + value: string | undefined, +): LoggerOutput[] | undefined => { + if (!value) return undefined; + const outputs = value + .split(",") + .map((part) => part.trim()) + .filter(Boolean); + + if ( + outputs.every( + (output): output is LoggerOutput => + output === "console-pretty" || + output === "console-json" || + output === "axiom", + ) + ) { + return outputs; + } + + return undefined; +}; + +export const resolveLoggerOptions = ({ + options, + env = process.env, +}: { + options: CreateLoggerParams; + env?: NodeJS.ProcessEnv; +}): ResolvedLoggerOptions => { + const preset = options.preset ?? "default"; + const isDevOrTest = env.NODE_ENV === "development" || env.NODE_ENV === "test"; + const hasAxiomToken = Boolean(options.axiomToken ?? env.AXIOM_TOKEN); + + let outputs = options.outputs ?? parseOutputs(env.LOG_OUTPUTS); + if (!outputs) { + if (preset === "console-only") outputs = ["console-pretty"]; + else if (preset === "axiom-only") outputs = ["axiom"]; + else if (preset === "dual") + outputs = [isDevOrTest ? "console-pretty" : "console-json", "axiom"]; + else if (isDevOrTest) outputs = ["console-pretty", "axiom"]; + else outputs = ["axiom"]; + } + + const filteredOutputs = outputs.filter( + (output) => output !== "axiom" || hasAxiomToken, + ); + + return { + service: options.service, + dataset: options.dataset ?? options.service, + preset, + level: + options.level ?? + ((env.LOG_LEVEL as LoggerLevel | undefined) || + (isDevOrTest || preset === "dual" ? "debug" : "info")), + outputs: filteredOutputs.length > 0 ? filteredOutputs : ["console-pretty"], + hasAxiomToken, + }; +}; diff --git a/packages/logging/src/payload/asAxiomMap.ts b/packages/logging/src/payload/asAxiomMap.ts new file mode 100644 index 000000000..7c73aa94c --- /dev/null +++ b/packages/logging/src/payload/asAxiomMap.ts @@ -0,0 +1,8 @@ +export const asAxiomMap = ({ + value, +}: { + value: unknown; +}): Record => + value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : { value }; diff --git a/packages/logging/src/payload/guardLogPayload.ts b/packages/logging/src/payload/guardLogPayload.ts new file mode 100644 index 000000000..64171178e --- /dev/null +++ b/packages/logging/src/payload/guardLogPayload.ts @@ -0,0 +1,147 @@ +const defaultMaxPayloadBytes = 512_000; +const defaultTruncateAboveBytes = 4_000; +const defaultMaxArrayItems = 5; +const defaultMaxStringLength = 500; +const defaultMaxDepth = 6; + +export type GuardLogPayloadOptions = { + maxPayloadBytes?: number; + truncateAboveBytes?: number; + maxArrayItems?: number; + maxStringLength?: number; + maxDepth?: number; +}; + +const envNumber = ({ + value, + fallback, +}: { + value?: string; + fallback: number; +}) => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + +const resolveOptions = ({ + options = {}, +}: { + options?: GuardLogPayloadOptions; +}) => ({ + maxPayloadBytes: + options.maxPayloadBytes ?? + envNumber({ + value: process.env.LOG_MAX_PAYLOAD_BYTES, + fallback: defaultMaxPayloadBytes, + }), + truncateAboveBytes: + options.truncateAboveBytes ?? + envNumber({ + value: process.env.LOG_TRUNCATE_ABOVE_BYTES, + fallback: defaultTruncateAboveBytes, + }), + maxArrayItems: + options.maxArrayItems ?? + envNumber({ + value: process.env.LOG_MAX_ARRAY_ITEMS, + fallback: defaultMaxArrayItems, + }), + maxStringLength: + options.maxStringLength ?? + envNumber({ + value: process.env.LOG_MAX_STRING_LENGTH, + fallback: defaultMaxStringLength, + }), + maxDepth: options.maxDepth ?? defaultMaxDepth, +}); + +type ResolvedGuardOptions = ReturnType; + +const truncateString = ({ + value, + maxStringLength, +}: { + value: string; + maxStringLength: number; +}): string => + value.length > maxStringLength + ? `${value.slice(0, maxStringLength)}...[+${value.length - maxStringLength} chars]` + : value; + +const truncateValue = ({ + value, + options, + depth = 0, +}: { + value: unknown; + options: ResolvedGuardOptions; + depth?: number; +}): unknown => { + if (typeof value === "string") + return truncateString({ + value, + maxStringLength: options.maxStringLength, + }); + if (!value || typeof value !== "object") return value; + + if (depth >= options.maxDepth) { + if (Array.isArray(value)) return `...[${value.length} items]`; + return "...[object]"; + } + + if (Array.isArray(value)) { + const kept = value.slice(0, options.maxArrayItems).map((item) => + truncateValue({ + value: item, + options, + depth: depth + 1, + }), + ); + if (value.length > options.maxArrayItems) { + kept.push(`...[+${value.length - options.maxArrayItems} more items]`); + } + return kept; + } + + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack, + }; + } + + const out: Record = {}; + for (const [key, item] of Object.entries(value)) { + out[key] = truncateValue({ + value: item, + options, + depth: depth + 1, + }); + } + return out; +}; + +export const guardLogPayload = ({ + value, + options: guardOptions, +}: { + value: unknown; + options?: GuardLogPayloadOptions; +}): unknown => { + if (value === undefined) return undefined; + const options = resolveOptions({ options: guardOptions }); + try { + const json = JSON.stringify(value); + if (!json || json.length <= options.truncateAboveBytes) return value; + + const truncated = truncateValue({ value, options }); + const truncatedJson = JSON.stringify(truncated); + if (truncatedJson && truncatedJson.length > options.maxPayloadBytes) { + return { _truncated: true, _bytes: truncatedJson.length }; + } + return truncated; + } catch { + return { _unserializable: true }; + } +}; diff --git a/packages/logging/src/streams/consoleJsonStream.ts b/packages/logging/src/streams/consoleJsonStream.ts new file mode 100644 index 000000000..a204dde79 --- /dev/null +++ b/packages/logging/src/streams/consoleJsonStream.ts @@ -0,0 +1,9 @@ +import { Writable } from "node:stream"; + +export const createConsoleJsonStream = () => + new Writable({ + write(chunk, _encoding, callback) { + console.log(chunk.toString().trimEnd()); + callback(); + }, + }); diff --git a/packages/logging/src/streams/prettyLogStream.ts b/packages/logging/src/streams/prettyLogStream.ts new file mode 100644 index 000000000..f6300014e --- /dev/null +++ b/packages/logging/src/streams/prettyLogStream.ts @@ -0,0 +1,116 @@ +import { Writable } from "node:stream"; + +const FORMATTED_LOG_EXCLUDE_FIELDS = new Set([ + "time", + "level", + "msg", + "pid", + "hostname", + "req", + "res", + "statusCode", + "body", + "query", + "durationMs", + "duration_ms", + "context", + "workflow", + "trigger", + "stripe_event", + "vercel_event", + "worker", + "extras", + "type", + "data", + "aws", + "service", +]); + +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + white: "\x1b[37m", + gray: "\x1b[90m", + bgRed: "\x1b[41m", +}; + +const levelColors: Record = { + 10: colors.gray, + 20: colors.blue, + 30: colors.green, + 40: colors.yellow, + 50: colors.red, + 60: colors.bgRed, + TRACE: colors.gray, + DEBUG: colors.blue, + INFO: colors.green, + WARN: colors.yellow, + ERROR: colors.red, + FATAL: colors.bgRed, +}; + +const levelNames: Record = { + 10: "TRACE", + 20: "DEBUG", + 30: "INFO", + 40: "WARN", + 50: "ERROR", + 60: "FATAL", + TRACE: "TRACE", + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", +}; + +export const createPrettyLogStream = ({ + trailingNewline = true, + useConsoleLog = false, +}: { + trailingNewline?: boolean; + useConsoleLog?: boolean; +} = {}) => + new Writable({ + write(chunk, _encoding, callback) { + try { + const log = JSON.parse(chunk.toString()); + const timestamp = new Date(log.time) + .toISOString() + .replace("T", " ") + .replace("Z", ""); + const level = log.level; + const levelColor = levelColors[level] || colors.white; + const levelName = + levelNames[level] || (typeof level === "string" ? level : "UNKNOWN"); + let message = log.msg || ""; + + const additionalFields = Object.keys(log) + .filter((key) => !FORMATTED_LOG_EXCLUDE_FIELDS.has(key)) + .reduce( + (acc, key) => { + acc[key] = log[key]; + return acc; + }, + {} as Record, + ); + + if (Object.keys(additionalFields).length > 0) { + message += ` ${JSON.stringify(additionalFields, null, 2)}`; + } + + const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}${trailingNewline ? "\n" : ""}`; + if (useConsoleLog) console.log(formattedLog); + else process.stdout.write(formattedLog); + callback(); + } catch { + if (useConsoleLog) console.log(chunk.toString()); + else process.stdout.write(chunk); + callback(); + } + }, + }); diff --git a/packages/logging/src/types.ts b/packages/logging/src/types.ts new file mode 100644 index 000000000..0a46ee93d --- /dev/null +++ b/packages/logging/src/types.ts @@ -0,0 +1,56 @@ +import type pino from "pino"; + +export type LoggerOutput = "console-pretty" | "console-json" | "axiom"; +export type LoggerPreset = "default" | "dual" | "console-only" | "axiom-only"; +export type LoggerLevel = + | "trace" + | "debug" + | "info" + | "warn" + | "error" + | "fatal"; + +export type CreateLoggerParams = { + service: string; + dataset?: string; + level?: LoggerLevel; + preset?: LoggerPreset; + outputs?: LoggerOutput[]; + context?: Record; + mixin?: () => Record; + axiomToken?: string; + axiomOrgId?: string; + useConsoleLog?: boolean; +}; + +export type ResolvedLoggerOptions = Required< + Pick +> & { + dataset: string; + level: LoggerLevel; + outputs: LoggerOutput[]; + hasAxiomToken: boolean; +}; + +export type LogArgs = unknown[]; + +export type AutumnLogger = { + level?: string; + debug: (...args: LogArgs) => void; + info: (...args: LogArgs) => void; + warn: (...args: LogArgs) => void; + warning: (...args: LogArgs) => void; + error: (...args: LogArgs) => void; + child: (params: { + context: Record; + onlyProd?: boolean; + }) => AutumnLogger; +}; + +export type ConsoleLoggerLevel = "debug" | "info" | "warning" | "error"; + +export type ConsoleLogger = AutumnLogger & { + level: ConsoleLoggerLevel; +}; + +export type PinoLogger = pino.Logger; diff --git a/packages/logging/tsconfig.json b/packages/logging/tsconfig.json new file mode 100644 index 000000000..c4f77bb62 --- /dev/null +++ b/packages/logging/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "allowJs": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "checkJs": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "exactOptionalPropertyTypes": false, + "forceConsistentCasingInFileNames": true, + "incremental": false, + "isolatedModules": true, + "lib": ["es2024"], + "module": "Preserve", + "moduleResolution": "bundler", + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": false, + "noImplicitReturns": false, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noEmit": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2022", + "types": ["bun", "node"], + "useUnknownInCatchVariables": true + }, + "exclude": ["node_modules"], + "include": ["src/**/*.ts", "tests/**/*.ts"] +} diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 4976ab7f5..afddc2a5d 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -20,6 +20,7 @@ "prepublishOnly": "bun run build" }, "dependencies": { + "@autumn/logging": "workspace:*", "@autumn/shared": "workspace:*", "@axiomhq/js": "^1.6.1", "@mastra/core": "^1.36.0", diff --git a/packages/mcp/src/agent/axiom.ts b/packages/mcp/src/agent/axiom.ts index 3f59587b3..dcfcd640a 100644 --- a/packages/mcp/src/agent/axiom.ts +++ b/packages/mcp/src/agent/axiom.ts @@ -28,8 +28,10 @@ const defaultEndTime = "now"; const maxRangeMs = ms.days(7); const searchMaxRangeMs = ms.hours(1); +type AutumnOrg = { id: string; slug?: string | undefined }; + let axiomClient: Axiom | null = null; -const orgCache = new Map(); +const orgCache = new Map(); const getAxiomClient = () => { if (!process.env.AXIOM_ADMIN_TOKEN) { @@ -85,9 +87,15 @@ const assertCanUseAxiom = (auth: AutumnMcpAuth) => { } }; -export const resolveAutumnOrgId = async (auth: AutumnMcpAuth) => { - if (auth.orgId) return auth.orgId; - +/** + * Resolves the Autumn org (id + slug) for an authenticated request. Cached + * (~5min) per credential. Unlike `resolveAutumnOrgId`, this always hits + * `/v1/organization` when uncached so the slug is available — the id alone may + * already be on `auth`, but the slug never is. + */ +export const resolveAutumnOrg = async ( + auth: AutumnMcpAuth, +): Promise => { const cacheKey = [ auth.serverURL ?? "https://api.useautumn.com", auth.env, @@ -96,7 +104,7 @@ export const resolveAutumnOrgId = async (auth: AutumnMcpAuth) => { String(auth.failOpen), ].join(":"); const cached = orgCache.get(cacheKey); - if (cached && isFuture(cached.expiresAt)) return cached.orgId; + if (cached && isFuture(cached.expiresAt)) return cached.org; const client = createAutumnClient(auth); const response = await fetch(new URL("/v1/organization", client.baseUrl), { @@ -107,17 +115,26 @@ export const resolveAutumnOrgId = async (auth: AutumnMcpAuth) => { throw new Error("Could not resolve Autumn organization for MCP request."); } - const body = (await response.json()) as { id?: unknown }; + const body = (await response.json()) as { id?: unknown; slug?: unknown }; if (typeof body.id !== "string" || !body.id) { throw new Error("Autumn organization response did not include an id."); } + const org: AutumnOrg = { + id: body.id, + slug: typeof body.slug === "string" ? body.slug : undefined, + }; orgCache.set(cacheKey, { - orgId: body.id, + org, expiresAt: addMilliseconds(new Date(), ms.minutes(5)), }); - return body.id; + return org; +}; + +export const resolveAutumnOrgId = async (auth: AutumnMcpAuth) => { + if (auth.orgId) return auth.orgId; + return (await resolveAutumnOrg(auth)).id; }; export const prepareAxiomQuery = ({ diff --git a/packages/mcp/src/analytics/analyticsSink.ts b/packages/mcp/src/analytics/analyticsSink.ts index 397325f81..928754031 100644 --- a/packages/mcp/src/analytics/analyticsSink.ts +++ b/packages/mcp/src/analytics/analyticsSink.ts @@ -1,5 +1,5 @@ import type { AnalyticsSink } from "./analyticsTypes.js"; -import { createAxiomAnalyticsSink } from "./axiomSink.js"; +import { createLoggerAnalyticsSink } from "./loggerSink.js"; const DEFAULT_DATASET = "leaf"; @@ -23,7 +23,7 @@ export const setAnalyticsSink = (sink: AnalyticsSink | null | undefined) => { export const getAnalyticsSink = (): AnalyticsSink => { if (overrideSink !== undefined) return overrideSink ?? noopSink; if (cachedSink === undefined) { - cachedSink = createAxiomAnalyticsSink({ + cachedSink = createLoggerAnalyticsSink({ token: process.env.AXIOM_TOKEN, orgId: process.env.AXIOM_ORG_ID, dataset: process.env.MCP_ANALYTICS_DATASET ?? DEFAULT_DATASET, diff --git a/packages/mcp/src/analytics/analyticsTypes.ts b/packages/mcp/src/analytics/analyticsTypes.ts index 55d3cfb22..7307a3557 100644 --- a/packages/mcp/src/analytics/analyticsTypes.ts +++ b/packages/mcp/src/analytics/analyticsTypes.ts @@ -7,21 +7,33 @@ */ export type McpAnalyticsSurface = "mcp" | "agent"; +/** + * Org/auth context for a tool call. Mirrors the server's `context.*` log shape + * (see server/src/utils/logging) so MCP analytics and agent logs unify cleanly. + */ +export type McpAnalyticsContext = { + /** Autumn org id. Resolved lazily; may be absent if resolution fails. */ + orgId?: string | undefined; + /** Autumn org slug. Resolved lazily; may be absent if resolution fails. */ + orgSlug?: string | undefined; + env: string; + scopes?: string[] | undefined; +}; + export type McpAnalyticsEvent = { event: "mcp.tool_call"; surface: McpAnalyticsSurface; tool: string; + /** One-sentence statement of what the caller is trying to do. */ + intent?: string | undefined; status: "ok" | "error"; durationMs: number; principalId: string; - env: string; - /** Resolved lazily; may be absent if org resolution fails. */ - orgId?: string | undefined; /** HTTP User-Agent of the calling MCP client. Absent for `agent` surface. */ client?: string | undefined; - /** Stateless session grouping: hash(principal + client + time window). */ + /** MCP transport session id, or fallback hash(principal + client + window). */ sessionId: string; - scopes?: string[] | undefined; + context: McpAnalyticsContext; /** Tool request payload (stored as an Axiom map field). */ input?: unknown; /** Tool result payload (stored as an Axiom map field). */ @@ -32,8 +44,8 @@ export type McpAnalyticsEvent = { /** * Pluggable destination for analytics events. Implementations must be * non-blocking: `emit` runs on the hot path of every tool call and must never - * throw or await network I/O inline. Swap this (Axiom direct, `@axiomhq/pino`, - * an OTEL exporter, a test spy) without touching the instrumentation layer. + * throw or await network I/O inline. Swap this (pino/Axiom, an OTEL exporter, + * a test spy) without touching the instrumentation layer. */ export interface AnalyticsSink { emit(event: McpAnalyticsEvent): void; diff --git a/packages/mcp/src/analytics/axiomSink.ts b/packages/mcp/src/analytics/axiomSink.ts deleted file mode 100644 index 92577d8df..000000000 --- a/packages/mcp/src/analytics/axiomSink.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Axiom } from "@axiomhq/js"; -import type { AnalyticsSink, McpAnalyticsEvent } from "./analyticsTypes.js"; - -const maxPayloadBytes = - Number(process.env.MCP_ANALYTICS_MAX_PAYLOAD_BYTES) || 512_000; - -/** - * Map fields require an object value. Wrap scalars/arrays so heterogeneous - * tool outputs still land in a single Axiom map field instead of conflicting - * on type. - */ -const asMap = (value: unknown): Record => - value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : { value }; - -/** - * Keep individual events under Axiom's 1MB field cap. Oversized payloads are - * replaced with a marker rather than dropping the whole (otherwise rejected) - * event. - */ -const guardPayload = (value: unknown): unknown => { - if (value === undefined) return undefined; - try { - const json = JSON.stringify(value); - if (json && json.length > maxPayloadBytes) { - return { _truncated: true, _bytes: json.length }; - } - return value; - } catch { - return { _unserializable: true }; - } -}; - -const toAxiomRecord = (event: McpAnalyticsEvent) => ({ - _time: new Date().toISOString(), - event: event.event, - surface: event.surface, - tool: event.tool, - status: event.status, - duration_ms: event.durationMs, - org_id: event.orgId, - principal_id: event.principalId, - env: event.env, - client: event.client, - session_id: event.sessionId, - scopes: event.scopes, - // Map fields — see scripts/axiom/createLeafDataset.ts - input: asMap(guardPayload(event.input)), - output: asMap(guardPayload(event.output)), - error: event.error, -}); - -export const createAxiomAnalyticsSink = ({ - token, - orgId, - dataset, -}: { - token?: string | undefined; - orgId?: string | undefined; - dataset: string; -}): AnalyticsSink | null => { - if (!token) return null; - const client = new Axiom({ token, orgId }); - return { - emit(event) { - // Axiom batches internally; no inline await on the hot path. - client.ingest(dataset, [toAxiomRecord(event)]); - }, - flush: () => client.flush(), - }; -}; diff --git a/packages/mcp/src/analytics/emitToolEvent.ts b/packages/mcp/src/analytics/emitToolEvent.ts index ecbaadc79..86a342968 100644 --- a/packages/mcp/src/analytics/emitToolEvent.ts +++ b/packages/mcp/src/analytics/emitToolEvent.ts @@ -1,4 +1,4 @@ -import { resolveAutumnOrgId } from "../agent/axiom.js"; +import { resolveAutumnOrg } from "../agent/axiom.js"; import type { AutumnMcpAuth } from "../server/auth/auth.js"; import { getAnalyticsSink } from "./analyticsSink.js"; import type { McpAnalyticsSurface } from "./analyticsTypes.js"; @@ -14,6 +14,8 @@ export const emitMcpToolEvent = ({ toolId, auth, client, + transportSessionId, + intent, status, durationMs, input, @@ -24,6 +26,8 @@ export const emitMcpToolEvent = ({ toolId: string; auth: AutumnMcpAuth; client: string | undefined; + transportSessionId?: string | undefined; + intent?: string | undefined; status: "ok" | "error"; durationMs: number; input?: unknown; @@ -32,33 +36,40 @@ export const emitMcpToolEvent = ({ }) => { const sink = getAnalyticsSink(); - // Resolve org off the hot path; resolveAutumnOrgId is cached (~5min). + // Resolve org off the hot path; resolveAutumnOrg is cached (~5min). void (async () => { let orgId = auth.orgId; - if (!orgId) { - try { - orgId = await resolveAutumnOrgId(auth); - } catch { - // Best-effort: emit without org_id rather than dropping the event. - } + let orgSlug: string | undefined; + try { + const org = await resolveAutumnOrg(auth); + orgId = org.id; + orgSlug = org.slug; + } catch { + // Best-effort: emit without org context rather than dropping the event. } const now = Date.now(); sink.emit({ event: "mcp.tool_call", surface, tool: toolId, + intent, status, durationMs, - orgId, principalId: auth.principalId, - env: auth.env, client, - sessionId: deriveSessionId({ - principalId: auth.principalId, - client, - now, - }), - scopes: auth.scopes, + sessionId: + transportSessionId ?? + deriveSessionId({ + principalId: auth.principalId, + client, + now, + }), + context: { + orgId, + orgSlug, + env: auth.env, + scopes: auth.scopes, + }, input, output, error, diff --git a/packages/mcp/src/analytics/index.ts b/packages/mcp/src/analytics/index.ts index 15106380b..7c2769d98 100644 --- a/packages/mcp/src/analytics/index.ts +++ b/packages/mcp/src/analytics/index.ts @@ -8,5 +8,8 @@ export type { McpAnalyticsEvent, McpAnalyticsSurface, } from "./analyticsTypes.js"; -export { createAxiomAnalyticsSink } from "./axiomSink.js"; export { instrumentToolsWithAnalytics } from "./instrumentTools.js"; +export { + createAxiomAnalyticsSink, + createLoggerAnalyticsSink, +} from "./loggerSink.js"; diff --git a/packages/mcp/src/analytics/instrumentTools.ts b/packages/mcp/src/analytics/instrumentTools.ts index 28e4c18fb..a7c989391 100644 --- a/packages/mcp/src/analytics/instrumentTools.ts +++ b/packages/mcp/src/analytics/instrumentTools.ts @@ -1,5 +1,6 @@ import type { createTool } from "@mastra/core/tools"; import { type AutumnMcpAuth, getAutumnAuth } from "../server/auth/auth.js"; +import { getIntent } from "../tools/utils/intent.js"; import { isAnalyticsEnabled } from "./analyticsSink.js"; import type { McpAnalyticsSurface } from "./analyticsTypes.js"; import { emitMcpToolEvent } from "./emitToolEvent.js"; @@ -7,7 +8,9 @@ import { emitMcpToolEvent } from "./emitToolEvent.js"; type AnyTool = ReturnType; type ToolContext = Parameters>[1]; -const getClientFromContext = (context: ToolContext): string | undefined => { +const getHeadersFromContext = ( + context: ToolContext, +): Record | undefined => { const extra = ( context as { mcp?: { @@ -17,7 +20,19 @@ const getClientFromContext = (context: ToolContext): string | undefined => { }; } )?.mcp?.extra; - return extra?.requestInfo?.headers?.["user-agent"]; + return extra?.requestInfo?.headers; +}; + +const getHeader = ( + headers: Record | undefined, + name: string, +): string | undefined => { + const direct = headers?.[name] ?? headers?.[name.toLowerCase()]; + if (direct) return direct; + const entry = Object.entries(headers ?? {}).find( + ([key]) => key.toLowerCase() === name.toLowerCase(), + ); + return entry?.[1]; }; const extractRequest = (input: unknown): unknown => @@ -30,8 +45,9 @@ const extractRequest = (input: unknown): unknown => * read from the same MCP context the tools already use, so an unauthenticated * call simply skips analytics (it would have failed in the tool anyway). * - * Tools are created fresh per request (see `createAutumnOperationsMCPServer`), - * so mutating `execute` here carries no shared-state risk. + * Tools are wrapped once when the MCP server is created. The wrapper keeps no + * per-request mutable state; auth/session data is read from the execution + * context for each tool call. * * @param tools The toolset to instrument (mutated in place and returned). * @param surface Origin of the calls — `mcp` (external clients) or `agent` @@ -59,7 +75,10 @@ export const instrumentToolsWithAnalytics = < } catch { return original(input as never, context as never); } - const client = getClientFromContext(context); + const headers = getHeadersFromContext(context); + const client = getHeader(headers, "user-agent"); + const transportSessionId = getHeader(headers, "mcp-session-id"); + const intent = getIntent(input); try { const output = await original(input as never, context as never); emitMcpToolEvent({ @@ -67,6 +86,8 @@ export const instrumentToolsWithAnalytics = < toolId, auth, client, + transportSessionId, + intent, status: "ok", durationMs: Date.now() - started, input: extractRequest(input), @@ -79,6 +100,8 @@ export const instrumentToolsWithAnalytics = < toolId, auth, client, + transportSessionId, + intent, status: "error", durationMs: Date.now() - started, input: extractRequest(input), diff --git a/packages/mcp/src/analytics/loggerSink.ts b/packages/mcp/src/analytics/loggerSink.ts new file mode 100644 index 000000000..6fb15c4fc --- /dev/null +++ b/packages/mcp/src/analytics/loggerSink.ts @@ -0,0 +1,60 @@ +import { asAxiomMap, createLogger, guardLogPayload } from "@autumn/logging"; +import type { AnalyticsSink, McpAnalyticsEvent } from "./analyticsTypes.js"; + +const toLoggerRecord = (event: McpAnalyticsEvent) => ({ + _time: new Date().toISOString(), + event: event.event, + surface: event.surface, + tool: event.tool, + intent: event.intent, + status: event.status, + duration_ms: event.durationMs, + principal_id: event.principalId, + client: event.client, + session_id: event.sessionId, + context: { + org_id: event.context.orgId, + org_slug: event.context.orgSlug, + env: event.context.env, + scopes: event.context.scopes, + }, + input: asAxiomMap({ value: guardLogPayload({ value: event.input }) }), + output: asAxiomMap({ value: guardLogPayload({ value: event.output }) }), + error: event.error, +}); + +export const createLoggerAnalyticsSink = ({ + token, + orgId, + dataset, +}: { + token?: string | undefined; + orgId?: string | undefined; + dataset: string; +}): AnalyticsSink | null => { + if (!token) return null; + const logger = createLogger({ + service: "mcp", + dataset, + preset: "axiom-only", + outputs: ["axiom"], + axiomToken: token, + axiomOrgId: orgId, + }); + + return { + emit(event) { + logger.info(toLoggerRecord(event), "MCP tool call"); + }, + flush: async () => { + await new Promise((resolve) => { + const flush = logger.flush; + if (typeof flush !== "function") return resolve(); + flush.call(logger, () => resolve()); + }); + }, + }; +}; + +/** @deprecated Use createLoggerAnalyticsSink. */ +export const createAxiomAnalyticsSink = createLoggerAnalyticsSink; diff --git a/packages/mcp/src/analytics/sessionId.ts b/packages/mcp/src/analytics/sessionId.ts index f6318f3a0..717386cfd 100644 --- a/packages/mcp/src/analytics/sessionId.ts +++ b/packages/mcp/src/analytics/sessionId.ts @@ -7,10 +7,9 @@ const hash = (value: string) => createHash("sha256").update(value).digest("hex").slice(0, 32); /** - * Stateless session grouping. The serverless MCP transport issues no - * Mcp-Session-Id, so we synthesize one from the principal + client + a coarse - * time bucket — calls from the same client within the window collapse into one - * session. + * Fallback session grouping. Stateful MCP clients send Mcp-Session-Id; when it + * is absent, synthesize a coarse principal/client bucket so calls from the same + * client within the window still collapse into one session. */ export const deriveSessionId = ({ principalId, diff --git a/packages/mcp/src/server/auth/oauth.ts b/packages/mcp/src/server/auth/oauth.ts index 974800201..fae23f87d 100644 --- a/packages/mcp/src/server/auth/oauth.ts +++ b/packages/mcp/src/server/auth/oauth.ts @@ -1,6 +1,5 @@ import { ms } from "@autumn/shared/unixUtils"; import { addMilliseconds, isFuture } from "date-fns"; -import type { ConsoleLogger } from "../../console-logger.js"; import { MCP_OAUTH_SCOPES } from "../../constants.js"; import type { AutumnMcpAuth } from "./auth.js"; import { OAuthHttpError } from "./utils/errors.js"; @@ -37,6 +36,10 @@ type ExchangedToken = { scopes?: string[] | undefined; }; +type AuthLogger = { + warning: (message: string, data?: Record) => void; +}; + const apiKeyCache = new Map(); const exchangeOAuthToken = async ({ @@ -132,7 +135,7 @@ export const getAuthorizationServerMetadata = (flags: MCPOAuthFlags) => { export const buildAuthForRequest = async ( headers: Headers, flags: MCPOAuthFlags, - logger: ConsoleLogger, + logger: AuthLogger, resourcePath = "/mcp", ): Promise => { const env = getEnvironment({ headers, flags }); diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index d7b76401f..3e723430e 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -18,6 +18,7 @@ import { rawLocalPreviewTool, toTools, } from "./utils/factories.js"; +import { requireIntentOnTools } from "./utils/intent.js"; import type { ConfirmedWriteToolName, ToolDomain } from "./utils/types.js"; export { dateToEpochMillisecondsTool } from "./utils/dates.js"; @@ -62,14 +63,16 @@ const confirmedWrites = domains.flatMap( */ export const createRawAutumnOperationTools = () => instrumentToolsWithAnalytics({ - tools: { + // Require a one-sentence `intent` on every external tool call so we can + // see what clients are actually trying to do (captured in analytics). + tools: requireIntentOnTools({ ...toTools(operations, operationTool), ...toTools(billingPreviews, (config) => operationTool({ ...config, endpoint: config.previewEndpoint }), ), ...toTools(localPreviews, rawLocalPreviewTool), ...toTools(confirmedWrites, operationTool), - }, + }), surface: "mcp", }); diff --git a/packages/mcp/src/tools/utils/intent.ts b/packages/mcp/src/tools/utils/intent.ts new file mode 100644 index 000000000..0b72b97a5 --- /dev/null +++ b/packages/mcp/src/tools/utils/intent.ts @@ -0,0 +1,47 @@ +import type { createTool } from "@mastra/core/tools"; +import * as z from "zod/v4"; + +type AnyTool = ReturnType; + +export const INTENT_DESCRIPTION = + "Required. One concise sentence, in plain language, describing what the user " + + "asked you (the agent) to do — their original request in their own terms, " + + "not a restatement of the arguments or the tool name. If this call is one " + + 'step toward a larger ask, state that larger ask. Example: "Find customers ' + + 'on the Pro plan so we can email them about the new add-on."'; + +/** Required single-sentence statement of what the caller is trying to do. */ +export const intentSchema = z.string().min(1).describe(INTENT_DESCRIPTION); + +/** Reads the `intent` string out of a tool input without casting. */ +export const getIntent = (input: unknown): string | undefined => + input && + typeof input === "object" && + "intent" in input && + typeof input.intent === "string" + ? input.intent + : undefined; + +/** + * Adds a required `intent` field to every tool's input schema, in place, so + * external MCP clients must declare their goal on every call. Call this once on + * a fully-built toolset (the intent is captured by the analytics layer). + * + * Tools whose input isn't a plain object are left untouched. + */ +export const requireIntentOnTools = >( + tools: T, +): T => { + for (const tool of Object.values(tools)) { + const schema = tool.inputSchema; + if (schema instanceof z.ZodObject) { + // Runtime value is a plain zod object, but Mastra types the field as its + // JSON-schema-augmented schema (incompatible at the type level only), so + // route the reassignment through `unknown`. + tool.inputSchema = schema.extend({ + intent: intentSchema, + }) as unknown as typeof tool.inputSchema; + } + } + return tools; +}; diff --git a/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts b/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts index 07bf3f28e..fd874f169 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts @@ -104,7 +104,10 @@ describe("Autumn operation tools", () => { await expect( tool.execute( - { request: { customer_id: "cus_1", email: "charlie@example.com" } }, + { + intent: "create a customer", + request: { customer_id: "cus_1", email: "charlie@example.com" }, + }, { mcp: { extra: { authInfo: auth } } } as never, ), ).resolves.toEqual({ id: "cus_1" }); @@ -129,9 +132,12 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("createPlan is not executable"); await expect( - tool.execute({ request: { plan_id: "pro", name: "Pro" } }, { - mcp: { extra: { authInfo: auth } }, - } as never), + tool.execute( + { intent: "create a plan", request: { plan_id: "pro", name: "Pro" } }, + { + mcp: { extra: { authInfo: auth } }, + } as never, + ), ).resolves.toEqual({ id: "pro" }); } finally { globalThis.fetch = originalFetch; @@ -157,6 +163,7 @@ describe("Autumn operation tools", () => { await expect( tool.execute( { + intent: "create a schedule", request: { customer_id: "cus_1", phases: [{ starts_at: Date.now(), plans: [{ plan_id: "pro" }] }], @@ -189,7 +196,7 @@ describe("Autumn operation tools", () => { throw new Error("previewCreateBalance is not executable"); await expect( - tool.execute({ request }, { + tool.execute({ intent: "preview a balance grant", request }, { mcp: { extra: { authInfo: auth } }, } as never), ).resolves.toMatchObject({ @@ -222,6 +229,7 @@ describe("Autumn operation tools", () => { await expect( tool.execute( { + intent: "grant a balance", request: { customer_id: "cus_1", entity_id: "workspace_1", @@ -259,6 +267,7 @@ describe("Autumn operation tools", () => { await expect( tool.execute( { + intent: "preview a schedule", request: { customer_id: "cus_1", phases: [{ starts_at: Date.now(), plans: [{ plan_id: "pro" }] }], @@ -288,9 +297,15 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("listCustomers is not executable"); await expect( - tool.execute({ request: { limit: 5000, search: "charlie" } }, { - mcp: { extra: { authInfo: auth } }, - } as never), + tool.execute( + { + intent: "list customers", + request: { limit: 5000, search: "charlie" }, + }, + { + mcp: { extra: { authInfo: auth } }, + } as never, + ), ).resolves.toEqual({ customers: [] }); } finally { globalThis.fetch = originalFetch; @@ -317,9 +332,15 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("previewAttach is not executable"); await expect( - tool.execute({ request: { customer_id: "cus_1", plan_id: "pro" } }, { - mcp: { extra: { authInfo: auth } }, - } as never), + tool.execute( + { + intent: "preview an attach", + request: { customer_id: "cus_1", plan_id: "pro" }, + }, + { + mcp: { extra: { authInfo: auth } }, + } as never, + ), ).resolves.toEqual({ total: 50 }); await expect(claimLatestPendingAction(auth)).rejects.toThrow( "No pending", @@ -346,9 +367,15 @@ describe("Autumn operation tools", () => { if (!tool.execute) throw new Error("attach is not executable"); await expect( - tool.execute({ request: { customer_id: "cus_1", plan_id: "pro" } }, { - mcp: { extra: { authInfo: auth } }, - } as never), + tool.execute( + { + intent: "attach a plan", + request: { customer_id: "cus_1", plan_id: "pro" }, + }, + { + mcp: { extra: { authInfo: auth } }, + } as never, + ), ).resolves.toEqual({ ok: true }); } finally { globalThis.fetch = originalFetch; diff --git a/packages/mcp/tests/unit/mcp-server/analytics.test.ts b/packages/mcp/tests/unit/mcp-server/analytics.test.ts new file mode 100644 index 000000000..7b9d77666 --- /dev/null +++ b/packages/mcp/tests/unit/mcp-server/analytics.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, test } from "bun:test"; +import { createTool } from "@mastra/core/tools"; +import * as z from "zod/v4"; +import { + instrumentToolsWithAnalytics, + type McpAnalyticsEvent, + setAnalyticsSink, +} from "../../../src/analytics/index.js"; +import type { AutumnMcpAuth } from "../../../src/server/auth/auth.js"; + +const auth: AutumnMcpAuth = { + apiKey: "sk_test", + env: "sandbox", + principalId: "user_1", + resource: "http://localhost:2718/mcp", + scopes: ["billing:read"], + serverURL: "http://localhost:8080", +}; + +const waitForEvent = async (events: McpAnalyticsEvent[]) => { + for (let i = 0; i < 20; i++) { + if (events.length > 0) return events[0]; + await new Promise((resolve) => setTimeout(resolve, 5)); + } + throw new Error("Timed out waiting for analytics event"); +}; + +describe("MCP analytics instrumentation", () => { + test("emits successful tool calls", async () => { + const events: McpAnalyticsEvent[] = []; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + Response.json({ id: "org_1", slug: "acme" })) as unknown as typeof fetch; + setAnalyticsSink({ + emit: (event: McpAnalyticsEvent) => events.push(event), + flush: async () => {}, + }); + + try { + const tools = instrumentToolsWithAnalytics({ + surface: "mcp", + tools: { + echo: createTool({ + id: "echo", + description: "Echo input", + inputSchema: z.object({ intent: z.string(), request: z.unknown() }), + execute: async ({ request }) => ({ request }), + }), + }, + }); + + await expect( + tools.echo.execute?.({ intent: "echo input", request: { ok: true } }, { + mcp: { + extra: { + authInfo: auth, + requestInfo: { + headers: { + "mcp-session-id": "mcp_session_1", + "user-agent": "Claude Code", + }, + }, + }, + }, + } as never), + ).resolves.toEqual({ request: { ok: true } }); + + await expect(waitForEvent(events)).resolves.toMatchObject({ + event: "mcp.tool_call", + surface: "mcp", + tool: "echo", + intent: "echo input", + status: "ok", + principalId: "user_1", + client: "Claude Code", + sessionId: "mcp_session_1", + context: { + orgId: "org_1", + orgSlug: "acme", + env: "sandbox", + }, + input: { ok: true }, + output: { request: { ok: true } }, + }); + } finally { + setAnalyticsSink(undefined); + globalThis.fetch = originalFetch; + } + }); + + test("emits errors and rethrows", async () => { + const events: McpAnalyticsEvent[] = []; + setAnalyticsSink({ + emit: (event: McpAnalyticsEvent) => events.push(event), + flush: async () => {}, + }); + + try { + const tools = instrumentToolsWithAnalytics({ + surface: "agent", + tools: { + fail: createTool({ + id: "fail", + description: "Fail input", + inputSchema: z.object({ intent: z.string() }), + execute: async () => { + throw new Error("nope"); + }, + }), + }, + }); + + await expect( + tools.fail.execute?.({ intent: "fail intentionally" }, { + mcp: { extra: { authInfo: auth } }, + } as never), + ).rejects.toThrow("nope"); + + await expect(waitForEvent(events)).resolves.toMatchObject({ + surface: "agent", + tool: "fail", + intent: "fail intentionally", + status: "error", + error: "nope", + }); + } finally { + setAnalyticsSink(undefined); + } + }); +}); diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json index d9d31a185..457afcd7c 100644 --- a/packages/mcp/tsconfig.json +++ b/packages/mcp/tsconfig.json @@ -33,6 +33,7 @@ "@api/*": ["../../shared/api/*"], "@models/*": ["../../shared/models/*"], "@utils/*": ["../../shared/utils/*"], + "@autumn/logging": ["../logging/src/index.ts"], "@autumn/ksuid": ["../ksuid/src/index.ts"] }, "useUnknownInCatchVariables": true, diff --git a/scripts/axiom/createLeafDataset.ts b/scripts/axiom/createLeafDataset.ts index f5f88ab4c..c791c9523 100644 --- a/scripts/axiom/createLeafDataset.ts +++ b/scripts/axiom/createLeafDataset.ts @@ -1,14 +1,12 @@ /** - * Idempotently provisions the Axiom `leaf` dataset used for MCP usage - * analytics (events emitted from packages/mcp `tool.execute`), and configures - * its map fields. + * Idempotently provisions the Axiom `leaf` dataset used for Leaf runtime logs + * and MCP usage analytics, and configures its map fields. * * Map fields ("vacuum" the unpredictable nested payloads into a single column): - * MCP tool `input`/`output` payloads have an open-ended shape — every distinct - * arg key would otherwise become its own mapped field and quickly blow Axiom's - * per-dataset field limit. Declaring `input` and `output` as map fields stores - * their nested keys inside one field each, so they never count toward the limit - * while staying queryable (e.g. `where input.customer_id == '...'`). + * Tool payloads, req/res bodies, and per-log details have open-ended shape. + * Every distinct top-level key would otherwise become its own mapped field and + * quickly blow Axiom's per-dataset field limit. These map fields keep nested + * keys inside one field each while staying queryable. * * Run via the Axiom CLI (resolves AXIOM_ADMIN_TOKEN from infisical): * bun axiom create-leaf # dev @@ -17,17 +15,25 @@ * Notes: * - AXIOM_ADMIN_TOKEN must be a personal API token with dataset create/update * scope, NOT the `xaat-` ingest token used at runtime. - * - Safe to re-run: dataset creation tolerates "already exists", and the map - * field list is declared via PUT (full replace), so re-running converges. + * - Safe to re-run: dataset creation tolerates "already exists", and existing + * map fields are read before missing fields are created. */ const AXIOM_BASE = "https://api.axiom.co/v2"; const DATASET = "leaf"; -const DATASET_DESCRIPTION = "Leaf app MCP usage analytics (per tool.execute)"; +const DATASET_DESCRIPTION = "Leaf runtime logs and MCP usage analytics"; // Nested, open-ended payloads stored as map fields to stay under the field // limit. Keep this list minimal — only genuinely high-cardinality objects. -const MAP_FIELDS = ["input", "output"]; +const MAP_FIELDS = [ + "context", + "data", + "extras", + "input", + "output", + "req", + "res", +]; const authHeaders = (token: string) => ({ Authorization: `Bearer ${token}`, @@ -59,7 +65,42 @@ const createDataset = async (token: string) => { throw new Error(`Failed to create dataset: ${res.status} ${text}`); }; -const setMapField = async (token: string, name: string) => { +const getMapFields = async (token: string) => { + const res = await fetch( + `${AXIOM_BASE}/datasets/${encodeURIComponent(DATASET)}/mapfields`, + { + method: "GET", + headers: authHeaders(token), + }, + ); + + const text = await res.text(); + if (!res.ok) { + throw new Error(`Failed to list map fields: ${res.status} ${text}`); + } + + const parsed = JSON.parse(text) as unknown; + if (!Array.isArray(parsed) || parsed.some((name) => typeof name !== "string")) { + throw new Error(`Unexpected map fields response: ${text}`); + } + + return new Set(parsed); +}; + +const setMapField = async ({ + existing, + name, + token, +}: { + existing: Set; + name: string; + token: string; +}) => { + if (existing.has(name)) { + console.log(` = map field: ${name} (already set)`); + return; + } + const res = await fetch( `${AXIOM_BASE}/datasets/${encodeURIComponent(DATASET)}/mapfields`, { @@ -69,13 +110,15 @@ const setMapField = async (token: string, name: string) => { }, ); - // Re-declaring an existing map field returns a 4xx mentioning existence. const text = await res.text(); if (res.ok) { + existing.add(name); console.log(` + map field: ${name}`); return; } + if (/exist/i.test(text)) { + existing.add(name); console.log(` = map field: ${name} (already set)`); return; } @@ -84,8 +127,9 @@ const setMapField = async (token: string, name: string) => { }; const setMapFields = async (token: string) => { + const existing = await getMapFields(token); for (const name of MAP_FIELDS) { - await setMapField(token, name); + await setMapField({ existing, name, token }); } }; diff --git a/scripts/mcp/addMcp.ts b/scripts/mcp/addMcp.ts new file mode 100644 index 000000000..3b76d3e41 --- /dev/null +++ b/scripts/mcp/addMcp.ts @@ -0,0 +1,91 @@ +/** + * Registers the Autumn MCP server with local AI CLIs (Claude Code + Codex). + * + * Usage: + * bun add-mcp # autumn-dev -> http://localhost:3099/mcp + * bun add-mcp # custom name / url + * + * Only CLIs that are actually installed are touched; the rest are skipped. + * The server uses OAuth, so you authenticate on first connect (Claude prompts + * automatically; for Codex run `codex mcp login `). + */ + +const DEFAULT_NAME = "autumn-dev"; +const DEFAULT_URL = "http://localhost:3099/mcp"; + +type Client = { + label: string; + bin: string; + /** Args to remove an existing server of this name (best-effort, ignored). */ + removeArgs: (name: string) => string[]; + /** Args to add the streamable-HTTP server. */ + addArgs: (name: string, url: string) => string[]; + /** Follow-up the user must run/do (e.g. OAuth login). */ + next: (name: string) => string; +}; + +const clients: Client[] = [ + { + label: "Claude Code", + bin: "claude", + removeArgs: (name) => ["mcp", "remove", name], + addArgs: (name, url) => ["mcp", "add", "--transport", "http", name, url], + next: () => "Claude prompts for OAuth automatically on first use.", + }, + { + label: "Codex", + bin: "codex", + removeArgs: (name) => ["mcp", "remove", name], + addArgs: (name, url) => ["mcp", "add", name, "--url", url], + next: (name) => + `Run \`codex mcp login ${name}\` to authenticate (OAuth). If the handshake fails, retry with \`-c experimental_use_rmcp_client=true\`.`, + }, +]; + +const run = (bin: string, args: string[]) => { + const proc = Bun.spawnSync([bin, ...args], { + stdout: "pipe", + stderr: "pipe", + }); + const output = `${proc.stdout.toString()}${proc.stderr.toString()}`.trim(); + return { ok: proc.exitCode === 0, output }; +}; + +const addToClient = (client: Client, name: string, url: string) => { + if (!Bun.which(client.bin)) { + console.log(`- ${client.label}: skipped (\`${client.bin}\` not found)`); + return; + } + + // Remove any existing entry first so re-running converges cleanly. + run(client.bin, client.removeArgs(name)); + + const { ok, output } = run(client.bin, client.addArgs(name, url)); + if (ok) { + console.log(`+ ${client.label}: added \`${name}\` -> ${url}`); + console.log(` next: ${client.next(name)}`); + return; + } + + console.log(`! ${client.label}: failed to add \`${name}\``); + if (output) console.log(` ${output.replaceAll("\n", "\n ")}`); +}; + +const main = () => { + const [, , nameArg, urlArg] = process.argv; + if (nameArg === "--help" || nameArg === "-h") { + console.log("Usage: bun add-mcp [name] [url]"); + console.log(`Defaults: ${DEFAULT_NAME} ${DEFAULT_URL}`); + return; + } + + const name = nameArg ?? DEFAULT_NAME; + const url = urlArg ?? DEFAULT_URL; + + console.log(`Registering MCP server \`${name}\` (${url})\n`); + for (const client of clients) { + addToClient(client, name, url); + } +}; + +main(); From 01fa3a311fdce5d14ee5c3676ef6954600bb1cc2 Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 15:11:12 +0100 Subject: [PATCH 06/12] latest --- ai | 2 +- shared/db/auth-schema.ts | 1 + shared/drizzle/0005_fresh_runaways.sql | 1 + shared/drizzle/meta/0005_snapshot.json | 7359 ++++++++++++++++++++++++ shared/drizzle/meta/_journal.json | 7 + 5 files changed, 7369 insertions(+), 1 deletion(-) create mode 100644 shared/drizzle/0005_fresh_runaways.sql create mode 100644 shared/drizzle/meta/0005_snapshot.json diff --git a/ai b/ai index 0e52f71fb..db7737aca 160000 --- a/ai +++ b/ai @@ -1 +1 @@ -Subproject commit 0e52f71fbd69e7a4a58b63863a8c4929bfd9ebf8 +Subproject commit db7737aca7d9d613fcc9a49f8aa8690253505e03 diff --git a/shared/db/auth-schema.ts b/shared/db/auth-schema.ts index 3a1a3d902..56d0e5f89 100644 --- a/shared/db/auth-schema.ts +++ b/shared/db/auth-schema.ts @@ -215,6 +215,7 @@ export const oauthRefreshToken = pgTable("oauth_refresh_token", { expiresAt: timestamp("expires_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }), revoked: timestamp("revoked", { withTimezone: true }), + authTime: timestamp("auth_time", { withTimezone: true }), scopes: text("scopes").array().notNull(), }).enableRLS(); diff --git a/shared/drizzle/0005_fresh_runaways.sql b/shared/drizzle/0005_fresh_runaways.sql new file mode 100644 index 000000000..b9d89b2fb --- /dev/null +++ b/shared/drizzle/0005_fresh_runaways.sql @@ -0,0 +1 @@ +ALTER TABLE "oauth_refresh_token" ADD COLUMN "auth_time" timestamp with time zone; \ No newline at end of file diff --git a/shared/drizzle/meta/0005_snapshot.json b/shared/drizzle/meta/0005_snapshot.json new file mode 100644 index 000000000..b7b081947 --- /dev/null +++ b/shared/drizzle/meta/0005_snapshot.json @@ -0,0 +1,7359 @@ +{ + "id": "3ee43a45-bd02-43e2-a2d1-2080d51b5674", + "prevId": "20fbfba1-ef02-4637-b7f8-4ae1ee5983d7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.actions": { + "name": "actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_actions_on_internal_entity_id": { + "name": "idx_actions_on_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "actions_org_id_fkey": { + "name": "actions_org_id_fkey", + "tableFrom": "actions", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "actions_customer_id_fkey": { + "name": "actions_customer_id_fkey", + "tableFrom": "actions", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "actions_entity_id_fkey": { + "name": "actions_entity_id_fkey", + "tableFrom": "actions", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hashed_key": { + "name": "hashed_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_org_id_fkey": { + "name": "api_keys_org_id_fkey", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_hashed_key_key": { + "name": "api_keys_hashed_key_key", + "nullsNotDistinct": false, + "columns": [ + "hashed_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_topup_limit_states": { + "name": "auto_topup_limit_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchase_window_ends_at": { + "name": "purchase_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "purchase_count": { + "name": "purchase_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempt_window_ends_at": { + "name": "attempt_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_attempt_window_ends_at": { + "name": "failed_attempt_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "failed_attempt_count": { + "name": "failed_attempt_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "last_failed_attempt_at": { + "name": "last_failed_attempt_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + } + }, + "indexes": { + "auto_topup_limits_org_env_internal_customer_feature_unique": { + "name": "auto_topup_limits_org_env_internal_customer_feature_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_topup_limits_org_id_fkey": { + "name": "auto_topup_limits_org_id_fkey", + "tableFrom": "auto_topup_limit_states", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_topup_limits_internal_customer_id_fkey": { + "name": "auto_topup_limits_internal_customer_id_fkey", + "tableFrom": "auto_topup_limit_states", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_approvals": { + "name": "chat_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_ts": { + "name": "message_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_args": { + "name": "tool_args", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "preview": { + "name": "preview", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "decided_at": { + "name": "decided_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "decided_by_provider_user_id": { + "name": "decided_by_provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_approvals_org_id_fkey": { + "name": "chat_approvals_org_id_fkey", + "tableFrom": "chat_approvals", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_installations": { + "name": "chat_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bot_access_token": { + "name": "bot_access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_env": { + "name": "default_env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_api_key_id": { + "name": "sandbox_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_api_key": { + "name": "sandbox_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_api_key_id": { + "name": "live_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_api_key": { + "name": "live_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_by_user_id": { + "name": "installed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_by_provider_user_id": { + "name": "installed_by_provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_installations_org_id_fkey": { + "name": "chat_installations_org_id_fkey", + "tableFrom": "chat_installations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_installations_org_provider_key": { + "name": "chat_installations_org_provider_key", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "provider" + ] + }, + "chat_installations_provider_workspace_key": { + "name": "chat_installations_provider_workspace_key", + "nullsNotDistinct": false, + "columns": [ + "provider", + "workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_results": { + "name": "chat_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.checkouts": { + "name": "checkouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "params_version": { + "name": "params_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_checkouts_stripe_invoice_id": { + "name": "idx_checkouts_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_entitlements": { + "name": "customer_entitlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "customer_product_id": { + "name": "customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entitlement_id": { + "name": "entitlement_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unlimited": { + "name": "unlimited", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "next_reset_at": { + "name": "next_reset_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "usage_allowed": { + "name": "usage_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "adjustment": { + "name": "adjustment", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "additional_balance": { + "name": "additional_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "cache_version": { + "name": "cache_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_entitlements_product_id": { + "name": "idx_customer_entitlements_product_id", + "columns": [ + { + "expression": "customer_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_internal_customer_id": { + "name": "idx_customer_entitlements_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "idx_customer_entitlements_internal_customer_id_btree": { + "name": "idx_customer_entitlements_internal_customer_id_btree", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_entitlement_id": { + "name": "idx_customer_entitlements_entitlement_id", + "columns": [ + { + "expression": "entitlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_internal_entity_id": { + "name": "idx_customer_entitlements_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "idx_customer_entitlements_on_next_reset_at": { + "name": "idx_customer_entitlements_on_next_reset_at", + "columns": [ + { + "expression": "next_reset_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_loose_customer_expires": { + "name": "idx_customer_entitlements_loose_customer_expires", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customer_entitlements\".\"customer_product_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlements_internal_feature_id_fkey": { + "name": "entitlements_internal_feature_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "features", + "columnsFrom": [ + "internal_feature_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_entitlements_internal_entity_id_fkey": { + "name": "customer_entitlements_internal_entity_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_entitlements_customer_product_id_fkey": { + "name": "customer_entitlements_customer_product_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "customer_products", + "columnsFrom": [ + "customer_product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "customer_entitlements_entitlement_id_fkey": { + "name": "customer_entitlements_entitlement_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "entitlements", + "columnsFrom": [ + "entitlement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_prices": { + "name": "customer_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_product_id": { + "name": "customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_prices_product_id": { + "name": "idx_customer_prices_product_id", + "columns": [ + { + "expression": "customer_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_prices_price_id": { + "name": "idx_customer_prices_price_id", + "columns": [ + { + "expression": "price_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_prices_internal_customer_id": { + "name": "idx_customer_prices_internal_customer_id", + "columns": [ + { + "expression": "\"internal_customer_id\" COLLATE \"C\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customer_prices\".\"internal_customer_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_prices_customer_product_id_fkey": { + "name": "customer_prices_customer_product_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "customer_products", + "columnsFrom": [ + "customer_product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_prices_internal_customer_id_fkey": { + "name": "customer_prices_internal_customer_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_prices_price_id_fkey": { + "name": "customer_prices_price_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "prices", + "columnsFrom": [ + "price_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_products": { + "name": "customer_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "canceled": { + "name": "canceled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "access_starts_at": { + "name": "access_starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "free_trial_id": { + "name": "free_trial_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "billing_cycle_anchor_resets_at": { + "name": "billing_cycle_anchor_resets_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "collection_method": { + "name": "collection_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'charge_automatically'" + }, + "subscription_ids": { + "name": "subscription_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scheduled_ids": { + "name": "scheduled_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_version": { + "name": "billing_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_version": { + "name": "api_version", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "api_semver": { + "name": "api_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_customer_product_id": { + "name": "previous_customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_trial_end": { + "name": "on_trial_end", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_products_customer_status": { + "name": "idx_customer_products_customer_status", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_on_internal_entity_id": { + "name": "idx_customer_products_on_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_on_internal_product_id": { + "name": "idx_customer_products_on_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_subscription_ids": { + "name": "idx_customer_products_subscription_ids", + "columns": [ + { + "expression": "subscription_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customer_products_scheduled_ids": { + "name": "idx_customer_products_scheduled_ids", + "columns": [ + { + "expression": "scheduled_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customer_products_stripe_checkout_session_id": { + "name": "idx_customer_products_stripe_checkout_session_id", + "columns": [ + { + "expression": "stripe_checkout_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_revenuecat_processor": { + "name": "idx_customer_products_revenuecat_processor", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "(\"customer_products\".\"processor\" ->> 'type') = 'revenuecat'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_products_free_trial_id_fkey": { + "name": "customer_products_free_trial_id_fkey", + "tableFrom": "customer_products", + "tableTo": "free_trials", + "columnsFrom": [ + "free_trial_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "customer_products_internal_customer_id_fkey": { + "name": "customer_products_internal_customer_id_fkey", + "tableFrom": "customer_products", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "customer_products_internal_product_id_fkey": { + "name": "customer_products_internal_product_id_fkey", + "tableFrom": "customer_products", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "customer_products_internal_entity_id_fkey": { + "name": "customer_products_internal_entity_id_fkey", + "tableFrom": "customer_products", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processors": { + "name": "processors", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "send_email_receipts": { + "name": "send_email_receipts", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_topups": { + "name": "auto_topups", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "spend_limits": { + "name": "spend_limits", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "overage_allowed": { + "name": "overage_allowed", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "customers_email_null_id_unique": { + "name": "customers_email_null_id_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"customers\".\"id\" IS NULL AND \"customers\".\"email\" IS NOT NULL AND \"customers\".\"email\" != ''", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_org_env_fingerprint": { + "name": "idx_customers_org_env_fingerprint", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"fingerprint\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processor_id": { + "name": "idx_customers_processor_id", + "columns": [ + { + "expression": "(\"processor\" ->> 'id')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_composite": { + "name": "idx_customers_composite", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_org_env_internal_id": { + "name": "idx_customers_org_env_internal_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"internal_id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_email_trgm": { + "name": "idx_customers_email_trgm", + "columns": [ + { + "expression": "\"email\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_name_trgm": { + "name": "idx_customers_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_id_trgm": { + "name": "idx_customers_id_trgm", + "columns": [ + { + "expression": "\"id\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"id\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_org_id_env_created_at": { + "name": "idx_customers_org_id_env_created_at", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_cursor": { + "name": "idx_customers_cursor", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processors_revenuecat": { + "name": "idx_customers_processors_revenuecat", + "columns": [ + { + "expression": "(\"processors\" ->> 'revenuecat')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processors_vercel": { + "name": "idx_customers_processors_vercel", + "columns": [ + { + "expression": "(\"processors\" ->> 'vercel')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_org_id_fkey": { + "name": "customers_org_id_fkey", + "tableFrom": "customers", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cus_id_constraint": { + "name": "cus_id_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "id", + "env" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.entities": { + "name": "entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spend_limits": { + "name": "spend_limits", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "overage_allowed": { + "name": "overage_allowed", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_entities_internal_customer_id": { + "name": "idx_entities_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_customer_internal_desc": { + "name": "idx_entities_customer_internal_desc", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"internal_id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_org_env_id": { + "name": "idx_entities_org_env_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_cursor": { + "name": "idx_entities_cursor", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entities_internal_customer_id_fkey": { + "name": "entities_internal_customer_id_fkey", + "tableFrom": "entities", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entities_internal_feature_id_fkey": { + "name": "entities_internal_feature_id_fkey", + "tableFrom": "entities", + "tableTo": "features", + "columnsFrom": [ + "internal_feature_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entities_org_id_fkey": { + "name": "entities_org_id_fkey", + "tableFrom": "entities", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entity_id_constraint": { + "name": "entity_id_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "env", + "internal_customer_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entitlements": { + "name": "entitlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_reward_id": { + "name": "internal_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "allowance_type": { + "name": "allowance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowance": { + "name": "allowance", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "interval": { + "name": "interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_count": { + "name": "interval_count", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "carry_from_previous": { + "name": "carry_from_previous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entity_feature_id": { + "name": "entity_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "expiry_duration": { + "name": "expiry_duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_length": { + "name": "expiry_length", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "rollover": { + "name": "rollover", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_entitlements_internal_product_id": { + "name": "idx_entitlements_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_internal_reward_id": { + "name": "idx_entitlements_internal_reward_id", + "columns": [ + { + "expression": "internal_reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_reward_feature": { + "name": "idx_entitlements_reward_feature", + "columns": [ + { + "expression": "internal_reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_feature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_internal_reward_id_c_partial": { + "name": "idx_entitlements_internal_reward_id_c_partial", + "columns": [ + { + "expression": "\"internal_reward_id\" COLLATE \"C\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"entitlements\".\"internal_reward_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlements_internal_feature_id_fkey": { + "name": "entitlements_internal_feature_id_fkey", + "tableFrom": "entitlements", + "tableTo": "features", + "columnsFrom": [ + "internal_feature_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entitlements_internal_product_id_fkey": { + "name": "entitlements_internal_product_id_fkey", + "tableFrom": "entitlements", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "entitlements_internal_reward_id_fkey": { + "name": "entitlements_internal_reward_id_fkey", + "tableFrom": "entitlements", + "tableTo": "rewards", + "columnsFrom": [ + "internal_reward_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entitlements_id_key": { + "name": "entitlements_id_key", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "set_usage": { + "name": "set_usage", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deductions": { + "name": "deductions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_events_internal_customer_id": { + "name": "idx_events_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_events_internal_entity_id": { + "name": "idx_events_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_events_customer_non_usage_ts": { + "name": "idx_events_customer_non_usage_ts", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"timestamp\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"events\".\"set_usage\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_internal_customer_id_fkey": { + "name": "events_internal_customer_id_fkey", + "tableFrom": "events", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_event_constraint": { + "name": "unique_event_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "env", + "customer_id", + "event_name", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.features": { + "name": "features", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "display": { + "name": "display", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "event_names": { + "name": "event_names", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "features_org_id_fkey": { + "name": "features_org_id_fkey", + "tableFrom": "features", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "feature_id_constraint": { + "name": "feature_id_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "id", + "env" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_trials": { + "name": "free_trials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'day'" + }, + "length": { + "name": "length", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unique_fingerprint": { + "name": "unique_fingerprint", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "card_required": { + "name": "card_required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "on_end": { + "name": "on_end", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_free_trials_internal_product_id": { + "name": "idx_free_trials_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_trials_internal_product_id_fkey": { + "name": "free_trials_internal_product_id_fkey", + "tableFrom": "free_trials", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organizations_id_fk": { + "name": "invitation_organization_id_organizations_id_fk", + "tableFrom": "invitation", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.invoice_line_items": { + "name": "invoice_line_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_item_id": { + "name": "stripe_invoice_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_item_id": { + "name": "stripe_subscription_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_product_id": { + "name": "stripe_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_discountable": { + "name": "stripe_discountable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "amount_after_discounts": { + "name": "amount_after_discounts", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "stripe_quantity": { + "name": "stripe_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "total_quantity": { + "name": "total_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "paid_quantity": { + "name": "paid_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_source": { + "name": "description_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_timing": { + "name": "billing_timing", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prorated": { + "name": "prorated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_product_ids": { + "name": "customer_product_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_price_ids": { + "name": "customer_price_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_entitlement_ids": { + "name": "customer_entitlement_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_period_start": { + "name": "effective_period_start", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "effective_period_end": { + "name": "effective_period_end", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "discounts": { + "name": "discounts", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "invoice_line_items_invoice_id_fkey": { + "name": "invoice_line_items_invoice_id_fkey", + "tableFrom": "invoice_line_items", + "tableTo": "invoices", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoice_line_items_stripe_id_unique": { + "name": "invoice_line_items_stripe_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_templates": { + "name": "invoice_templates", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "footer": { + "name": "footer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "net_terms_days": { + "name": "net_terms_days", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_invoice_templates_org_id": { + "name": "idx_invoice_templates_org_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoice_templates_org_id_fkey": { + "name": "invoice_templates_org_id_fkey", + "tableFrom": "invoice_templates", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoice_templates_id_unique": { + "name": "invoice_templates_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoices": { + "name": "invoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "product_ids": { + "name": "product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "internal_product_ids": { + "name": "internal_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_type": { + "name": "processor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "hosted_invoice_url": { + "name": "hosted_invoice_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total": { + "name": "total", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "refunded_amount": { + "name": "refunded_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "discounts": { + "name": "discounts", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "items": { + "name": "items", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "idx_invoices_customer_created": { + "name": "idx_invoices_customer_created", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_internal_entity_id": { + "name": "idx_invoices_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"invoices\".\"internal_entity_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoices_internal_customer_id_fkey": { + "name": "invoices_internal_customer_id_fkey", + "tableFrom": "invoices", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invoices_internal_entity_id_fkey": { + "name": "invoices_internal_entity_id_fkey", + "tableFrom": "invoices", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoices_stripe_id_key": { + "name": "invoices_stripe_id_key", + "nullsNotDistinct": false, + "columns": [ + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organizations_id_fk": { + "name": "member_organization_id_organizations_id_fk", + "tableFrom": "member", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_errors": { + "name": "migration_errors", + "schema": "", + "columns": { + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_job_id": { + "name": "migration_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "migration_customers_internal_customer_id_fkey": { + "name": "migration_customers_internal_customer_id_fkey", + "tableFrom": "migration_errors", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_customers_migration_job_id_fkey": { + "name": "migration_customers_migration_job_id_fkey", + "tableFrom": "migration_errors", + "tableTo": "migration_jobs", + "columnsFrom": [ + "migration_job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "migration_errors_pkey": { + "name": "migration_errors_pkey", + "columns": [ + "internal_customer_id", + "migration_job_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_item_runs": { + "name": "migration_item_runs", + "schema": "", + "columns": { + "migration_item_run_id": { + "name": "migration_item_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_internal_id": { + "name": "migration_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_run_id": { + "name": "migration_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migration_item_runs_live_unique": { + "name": "migration_item_runs_live_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_item_runs\".\"dry_run\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_item_runs_dry_run_unique": { + "name": "migration_item_runs_dry_run_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_item_runs\".\"dry_run\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_item_runs_customer_recent_idx": { + "name": "migration_item_runs_customer_recent_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"updated_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"migration_item_runs\".\"item_kind\" = 'customer'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_jobs": { + "name": "migration_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_step": { + "name": "current_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_internal_product_id": { + "name": "from_internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_internal_product_id": { + "name": "to_internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "step_details": { + "name": "step_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "migration_jobs_from_internal_product_id_fkey": { + "name": "migration_jobs_from_internal_product_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "products", + "columnsFrom": [ + "from_internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_jobs_org_id_fkey": { + "name": "migration_jobs_org_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_jobs_to_internal_product_id_fkey": { + "name": "migration_jobs_to_internal_product_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "products", + "columnsFrom": [ + "to_internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_runs": { + "name": "migration_runs", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_internal_id": { + "name": "migration_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "lazy_run": { + "name": "lazy_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_run_id": { + "name": "trigger_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "only_ids": { + "name": "only_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "target_limit": { + "name": "target_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migration_runs_active_per_migration_unique": { + "name": "migration_runs_active_per_migration_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_runs\".\"status\" IN ('queued', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migration_runs_migration_internal_id_fkey": { + "name": "migration_runs_migration_internal_id_fkey", + "tableFrom": "migration_runs", + "tableTo": "migrations", + "columnsFrom": [ + "migration_internal_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_runs_org_id_fkey": { + "name": "migration_runs_org_id_fkey", + "tableFrom": "migration_runs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migrations": { + "name": "migrations", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "prepared_state": { + "name": "prepared_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "no_billing_changes": { + "name": "no_billing_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "retry_failed": { + "name": "retry_failed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migrations_org_env_id_unique": { + "name": "migrations_org_env_id_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migrations_org_id_fkey": { + "name": "migrations_org_id_fkey", + "tableFrom": "migrations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_currency": { + "name": "default_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'usd'" + }, + "stripe_connected": { + "name": "stripe_connected", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "stripe_config": { + "name": "stripe_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "test_stripe_connect": { + "name": "test_stripe_connect", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "live_stripe_connect": { + "name": "live_stripe_connect", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "processor_configs": { + "name": "processor_configs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "test_pkey": { + "name": "test_pkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_pkey": { + "name": "live_pkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "svix_config": { + "name": "svix_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "deployed": { + "name": "deployed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "redis_config": { + "name": "redis_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_organizations_name_trgm": { + "name": "idx_organizations_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organizations\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_organizations_slug_trgm": { + "name": "idx_organizations_slug_trgm", + "columns": [ + { + "expression": "\"slug\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organizations\".\"slug\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_organizations_created_at_id": { + "name": "idx_organizations_created_at_id", + "columns": [ + { + "expression": "\"createdAt\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "organizations_test_pkey_key": { + "name": "organizations_test_pkey_key", + "nullsNotDistinct": false, + "columns": [ + "test_pkey" + ] + }, + "organizations_live_pkey_key": { + "name": "organizations_live_pkey_key", + "nullsNotDistinct": false, + "columns": [ + "live_pkey" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialId_idx": { + "name": "passkey_credentialId_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "passkey_credential_id_unique": { + "name": "passkey_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.prices": { + "name": "prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier_behavior": { + "name": "tier_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entitlement_id": { + "name": "entitlement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "proration_config": { + "name": "proration_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + } + }, + "indexes": { + "idx_prices_internal_product_id": { + "name": "idx_prices_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_prices_entitlement_id": { + "name": "idx_prices_entitlement_id", + "columns": [ + { + "expression": "entitlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prices_entitlement_id_fkey": { + "name": "prices_entitlement_id_fkey", + "tableFrom": "prices", + "tableTo": "entitlements", + "columnsFrom": [ + "entitlement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "prices_internal_product_id_fkey": { + "name": "prices_internal_product_id_fkey", + "tableFrom": "prices", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "prices_id_key": { + "name": "prices_id_key", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_add_on": { + "name": "is_add_on", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "version": { + "name": "version", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "base_variant_id": { + "name": "base_variant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_products_org_env_id_version": { + "name": "idx_products_org_env_id_version", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "products_org_id_fkey": { + "name": "products_org_id_fkey", + "tableFrom": "products", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_product": { + "name": "unique_product", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "id", + "env", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": false + }, + "internal_reward_program_id": { + "name": "internal_reward_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_referral_codes_internal_customer_id": { + "name": "idx_referral_codes_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_codes_internal_customer_id_fkey": { + "name": "referral_codes_internal_customer_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_codes_internal_reward_program_id_fkey": { + "name": "referral_codes_internal_reward_program_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "reward_programs", + "columnsFrom": [ + "internal_reward_program_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_codes_org_id_fkey": { + "name": "referral_codes_org_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_codes_pkey": { + "name": "referral_codes_pkey", + "columns": [ + "code", + "org_id", + "env" + ] + } + }, + "uniqueConstraints": { + "referral_codes_id_key": { + "name": "referral_codes_id_key", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replaceables": { + "name": "replaceables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cus_ent_id": { + "name": "cus_ent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "from_entity_id": { + "name": "from_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delete_next_cycle": { + "name": "delete_next_cycle", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_replaceables_cus_ent_id": { + "name": "idx_replaceables_cus_ent_id", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "replaceables_cus_ent_id_fkey": { + "name": "replaceables_cus_ent_id_fkey", + "tableFrom": "replaceables", + "tableTo": "customer_entitlements", + "columnsFrom": [ + "cus_ent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.revenuecat_mappings": { + "name": "revenuecat_mappings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autumn_product_id": { + "name": "autumn_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revenuecat_product_ids": { + "name": "revenuecat_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "revenuecat_mappings_org_id_fkey": { + "name": "revenuecat_mappings_org_id_fkey", + "tableFrom": "revenuecat_mappings", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revenuecat_mappings_pkey": { + "name": "revenuecat_mappings_pkey", + "columns": [ + "org_id", + "env", + "autumn_product_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reward_programs": { + "name": "reward_programs", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "internal_reward_id": { + "name": "internal_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unlimited_redemptions": { + "name": "unlimited_redemptions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "when": { + "name": "when", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'immediately'" + }, + "product_ids": { + "name": "product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{\"\"}'" + }, + "exclude_trial": { + "name": "exclude_trial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "received_by": { + "name": "received_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reward_triggers_internal_reward_id_fkey": { + "name": "reward_triggers_internal_reward_id_fkey", + "tableFrom": "reward_programs", + "tableTo": "rewards", + "columnsFrom": [ + "internal_reward_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_triggers_org_id_fkey": { + "name": "reward_triggers_org_id_fkey", + "tableFrom": "reward_programs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reward_redemptions": { + "name": "reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": false + }, + "triggered": { + "name": "triggered", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "internal_reward_program_id": { + "name": "internal_reward_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied": { + "name": "applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "redeemer_applied": { + "name": "redeemer_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "referral_code_id": { + "name": "referral_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_internal_id": { + "name": "reward_internal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promo_code": { + "name": "promo_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_reward_redemptions_referral_code_id": { + "name": "idx_reward_redemptions_referral_code_id", + "columns": [ + { + "expression": "referral_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reward_redemptions_reward_internal_id": { + "name": "idx_reward_redemptions_reward_internal_id", + "columns": [ + { + "expression": "reward_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reward_redemptions_customer_reward": { + "name": "idx_reward_redemptions_customer_reward", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reward_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reward_redemptions_internal_customer_id_fkey": { + "name": "reward_redemptions_internal_customer_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_redemptions_internal_reward_program_id_fkey": { + "name": "reward_redemptions_internal_reward_program_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "reward_programs", + "columnsFrom": [ + "internal_reward_program_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_redemptions_referral_code_id_fkey": { + "name": "reward_redemptions_referral_code_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "referral_codes", + "columnsFrom": [ + "referral_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rewards": { + "name": "rewards", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discount_config": { + "name": "discount_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "free_product_config": { + "name": "free_product_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "free_product_id": { + "name": "free_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promo_codes": { + "name": "promo_codes", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "coupons_org_id_fkey": { + "name": "coupons_org_id_fkey", + "tableFrom": "rewards", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollovers": { + "name": "rollovers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cus_ent_id": { + "name": "cus_ent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_rollovers_cus_ent_id": { + "name": "idx_rollovers_cus_ent_id", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rollovers_cus_ent_expires": { + "name": "idx_rollovers_cus_ent_expires", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rollover_cus_ent_id_fkey": { + "name": "rollover_cus_ent_id_fkey", + "tableFrom": "rollovers", + "tableTo": "customer_entitlements", + "columnsFrom": [ + "cus_ent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.phases": { + "name": "phases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "customer_product_ids": { + "name": "customer_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "phases_schedule_id_fkey": { + "name": "phases_schedule_id_fkey", + "tableFrom": "phases", + "tableTo": "schedules", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "phases_schedule_id_starts_at_key": { + "name": "phases_schedule_id_starts_at_key", + "nullsNotDistinct": false, + "columns": [ + "schedule_id", + "starts_at" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedules": { + "name": "schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "schedules_customer_scope_unique": { + "name": "schedules_customer_scope_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"schedules\".\"internal_entity_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedules_entity_scope_unique": { + "name": "schedules_entity_scope_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"schedules\".\"internal_entity_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_schedules_internal_customer_id": { + "name": "idx_schedules_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_schedules_internal_entity_id": { + "name": "idx_schedules_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedules_org_id_fkey": { + "name": "schedules_org_id_fkey", + "tableFrom": "schedules", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedules_internal_customer_id_fkey": { + "name": "schedules_internal_customer_id_fkey", + "tableFrom": "schedules", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedules_internal_entity_id_fkey": { + "name": "schedules_internal_entity_id_fkey", + "tableFrom": "schedules", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "usage_features": { + "name": "usage_features", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "subscriptions_org_id_fkey": { + "name": "subscriptions_org_id_fkey", + "tableFrom": "subscriptions", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_stripe_id_key": { + "name": "subscriptions_stripe_id_key", + "nullsNotDistinct": false, + "columns": [ + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_name_trgm": { + "name": "idx_user_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_user_email_trgm": { + "name": "idx_user_email_trgm", + "columns": [ + { + "expression": "\"email\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_user_created_at_id": { + "name": "idx_user_created_at_id", + "columns": [ + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_created_by_fkey": { + "name": "user_created_by_fkey", + "tableFrom": "user", + "tableTo": "organizations", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_resources": { + "name": "vercel_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "vercel_resources_installation_name_unique_idx": { + "name": "vercel_resources_installation_name_unique_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status <> 'uninstalled'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vercel_resources_org_id_fkey": { + "name": "vercel_resources_org_id_fkey", + "tableFrom": "vercel_resources", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/shared/drizzle/meta/_journal.json b/shared/drizzle/meta/_journal.json index 6dfc93d64..9d13cade1 100644 --- a/shared/drizzle/meta/_journal.json +++ b/shared/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1780493543535, "tag": "0004_lucky_electro", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1780582242747, + "tag": "0005_fresh_runaways", + "breakpoints": true } ] } \ No newline at end of file From b9bfe213f707886e913b7424ff2f619cc78f910e Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 16:49:28 +0100 Subject: [PATCH 07/12] fix: mcp oauth --- packages/atmn/src/commands/auth/constants.ts | 9 +- packages/mcp/src/server/auth/oauth.ts | 107 +- .../mcp/src/server/auth/utils/principal.ts | 30 - packages/mcp/src/server/auth/utils/schemas.ts | 9 - packages/mcp/src/server/auth/utils/urls.ts | 3 - packages/mcp/src/tools/index.ts | 4 +- packages/mcp/src/tools/org.ts | 34 + packages/mcp/src/tools/utils/client.ts | 29 + .../unit/mcp-server/agent/server.test.ts | 1 + .../tests/unit/mcp-server/agent/tools.test.ts | 36 + .../mcp/tests/unit/mcp-server/oauth.test.ts | 105 +- server/src/initHono.ts | 50 +- .../internal/admin/handleListOAuthClients.ts | 23 +- server/src/internal/auth/actions/index.ts | 4 + .../auth/actions/registerMcpOAuthClient.ts | 315 + .../internal/auth/oauth/atmnOAuthClients.ts | 60 + .../auth/oauth/handleGetOAuthClient.ts | 34 + .../auth/oauth/handleMcpOAuthRegistration.ts | 35 + .../auth/oauth/handleOAuthConsentWithEnv.ts | 98 + .../auth/oauth/handleOAuthTokenWithApiKey.ts | 82 + .../auth/oauth/internalMcpOAuthClients.ts | 103 + .../auth/oauth/oauthAccessTokenApiKey.ts | 174 + .../internal/auth/oauth/oauthConsentApiKey.ts | 136 + server/src/internal/auth/oauth/oauthRouter.ts | 55 + server/src/internal/auth/repos/index.ts | 11 + .../auth/repos/oauthAccessTokenRepo.ts | 49 + .../internal/auth/repos/oauthApiKeyRepo.ts | 107 + .../internal/auth/repos/oauthClientRepo.ts | 141 + .../internal/auth/repos/oauthConsentRepo.ts | 164 + .../auth/repos/oauthRefreshTokenRepo.ts | 27 + .../cli/handlers/handleCreateOAuthApiKeys.ts | 155 +- .../handlers/handleGetConsentApiKeys.ts | 36 +- .../consent/handlers/handleGetOrgConsents.ts | 21 +- .../consent/handlers/handleRevokeConsent.ts | 81 +- shared/db/auth-schema.ts | 5 + shared/drizzle/0006_sad_madrox.sql | 4 + shared/drizzle/meta/0006_snapshot.json | 7383 +++++++++++++++++ shared/drizzle/meta/_journal.json | 7 + vite/src/views/auth/Consent.tsx | 322 +- 39 files changed, 9443 insertions(+), 606 deletions(-) create mode 100644 packages/mcp/src/tools/org.ts create mode 100644 server/src/internal/auth/actions/index.ts create mode 100644 server/src/internal/auth/actions/registerMcpOAuthClient.ts create mode 100644 server/src/internal/auth/oauth/atmnOAuthClients.ts create mode 100644 server/src/internal/auth/oauth/handleGetOAuthClient.ts create mode 100644 server/src/internal/auth/oauth/handleMcpOAuthRegistration.ts create mode 100644 server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts create mode 100644 server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts create mode 100644 server/src/internal/auth/oauth/internalMcpOAuthClients.ts create mode 100644 server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts create mode 100644 server/src/internal/auth/oauth/oauthConsentApiKey.ts create mode 100644 server/src/internal/auth/oauth/oauthRouter.ts create mode 100644 server/src/internal/auth/repos/index.ts create mode 100644 server/src/internal/auth/repos/oauthAccessTokenRepo.ts create mode 100644 server/src/internal/auth/repos/oauthApiKeyRepo.ts create mode 100644 server/src/internal/auth/repos/oauthClientRepo.ts create mode 100644 server/src/internal/auth/repos/oauthConsentRepo.ts create mode 100644 server/src/internal/auth/repos/oauthRefreshTokenRepo.ts create mode 100644 shared/drizzle/0006_sad_madrox.sql create mode 100644 shared/drizzle/meta/0006_snapshot.json diff --git a/packages/atmn/src/commands/auth/constants.ts b/packages/atmn/src/commands/auth/constants.ts index b174d59a2..704a3f480 100644 --- a/packages/atmn/src/commands/auth/constants.ts +++ b/packages/atmn/src/commands/auth/constants.ts @@ -1,9 +1,10 @@ // OAuth constants for CLI authentication -/** The OAuth client ID for the CLI (public client) */ -// export const CLI_CLIENT_ID = "khicXGthBbGMIWmpgodOTDcCCJHJMDpN"; (local i think) -// export const CLI_CLIENT_ID = "NiKwaSyAfaeEEKEvFaUYihTXdTPtIRCk" (dev i think) -export const CLI_CLIENT_ID = "hAWUopQqLnsSwuRgeRzIBzKslwXmQUSr"; // (prod i think) +// Historical Better Auth OAuth clients for atmn CLI environments. +// Server auth should identify atmn from oauth_client metadata/name instead. +export const LOCAL_CLI_CLIENT_ID = "khicXGthBbGMIWmpgodOTDcCCJHJMDpN"; +export const DEV_CLI_CLIENT_ID = "NiKwaSyAfaeEEKEvFaUYihTXdTPtIRCk"; +export const CLI_CLIENT_ID = "hAWUopQqLnsSwuRgeRzIBzKslwXmQUSr"; /** Base port for the local OAuth callback server */ export const OAUTH_PORT_BASE = 31448; diff --git a/packages/mcp/src/server/auth/oauth.ts b/packages/mcp/src/server/auth/oauth.ts index fae23f87d..dd975f1e8 100644 --- a/packages/mcp/src/server/auth/oauth.ts +++ b/packages/mcp/src/server/auth/oauth.ts @@ -1,9 +1,7 @@ -import { ms } from "@autumn/shared/unixUtils"; -import { addMilliseconds, isFuture } from "date-fns"; import { MCP_OAUTH_SCOPES } from "../../constants.js"; import type { AutumnMcpAuth } from "./auth.js"; import { OAuthHttpError } from "./utils/errors.js"; -import { getOAuthPrincipalId, principalFromSecret } from "./utils/principal.js"; +import { principalFromSecret } from "./utils/principal.js"; import { getEnvironment, getStaticApiKey, @@ -13,11 +11,9 @@ import { failOpenSchema, type MCPOAuthFlags, secretKeySchema, - tokenExchangeSchema, xApiVersionSchema, } from "./utils/schemas.js"; import { - getApiKeyUrl, getIssuerUrl, getResourceUrl, getWWWAuthenticate, @@ -28,77 +24,10 @@ export { MCP_OAUTH_SCOPES } from "../../constants.js"; export { OAuthHttpError } from "./utils/errors.js"; export type { MCPOAuthFlags, OAuthEnvironment } from "./utils/schemas.js"; -type ExchangedToken = { - key: string; - orgId?: string | undefined; - userId?: string | undefined; - clientId?: string | undefined; - scopes?: string[] | undefined; -}; - type AuthLogger = { warning: (message: string, data?: Record) => void; }; -const apiKeyCache = new Map(); - -const exchangeOAuthToken = async ({ - headers, - flags, - resource, - token, -}: { - headers: Headers; - flags: MCPOAuthFlags; - resource: string; - token: string; -}): Promise => { - const env = getEnvironment({ headers, flags }); - const cacheKey = `${token}:${resource}:${env}`; - const cached = apiKeyCache.get(cacheKey); - if (cached && isFuture(cached.expiresAt)) return cached; - - const response = await fetch(getApiKeyUrl(flags), { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ resource, scopes: MCP_OAUTH_SCOPES }), - }); - - if (!response.ok) { - throw new OAuthHttpError( - response.status === 403 ? 403 : 401, - await response.text(), - response.status === 403 ? "insufficient_scope" : "invalid_token", - response.status === 403 - ? undefined - : getWWWAuthenticate({ resourceUrl: resource, error: "invalid_token" }), - ); - } - - const data = tokenExchangeSchema.parse(await response.json()); - const key = env === "live" ? data.prod_key : data.sandbox_key; - if (!key) { - throw new OAuthHttpError( - 502, - "OAuth key exchange did not return an API key", - ); - } - - const exchanged = { - key, - orgId: data.org_id, - userId: data.user_id, - clientId: data.client_id, - scopes: data.scopes, - expiresAt: addMilliseconds(new Date(), ms.minutes(1)), - }; - apiKeyCache.set(cacheKey, exchanged); - return exchanged; -}; - export const getProtectedResourceMetadata = ( headers: Headers, flags: MCPOAuthFlags, @@ -170,34 +99,12 @@ export const buildAuthForRequest = async ( } if (flags["oauth-enabled"]) { - const authHeader = headers.get("authorization"); - if (!authHeader?.startsWith("Bearer ")) { - throw new OAuthHttpError( - 401, - "Missing Authorization bearer token", - "invalid_token", - getWWWAuthenticate({ resourceUrl: resource }), - ); - } - - const token = authHeader.slice("Bearer ".length); - const exchanged = await exchangeOAuthToken({ - headers, - flags, - resource, - token, - }); - return { - apiKey: exchanged.key, - env, - resource, - principalId: getOAuthPrincipalId({ token, exchanged }), - scopes: exchanged.scopes ?? [...MCP_OAUTH_SCOPES], - orgId: exchanged.orgId, - serverURL: flags["server-url"], - xApiVersion, - failOpen, - }; + throw new OAuthHttpError( + 401, + "Missing Autumn API key bearer token", + "invalid_token", + getWWWAuthenticate({ resourceUrl: resource, error: "invalid_token" }), + ); } logger.warning("Missing secret-key for MCP request"); diff --git a/packages/mcp/src/server/auth/utils/principal.ts b/packages/mcp/src/server/auth/utils/principal.ts index 778c34e7b..f3b14258f 100644 --- a/packages/mcp/src/server/auth/utils/principal.ts +++ b/packages/mcp/src/server/auth/utils/principal.ts @@ -15,33 +15,3 @@ export const principalFromSecret = ({ kind: string; value: string; }) => `${kind}:${hash(value)}`; - -type ExchangedIdentity = { - orgId?: string | undefined; - userId?: string | undefined; - clientId?: string | undefined; -}; - -/** - * Derives a principal id for an OAuth session. When the token exchange returned - * an org we build a human-readable `oauth:::` id; otherwise we - * fall back to a hashed token so unidentified callers still group consistently. - */ -export const getOAuthPrincipalId = ({ - token, - exchanged, -}: { - token: string; - exchanged: ExchangedIdentity; -}) => { - if (!exchanged.orgId) { - return principalFromSecret({ kind: "oauth", value: token }); - } - - return [ - "oauth", - exchanged.orgId, - exchanged.userId ?? "unknown-user", - exchanged.clientId ?? "unknown-client", - ].join(":"); -}; diff --git a/packages/mcp/src/server/auth/utils/schemas.ts b/packages/mcp/src/server/auth/utils/schemas.ts index add8d13e8..27c81980f 100644 --- a/packages/mcp/src/server/auth/utils/schemas.ts +++ b/packages/mcp/src/server/auth/utils/schemas.ts @@ -16,15 +16,6 @@ export const failOpenSchema = z export const secretKeySchema = z.string().min(1).optional(); -export const tokenExchangeSchema = z.object({ - sandbox_key: z.string().optional(), - prod_key: z.string().optional(), - org_id: z.string().optional(), - user_id: z.string().optional(), - client_id: z.string().optional(), - scopes: z.array(z.string()).optional(), -}); - export interface MCPOAuthFlags extends MCPServerFlags { readonly "oauth-enabled"?: boolean | undefined; readonly "oauth-environment"?: OAuthEnvironment | undefined; diff --git a/packages/mcp/src/server/auth/utils/urls.ts b/packages/mcp/src/server/auth/utils/urls.ts index 7cb4809b9..55510d869 100644 --- a/packages/mcp/src/server/auth/utils/urls.ts +++ b/packages/mcp/src/server/auth/utils/urls.ts @@ -45,9 +45,6 @@ export const getIssuerUrl = (flags: MCPOAuthFlags): string => new URL("/api/auth", flags["server-url"] ?? DEFAULT_AUTUMN_API_URL).href, ); -export const getApiKeyUrl = (flags: MCPOAuthFlags): string => - new URL("/cli/api-keys", getIssuerUrl(flags)).href; - export const getWWWAuthenticate = ({ resourceUrl, error, diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts index 3e723430e..c2f5d30dd 100644 --- a/packages/mcp/src/tools/index.ts +++ b/packages/mcp/src/tools/index.ts @@ -6,6 +6,7 @@ import { type AutumnMcpAuth, getAutumnAuth } from "../server/auth/auth.js"; import { balances } from "./balances.js"; import { billing } from "./billing.js"; import { customers } from "./customers.js"; +import { orgTools } from "./org.js"; import { plans } from "./plans.js"; import { callAutumn } from "./utils/client.js"; import { dateToEpochMillisecondsTool } from "./utils/dates.js"; @@ -72,7 +73,8 @@ export const createRawAutumnOperationTools = () => ), ...toTools(localPreviews, rawLocalPreviewTool), ...toTools(confirmedWrites, operationTool), - }), + ...orgTools, + } as Record>), surface: "mcp", }); diff --git a/packages/mcp/src/tools/org.ts b/packages/mcp/src/tools/org.ts new file mode 100644 index 000000000..ab567d5e3 --- /dev/null +++ b/packages/mcp/src/tools/org.ts @@ -0,0 +1,34 @@ +import { createTool } from "@mastra/core/tools"; +import * as z from "zod/v4"; +import { getAutumnAuth } from "../server/auth/auth.js"; +import { mcpAnnotations } from "./utils/annotations.js"; +import { callAutumnGet } from "./utils/client.js"; + +const organizationMeSchema = z + .object({ + name: z.string(), + slug: z.string(), + env: z.string(), + }) + .strict(); + +const signalOf = (context: { mcp?: { extra?: { signal?: AbortSignal } } }) => + context?.mcp?.extra?.signal; + +export const orgTools = { + getCurrentOrganization: createTool({ + id: "getCurrentOrganization", + description: + "Fetch the current Autumn organization name, slug, and environment.", + inputSchema: z.object({}).strict(), + mcp: { annotations: mcpAnnotations() }, + execute: async (_input, context) => + organizationMeSchema.parse( + await callAutumnGet({ + auth: getAutumnAuth(context), + endpoint: "/v1/organization/me", + signal: signalOf(context), + }), + ), + }), +} as const; diff --git a/packages/mcp/src/tools/utils/client.ts b/packages/mcp/src/tools/utils/client.ts index 35f1db178..a5033f9f3 100644 --- a/packages/mcp/src/tools/utils/client.ts +++ b/packages/mcp/src/tools/utils/client.ts @@ -43,3 +43,32 @@ export const callAutumn = async ({ } return body; }; + +export const callAutumnGet = async ({ + auth, + endpoint, + signal, +}: { + auth: AutumnMcpAuth; + endpoint: string; + signal?: AbortSignal | undefined; +}) => { + const client = createAutumnClient(auth); + const init: RequestInit = { + method: "GET", + headers: client.headers, + }; + if (signal) init.signal = signal; + + const response = await fetch(new URL(endpoint, client.baseUrl), init); + const text = await response.text(); + const body = text ? parseBody(text) : null; + if (!response.ok) { + throw new Error( + `Autumn API request failed (${response.status}): ${ + typeof body === "string" ? body : JSON.stringify(body) + }`, + ); + } + return body; +}; diff --git a/packages/mcp/tests/unit/mcp-server/agent/server.test.ts b/packages/mcp/tests/unit/mcp-server/agent/server.test.ts index b7b0d9245..4f0df3b75 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/server.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/server.test.ts @@ -21,6 +21,7 @@ describe("Autumn MCP server", () => { "attach", "updateSubscription", "createSchedule", + "getCurrentOrganization", ]); expect(tools.tools.map((tool) => tool.name)).not.toContain("ask_autumn"); expect(tools.tools.map((tool) => tool.name)).not.toContain( diff --git a/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts b/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts index fd874f169..fa3f17a60 100644 --- a/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts +++ b/packages/mcp/tests/unit/mcp-server/agent/tools.test.ts @@ -43,6 +43,7 @@ describe("Autumn operation tools", () => { expect(tools.previewCreateBalance.description).toContain("Does not mutate"); expect(tools.createSchedule.description).toContain("starts_at"); expect(tools.previewCreateSchedule.description).toContain("billing impact"); + expect(tools.getCurrentOrganization.description).toContain("organization"); }); test("write tools are annotated as destructive", () => { @@ -67,6 +68,7 @@ describe("Autumn operation tools", () => { "previewUpdateSubscription", "previewCreateSchedule", "previewCreateBalance", + "getCurrentOrganization", ] as const) { expect(tools[name].mcp?.annotations?.destructiveHint).toBe(false); } @@ -312,6 +314,40 @@ describe("Autumn operation tools", () => { } }); + test("raw getCurrentOrganization calls the organization me endpoint", async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (url, init) => { + expect(String(url)).toBe("http://localhost:8080/v1/organization/me"); + expect(init?.method).toBe("GET"); + expect(init?.body).toBeUndefined(); + return Response.json({ + name: "Unit Tests", + slug: "unit-tests", + env: "sandbox", + }); + }) as typeof fetch; + + try { + const tool = createRawAutumnOperationTools().getCurrentOrganization; + if (!tool.execute) { + throw new Error("getCurrentOrganization is not executable"); + } + + await expect( + tool.execute( + { intent: "check which Autumn organization is connected" }, + { mcp: { extra: { authInfo: auth } } } as never, + ), + ).resolves.toEqual({ + name: "Unit Tests", + slug: "unit-tests", + env: "sandbox", + }); + } finally { + globalThis.fetch = originalFetch; + } + }); + test("raw previewAttach does not create a pending action", async () => { await clearPendingActions(); const originalFetch = globalThis.fetch; diff --git a/packages/mcp/tests/unit/mcp-server/oauth.test.ts b/packages/mcp/tests/unit/mcp-server/oauth.test.ts index b29878dfc..70c397748 100644 --- a/packages/mcp/tests/unit/mcp-server/oauth.test.ts +++ b/packages/mcp/tests/unit/mcp-server/oauth.test.ts @@ -41,7 +41,7 @@ describe("MCP OAuth auth resolution", () => { status: 401, error: "invalid_token", wwwAuthenticate: - 'Bearer resource_metadata="http://localhost:2718/.well-known/oauth-protected-resource/mcp"', + 'Bearer resource_metadata="http://localhost:2718/.well-known/oauth-protected-resource/mcp", error="invalid_token"', } satisfies Partial); }); @@ -57,47 +57,34 @@ describe("MCP OAuth auth resolution", () => { status: 401, error: "invalid_token", wwwAuthenticate: - 'Bearer resource_metadata="http://localhost:2718/.well-known/oauth-protected-resource/internal/mcp"', + 'Bearer resource_metadata="http://localhost:2718/.well-known/oauth-protected-resource/internal/mcp", error="invalid_token"', } satisfies Partial); }); - test("exchanges a bearer token for Autumn API credentials", async () => { + test("rejects opaque bearer tokens without exchanging them", async () => { const originalFetch = globalThis.fetch; - globalThis.fetch = (async (_url, init) => { - expect(init?.headers).toEqual({ - Authorization: "Bearer oauth_token", - "Content-Type": "application/json", - }); - expect(JSON.parse(init?.body as string)).toEqual({ - resource: "http://localhost:2718/mcp", - scopes: MCP_OAUTH_SCOPES, - }); - return Response.json({ - sandbox_key: "sk_sandbox", - prod_key: "sk_live", - org_id: "org_123", - user_id: "user_123", - client_id: "client_123", - scopes: MCP_OAUTH_SCOPES, - }); - }) as typeof fetch; + let fetchCalled = false; + const mockFetch = (async () => { + fetchCalled = true; + return Response.json({}); + }) as unknown as typeof fetch; + globalThis.fetch = mockFetch; try { - const auth = await buildAuthForRequest( - new Headers({ - authorization: "Bearer oauth_token", - host: "localhost:2718", - }), - flags as MCPOAuthFlags, - logger, - ); - - expect(auth.apiKey).toBe("sk_sandbox"); - expect(auth.env).toBe("sandbox"); - expect(auth.resource).toBe("http://localhost:2718/mcp"); - expect(auth.principalId).toBe("oauth:org_123:user_123:client_123"); - expect(auth.scopes).toEqual([...MCP_OAUTH_SCOPES]); - expect(auth.orgId).toBe("org_123"); + await expect( + buildAuthForRequest( + new Headers({ + authorization: "Bearer oauth_token", + host: "localhost:2718", + }), + flags as MCPOAuthFlags, + logger, + ), + ).rejects.toMatchObject({ + status: 401, + error: "invalid_token", + } satisfies Partial); + expect(fetchCalled).toBe(false); } finally { globalThis.fetch = originalFetch; } @@ -133,40 +120,24 @@ describe("MCP OAuth auth resolution", () => { }); test("uses route-specific resource URLs", async () => { - const originalFetch = globalThis.fetch; - globalThis.fetch = (async (_url, init) => { - expect(JSON.parse(init?.body as string)).toMatchObject({ - resource: "http://localhost:2718/internal/mcp", - }); - return Response.json({ - sandbox_key: "sk_sandbox", - org_id: "org_123", - scopes: MCP_OAUTH_SCOPES, - }); - }) as typeof fetch; + const auth = await buildAuthForRequest( + new Headers({ + authorization: "Bearer am_sk_test_chat", + host: "localhost:2718", + }), + flags as MCPOAuthFlags, + logger, + "/internal/mcp", + ); - try { - const auth = await buildAuthForRequest( - new Headers({ - authorization: "Bearer internal_oauth_token", - host: "localhost:2718", - }), + expect(auth.resource).toBe("http://localhost:2718/internal/mcp"); + expect( + getProtectedResourceMetadata( + new Headers({ host: "localhost:2718" }), flags as MCPOAuthFlags, - logger, "/internal/mcp", - ); - - expect(auth.resource).toBe("http://localhost:2718/internal/mcp"); - expect( - getProtectedResourceMetadata( - new Headers({ host: "localhost:2718" }), - flags as MCPOAuthFlags, - "/internal/mcp", - ).resource, - ).toBe("http://localhost:2718/internal/mcp"); - } finally { - globalThis.fetch = originalFetch; - } + ).resource, + ).toBe("http://localhost:2718/internal/mcp"); }); test("missing static secret-key returns the auth error path", async () => { diff --git a/server/src/initHono.ts b/server/src/initHono.ts index c3b29ea36..efb690283 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -1,10 +1,4 @@ -import { oauthClient } from "@autumn/shared"; -import { - oauthProviderAuthServerMetadata, - oauthProviderOpenIdConfigMetadata, -} from "@better-auth/oauth-provider"; import { httpInstrumentationMiddleware } from "@hono/otel"; -import { eq } from "drizzle-orm"; import { Hono } from "hono"; import { cors } from "hono/cors"; import { autumnWebhookRouter } from "./external/autumn/autumnWebhookRouter.js"; @@ -19,6 +13,7 @@ import type { HonoEnv } from "./honoUtils/HonoEnv.js"; import { handleHealthCheck } from "./honoUtils/handleHealthCheck.js"; import { handleReadyCheck } from "./honoUtils/handleReadyCheck.js"; import { handleListAuthOrganizations } from "./internal/auth/handleListAuthOrganizations.js"; +import { oauthRouter } from "./internal/auth/oauth/oauthRouter.js"; import { cliRouter } from "./internal/dev/cli/cliRouter.js"; import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js"; import { apiRouter } from "./routers/apiRouter.js"; @@ -70,21 +65,7 @@ export const createHonoApp = () => { }), ); - app.get("/api/auth/.well-known/openid-configuration", (c) => { - return oauthProviderOpenIdConfigMetadata(auth)(c.req.raw); - }); - - app.get("/.well-known/oauth-authorization-server", (c) => { - return oauthProviderAuthServerMetadata(auth)(c.req.raw); - }); - - app.get("/api/auth/.well-known/oauth-authorization-server", (c) => { - return oauthProviderAuthServerMetadata(auth)(c.req.raw); - }); - - app.get("/.well-known/oauth-authorization-server/api/auth", (c) => { - return oauthProviderAuthServerMetadata(auth)(c.req.raw); - }); + app.route("", oauthRouter); // Better Auth's joined Drizzle query defaults to 100 memberships. app.get("/api/auth/organization/list", handleListAuthOrganizations); @@ -111,33 +92,6 @@ export const createHonoApp = () => { app.use("*", baseMiddleware); app.use("*", replicaDbMiddleware); - // Public endpoint to get OAuth client name (for consent page) - app.get("/oauth/client/:client_id", async (c) => { - const clientId = c.req.param("client_id"); - if (!clientId) { - return c.json({ error: "client_id is required" }, 400); - } - - const db = c.get("ctx").db; - const client = await db - .select({ - name: oauthClient.name, - clientId: oauthClient.clientId, - }) - .from(oauthClient) - .where(eq(oauthClient.clientId, clientId)) - .limit(1); - - if (!client.length) { - return c.json({ error: "Client not found" }, 404); - } - - return c.json({ - client_id: client[0].clientId, - name: client[0].name || "Unknown Application", - }); - }); - // CLI routes (uses Bearer token auth, not session auth) app.route("/cli", cliRouter); diff --git a/server/src/internal/admin/handleListOAuthClients.ts b/server/src/internal/admin/handleListOAuthClients.ts index 01cf75848..0147e1804 100644 --- a/server/src/internal/admin/handleListOAuthClients.ts +++ b/server/src/internal/admin/handleListOAuthClients.ts @@ -1,5 +1,5 @@ -import { oauthClient, Scopes } from "@autumn/shared"; -import { desc } from "drizzle-orm"; +import { Scopes } from "@autumn/shared"; +import { oauthClientRepo } from "@/internal/auth/repos/index.js"; import { createRoute } from "../../honoMiddlewares/routeHandler"; export const handleListOAuthClients = createRoute({ @@ -8,24 +8,7 @@ export const handleListOAuthClients = createRoute({ const ctx = c.get("ctx"); const { db } = ctx; - const clients = await db - .select({ - id: oauthClient.id, - clientId: oauthClient.clientId, - name: oauthClient.name, - redirectUris: oauthClient.redirectUris, - public: oauthClient.public, - disabled: oauthClient.disabled, - skipConsent: oauthClient.skipConsent, - scopes: oauthClient.scopes, - tokenEndpointAuthMethod: oauthClient.tokenEndpointAuthMethod, - grantTypes: oauthClient.grantTypes, - responseTypes: oauthClient.responseTypes, - createdAt: oauthClient.createdAt, - updatedAt: oauthClient.updatedAt, - }) - .from(oauthClient) - .orderBy(desc(oauthClient.createdAt)); + const clients = await oauthClientRepo.listForAdmin({ db }); return c.json({ clients: clients.map((client) => ({ diff --git a/server/src/internal/auth/actions/index.ts b/server/src/internal/auth/actions/index.ts new file mode 100644 index 000000000..ce439fdf0 --- /dev/null +++ b/server/src/internal/auth/actions/index.ts @@ -0,0 +1,4 @@ +export { + isSafeOAuthRedirectUri, + registerMcpOAuthClient, +} from "./registerMcpOAuthClient.js"; diff --git a/server/src/internal/auth/actions/registerMcpOAuthClient.ts b/server/src/internal/auth/actions/registerMcpOAuthClient.ts new file mode 100644 index 000000000..e687be395 --- /dev/null +++ b/server/src/internal/auth/actions/registerMcpOAuthClient.ts @@ -0,0 +1,315 @@ +import { ALL_SCOPES } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { generateId } from "@/utils/genUtils.js"; +import { type OAuthClientRecord, oauthClientRepo } from "../repos/index.js"; + +const MCP_CLIENT_KIND = "mcp_client"; +const REGISTER_CACHE_TTL_MS = 5 * 60 * 1000; +const DANGEROUS_REDIRECT_SCHEMES = new Set([ + "javascript:", + "data:", + "vbscript:", +]); + +type MpcClientType = "claude" | "codex" | "cursor" | "opencode" | "slack"; + +type MpcClientInfo = { + type: MpcClientType; + name: string; + clientId: string; +}; + +type McpMetadata = { + kind?: string; + mcpClientType?: string; + redirectNames?: Record; +}; + +type RegistrationResponse = { + body: { + client_id: string; + client_id_issued_at: number; + client_name: string | null; + redirect_uris: string[]; + scope: string; + token_endpoint_auth_method: "none"; + grant_types: ["authorization_code", "refresh_token"]; + response_types: ["code"]; + public: true; + type: "native"; + }; + status: 200 | 201; +}; + +const registerCache = new Map(); + +const parseMetadata = (metadata: unknown): McpMetadata => { + if (!metadata) return {}; + if (typeof metadata === "string") { + try { + const parsed = JSON.parse(metadata); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } + } + + return typeof metadata === "object" ? metadata : {}; +}; + +const isLocalhost = (hostname: string) => + hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; + +export const isSafeOAuthRedirectUri = (redirectUri: string) => { + if (!URL.canParse(redirectUri)) return false; + + const url = new URL(redirectUri); + if (DANGEROUS_REDIRECT_SCHEMES.has(url.protocol)) return false; + if (url.protocol === "http:") return isLocalhost(url.hostname); + + return true; +}; + +const normalize = (value: string) => value.trim().toLowerCase(); + +const classifyMcpClient = ({ + clientName, + redirectUris, +}: { + clientName: unknown; + redirectUris: string[]; +}): MpcClientInfo | null => { + const haystack = [ + typeof clientName === "string" ? clientName : "", + ...redirectUris, + ] + .join(" ") + .toLowerCase(); + + if (haystack.includes("cursor")) { + return { type: "cursor", name: "Cursor", clientId: "autumn_mcp_cursor" }; + } + if (haystack.includes("claude")) { + return { type: "claude", name: "Claude", clientId: "autumn_mcp_claude" }; + } + if ( + haystack.includes("opencode") || + haystack.includes("open-code") || + haystack.includes("open code") + ) { + return { + type: "opencode", + name: "OpenCode", + clientId: "autumn_mcp_opencode", + }; + } + if (haystack.includes("codex")) { + return { type: "codex", name: "Codex", clientId: "autumn_mcp_codex" }; + } + if (haystack.includes("slack")) { + return { type: "slack", name: "Slack", clientId: "autumn_mcp_slack" }; + } + + return null; +}; + +const getRequestedScopes = (scope: unknown) => { + if (typeof scope !== "string" || !scope.trim()) return [...ALL_SCOPES]; + const allowed = new Set(ALL_SCOPES); + return scope.split(" ").filter((scope) => allowed.has(scope as never)); +}; + +const mergeMetadata = ({ + client, + info, + redirectUris, +}: { + client: OAuthClientRecord | null; + info: MpcClientInfo; + redirectUris: string[]; +}) => { + const existing = parseMetadata(client?.metadata); + const redirectNames = { ...(existing.redirectNames ?? {}) }; + for (const redirectUri of redirectUris) { + redirectNames[redirectUri] = info.name; + } + + return { + ...existing, + kind: MCP_CLIENT_KIND, + mcpClientType: info.type, + redirectNames, + }; +}; + +const clientMatches = ({ + client, + info, + redirectUris, +}: { + client: OAuthClientRecord; + info: MpcClientInfo; + redirectUris: string[]; +}) => { + const metadata = parseMetadata(client.metadata); + if ( + metadata.kind === MCP_CLIENT_KIND && + metadata.mcpClientType === info.type + ) { + return true; + } + if (client.clientId === info.clientId) return true; + + const requested = new Set(redirectUris); + const hasMatchingRedirectUri = client.redirectUris.some((redirectUri) => + requested.has(redirectUri), + ); + if (!hasMatchingRedirectUri) return false; + + if (normalize(client.name ?? "") === normalize(info.name)) return true; + return ( + classifyMcpClient({ + clientName: client.name, + redirectUris: client.redirectUris, + })?.type === info.type + ); +}; + +const getCachedRegistration = (cacheKey: string) => { + const cached = registerCache.get(cacheKey); + if (!cached || cached.expiresAt < Date.now()) { + registerCache.delete(cacheKey); + return null; + } + + return cached.body; +}; + +const setCachedRegistration = (cacheKey: string, body: unknown) => { + registerCache.set(cacheKey, { + expiresAt: Date.now() + REGISTER_CACHE_TTL_MS, + body, + }); +}; + +const getRegistrationResponse = ( + client: OAuthClientRecord, + status: 200 | 201, +): RegistrationResponse => ({ + body: { + client_id: client.clientId, + client_id_issued_at: client.createdAt + ? Math.floor(client.createdAt.getTime() / 1000) + : Math.floor(Date.now() / 1000), + client_name: client.name, + redirect_uris: client.redirectUris, + scope: client.scopes?.join(" ") ?? "", + token_endpoint_auth_method: "none", + grant_types: ["authorization_code", "refresh_token"], + response_types: ["code"], + public: true, + type: "native", + }, + status, +}); + +export const registerMcpOAuthClient = async ({ + db, + clientName, + redirectUris, + scope, +}: { + db: DrizzleCli; + clientName: unknown; + redirectUris: string[]; + scope: unknown; +}): Promise => { + if (redirectUris.length === 0) { + return { error: "redirect_uris is required", status: 400 }; + } + if (!redirectUris.every(isSafeOAuthRedirectUri)) { + return { error: "invalid_redirect_uri", status: 400 }; + } + + const info = classifyMcpClient({ clientName, redirectUris }); + if (!info) { + return { error: "unsupported_mcp_client", status: 400 }; + } + + const cacheKey = `${info.type}:${[...redirectUris].sort().join("|")}`; + const cached = getCachedRegistration(cacheKey); + if (cached) + return { body: cached as RegistrationResponse["body"], status: 200 }; + + const requestedScopes = getRequestedScopes(scope); + const clients = await oauthClientRepo.list({ db }); + const existingClient = + clients.find((client) => clientMatches({ client, info, redirectUris })) ?? + null; + const now = new Date(); + + if (existingClient) { + const mergedRedirectUris = [ + ...new Set([...existingClient.redirectUris, ...redirectUris]), + ]; + const mergedScopes = [ + ...new Set([...(existingClient.scopes ?? []), ...requestedScopes]), + ]; + + const updatedClient = await oauthClientRepo.updateById({ + db, + id: existingClient.id, + updates: { + name: info.name, + redirectUris: mergedRedirectUris, + scopes: mergedScopes, + tokenEndpointAuthMethod: "none", + grantTypes: ["authorization_code", "refresh_token"], + responseTypes: ["code"], + public: true, + type: "native", + metadata: mergeMetadata({ client: existingClient, info, redirectUris }), + updatedAt: now, + }, + }); + + const response = getRegistrationResponse(updatedClient!, 200); + setCachedRegistration(cacheKey, response.body); + return response; + } + + const client = await oauthClientRepo.upsert({ + db, + insert: { + id: generateId("oauth_client"), + clientId: info.clientId, + name: info.name, + redirectUris, + scopes: requestedScopes, + tokenEndpointAuthMethod: "none", + grantTypes: ["authorization_code", "refresh_token"], + responseTypes: ["code"], + public: true, + type: "native", + metadata: mergeMetadata({ client: null, info, redirectUris }), + createdAt: now, + updatedAt: now, + }, + update: { + name: info.name, + redirectUris, + scopes: requestedScopes, + tokenEndpointAuthMethod: "none", + grantTypes: ["authorization_code", "refresh_token"], + responseTypes: ["code"], + public: true, + type: "native", + metadata: mergeMetadata({ client: null, info, redirectUris }), + updatedAt: now, + }, + }); + + const response = getRegistrationResponse(client!, 201); + setCachedRegistration(cacheKey, response.body); + return response; +}; diff --git a/server/src/internal/auth/oauth/atmnOAuthClients.ts b/server/src/internal/auth/oauth/atmnOAuthClients.ts new file mode 100644 index 000000000..bcc860ee3 --- /dev/null +++ b/server/src/internal/auth/oauth/atmnOAuthClients.ts @@ -0,0 +1,60 @@ +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { oauthClientRepo } from "../repos/index.js"; + +const ATMN_OAUTH_CLIENT_NAMES = new Set(["atmn", "autumn cli"]); + +const configuredAtmnClientIds = () => + new Set( + (process.env.ATMN_OAUTH_CLIENT_IDS ?? "") + .split(",") + .map((id) => id.trim()) + .filter(Boolean), + ); + +const metadataMarksAtmn = (metadata: unknown) => { + if (!metadata) return false; + let metadataObject = metadata; + if (typeof metadata === "string") { + try { + metadataObject = JSON.parse(metadata); + } catch { + return false; + } + } + + if (!metadataObject || typeof metadataObject !== "object") return false; + + const values = Object.values(metadataObject as Record); + return values.some( + (value) => + typeof value === "string" && ["atmn", "autumn-cli"].includes(value), + ); +}; + +export const isAtmnOAuthClientRecord = ({ + clientId, + name, + metadata, +}: { + clientId: string | null | undefined; + name: string | null | undefined; + metadata?: unknown; +}) => { + if (clientId && configuredAtmnClientIds().has(clientId)) return true; + if (metadataMarksAtmn(metadata)) return true; + + const normalizedName = name?.trim().toLowerCase(); + return !!normalizedName && ATMN_OAUTH_CLIENT_NAMES.has(normalizedName); +}; + +export const isAtmnOAuthClientId = async ({ + db, + clientId, +}: { + db: DrizzleCli; + clientId: string; +}) => { + const client = await oauthClientRepo.getByClientId({ db, clientId }); + + return isAtmnOAuthClientRecord(client ?? { clientId, name: null }); +}; diff --git a/server/src/internal/auth/oauth/handleGetOAuthClient.ts b/server/src/internal/auth/oauth/handleGetOAuthClient.ts new file mode 100644 index 000000000..72c0598e7 --- /dev/null +++ b/server/src/internal/auth/oauth/handleGetOAuthClient.ts @@ -0,0 +1,34 @@ +import type { Context } from "hono"; +import { db } from "@/db/initDrizzle.js"; +import { oauthClientRepo } from "../repos/index.js"; +import { isAtmnOAuthClientRecord } from "./atmnOAuthClients.js"; +import { + getInternalMcpDisplayName, + isInternalMcpOAuthClientRecord, +} from "./internalMcpOAuthClients.js"; + +export const handleGetOAuthClient = async (c: Context) => { + const clientId = c.req.param("client_id"); + const redirectUri = c.req.query("redirect_uri"); + if (!clientId) { + return c.json({ error: "client_id is required" }, 400); + } + + const client = await oauthClientRepo.getByClientId({ db, clientId }); + + if (!client) { + return c.json({ error: "Client not found" }, 404); + } + + const internalMcpName = getInternalMcpDisplayName({ + metadata: client.metadata, + redirectUri, + }); + + return c.json({ + client_id: client.clientId, + name: internalMcpName || client.name || "Unknown Application", + is_atmn: isAtmnOAuthClientRecord(client), + is_internal_mcp: isInternalMcpOAuthClientRecord(client), + }); +}; diff --git a/server/src/internal/auth/oauth/handleMcpOAuthRegistration.ts b/server/src/internal/auth/oauth/handleMcpOAuthRegistration.ts new file mode 100644 index 000000000..f6abd4b61 --- /dev/null +++ b/server/src/internal/auth/oauth/handleMcpOAuthRegistration.ts @@ -0,0 +1,35 @@ +import type { Context } from "hono"; +import { db } from "@/db/initDrizzle.js"; +import { registerMcpOAuthClient } from "../actions/index.js"; + +type RegisterBody = { + redirect_uris?: unknown; + client_name?: unknown; + scope?: unknown; +}; + +const parseJsonObject = async (request: Request) => { + const body = await request.json().catch(() => null); + return body && typeof body === "object" ? (body as RegisterBody) : {}; +}; + +const getRedirectUris = (value: unknown) => + Array.isArray(value) + ? value.filter((uri): uri is string => typeof uri === "string" && !!uri) + : []; + +export const handleMcpOAuthRegistration = async (c: Context) => { + const body = await parseJsonObject(c.req.raw); + const result = await registerMcpOAuthClient({ + db, + clientName: body.client_name, + redirectUris: getRedirectUris(body.redirect_uris), + scope: body.scope, + }); + + if ("error" in result) { + return c.json({ error: result.error }, result.status); + } + + return c.json(result.body, result.status); +}; diff --git a/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts b/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts new file mode 100644 index 000000000..80e803493 --- /dev/null +++ b/server/src/internal/auth/oauth/handleOAuthConsentWithEnv.ts @@ -0,0 +1,98 @@ +import { AppEnv } from "@autumn/shared"; +import type { Context } from "hono"; +import { db } from "@/db/initDrizzle.js"; +import { auth } from "@/utils/auth.js"; +import { oauthConsentRepo } from "../repos/index.js"; +import { isAtmnOAuthClientId } from "./atmnOAuthClients.js"; + +type RequestFields = Record; + +const parseRequestFields = async (request: Request) => { + const contentType = request.headers.get("content-type") ?? ""; + const rawBody = await request.text(); + if (!rawBody) return {}; + + if (contentType.includes("application/json")) { + try { + const body = JSON.parse(rawBody); + return body && typeof body === "object" ? (body as RequestFields) : {}; + } catch { + return {}; + } + } + + const params = new URLSearchParams(rawBody); + return Object.fromEntries(params.entries()); +}; + +const getString = (value: unknown) => + typeof value === "string" && value.length > 0 ? value : null; + +const parseEnv = (value: unknown) => { + if (value === AppEnv.Live || value === AppEnv.Sandbox) return value; + return null; +}; + +const acceptedConsent = (value: unknown) => value === true || value === "true"; + +const getNestedOAuthField = (value: unknown, key: string) => { + if (!value) return null; + + if (typeof value === "string") { + try { + return getString(JSON.parse(value)?.[key]); + } catch { + return new URLSearchParams(value).get(key); + } + } + + if (typeof value === "object") { + return getString((value as Record)[key]); + } + + return null; +}; + +const getClientIdFromFields = (fields: RequestFields) => + getString(fields.client_id) ?? + getNestedOAuthField(fields.oauth_query, "client_id"); + +const getRedirectUriFromFields = (fields: RequestFields) => + getString(fields.redirect_uri) ?? + getString(fields.redirectUri) ?? + getNestedOAuthField(fields.oauth_query, "redirect_uri"); + +export const handleOAuthConsentWithEnv = async (c: Context) => { + const fields = await parseRequestFields(c.req.raw.clone()); + const response = await auth.handler(c.req.raw); + + if (!response.ok || !acceptedConsent(fields.accept)) { + return response; + } + + const clientId = getClientIdFromFields(fields); + const redirectUri = getRedirectUriFromFields(fields); + const env = parseEnv(fields.env); + if (!clientId || !env || (await isAtmnOAuthClientId({ db, clientId }))) { + return response; + } + + const session = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + const userId = session?.user?.id; + const orgId = session?.session?.activeOrganizationId; + if (!userId || !orgId) return response; + + await oauthConsentRepo.updateEnv({ + db, + clientId, + userId, + referenceId: orgId, + env, + redirectUri, + }); + + return response; +}; diff --git a/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts b/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts new file mode 100644 index 000000000..c89458e9e --- /dev/null +++ b/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts @@ -0,0 +1,82 @@ +import { RecaseError } from "@autumn/shared"; +import type { Context } from "hono"; +import { db } from "@/db/initDrizzle.js"; +import { auth } from "@/utils/auth.js"; +import { + getExternalOAuthApiKeyForToken, + getOAuthAccessTokenRecord, + scopesFromOAuthScopeString, +} from "./oauthAccessTokenApiKey.js"; + +const getString = (value: unknown) => + typeof value === "string" && value.length > 0 ? value : null; + +const getResourceFromTokenRequest = async (request: Request) => { + const contentType = request.headers.get("content-type") ?? ""; + const rawBody = await request.text(); + if (!rawBody) return null; + + if (contentType.includes("application/json")) { + try { + const body = JSON.parse(rawBody) as Record; + const resource = body.resource; + if (Array.isArray(resource)) return getString(resource[0]); + return getString(resource); + } catch { + return null; + } + } + + const params = new URLSearchParams(rawBody); + return params.getAll("resource")[0] ?? null; +}; + +export const handleOAuthTokenWithApiKey = async (c: Context) => { + const resource = await getResourceFromTokenRequest(c.req.raw.clone()); + const response = await auth.handler(c.req.raw); + if (!response.ok) return response; + + let body: Record; + try { + body = (await response.clone().json()) as Record; + } catch { + return response; + } + + const accessToken = getString(body.access_token); + if (!accessToken) return response; + + const requestedScopes = scopesFromOAuthScopeString(body.scope); + let apiKeyResult: Awaited>; + try { + const tokenRecord = await getOAuthAccessTokenRecord({ + db, + accessToken, + resource, + requestedScopes, + }); + apiKeyResult = await getExternalOAuthApiKeyForToken({ + db, + tokenRecord, + requestedScopes, + }); + } catch (error) { + if (error instanceof RecaseError) { + return c.json( + { + error: "invalid_grant", + error_description: error.message, + }, + error.statusCode as 400 | 401 | 403, + ); + } + throw error; + } + if (!apiKeyResult) return response; + + return c.json({ + ...body, + access_token: apiKeyResult.apiKey, + scope: apiKeyResult.scopes.join(" "), + }); +}; diff --git a/server/src/internal/auth/oauth/internalMcpOAuthClients.ts b/server/src/internal/auth/oauth/internalMcpOAuthClients.ts new file mode 100644 index 000000000..ff9894d0a --- /dev/null +++ b/server/src/internal/auth/oauth/internalMcpOAuthClients.ts @@ -0,0 +1,103 @@ +import type { Context } from "hono"; +import { type DrizzleCli, db } from "@/db/initDrizzle.js"; +import { auth } from "@/utils/auth.js"; +import { oauthClientRepo } from "../repos/index.js"; + +const INTERNAL_MCP_CLIENT_ID = process.env.INTERNAL_MCP_OAUTH_CLIENT_ID; +const INTERNAL_MCP_CLIENT_NAME = "Autumn internal-mcp"; +const INTERNAL_MCP_CLIENT_NAME_NORMALIZED = + INTERNAL_MCP_CLIENT_NAME.toLowerCase(); +const INTERNAL_MCP_KIND = "internal_mcp"; +const MCP_CLIENT_KIND = "mcp_client"; + +type InternalMcpMetadata = { + kind?: string; + mcpClientType?: string; + redirectNames?: Record; +}; + +const parseMetadata = (metadata: unknown): InternalMcpMetadata => { + if (!metadata) return {}; + if (typeof metadata === "string") { + try { + const parsed = JSON.parse(metadata); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } + } + + return typeof metadata === "object" ? metadata : {}; +}; + +const inferClientNameFromRedirectUri = (redirectUri: string) => { + const normalized = redirectUri.toLowerCase(); + if (normalized.includes("cursor")) return "Cursor"; + if (normalized.includes("claude")) return "Claude"; + if (normalized.includes("opencode")) return "OpenCode"; + if (normalized.includes("open-code")) return "OpenCode"; + if (normalized.includes("slack")) return "Slack"; + if (normalized.includes("codex")) return "Codex"; + return "MCP client"; +}; + +export const isInternalMcpOAuthClientRecord = ({ + clientId, + name, + metadata, +}: { + clientId: string | null | undefined; + name: string | null | undefined; + metadata?: unknown; +}) => { + if (INTERNAL_MCP_CLIENT_ID && clientId === INTERNAL_MCP_CLIENT_ID) + return true; + if (name?.trim().toLowerCase() === INTERNAL_MCP_CLIENT_NAME_NORMALIZED) { + return true; + } + const parsedMetadata = parseMetadata(metadata); + return [INTERNAL_MCP_KIND, MCP_CLIENT_KIND].includes( + parsedMetadata.kind ?? "", + ); +}; + +export const getInternalMcpDisplayName = ({ + metadata, + redirectUri, +}: { + metadata: unknown; + redirectUri: string | null | undefined; +}) => { + if (!redirectUri) return null; + const metadataObject = parseMetadata(metadata); + return ( + metadataObject.redirectNames?.[redirectUri] ?? + inferClientNameFromRedirectUri(redirectUri) + ); +}; + +export const isInternalMcpOAuthClientId = async ({ + db, + clientId, +}: { + db: DrizzleCli; + clientId: string; +}) => { + const client = await oauthClientRepo.getByClientId({ db, clientId }); + + return isInternalMcpOAuthClientRecord(client ?? { clientId, name: null }); +}; + +export const handleInternalMcpOAuthAuthorize = async (c: Context) => { + const url = new URL(c.req.raw.url); + const clientId = url.searchParams.get("client_id"); + if (!clientId || !(await isInternalMcpOAuthClientId({ db, clientId }))) { + return auth.handler(c.req.raw); + } + + const prompts = new Set(url.searchParams.get("prompt")?.split(" ") ?? []); + prompts.add("consent"); + url.searchParams.set("prompt", [...prompts].filter(Boolean).join(" ")); + + return auth.handler(new Request(url, c.req.raw)); +}; diff --git a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts new file mode 100644 index 000000000..81d0ddb10 --- /dev/null +++ b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts @@ -0,0 +1,174 @@ +import { + AppEnv, + checkScopes, + ErrCode, + RecaseError, + type ScopeString, +} from "@autumn/shared"; +import { verifyAccessToken } from "better-auth/oauth2"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { + parseRequestedScopes, + type ResourceAccessTokenRecord, + tokenRecordFromResourceToken, +} from "@/internal/dev/cli/oauthApiKeyUtils.js"; +import { hashOAuthToken } from "@/utils/oauthUtils.js"; +import { oauthAccessTokenRepo, oauthConsentRepo } from "../repos/index.js"; +import { isAtmnOAuthClientId } from "./atmnOAuthClients.js"; +import { getOrCreateOAuthConsentApiKey } from "./oauthConsentApiKey.js"; + +const getOAuthIssuer = () => + `${process.env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`; + +const verifyResourceAccessToken = async ({ + accessToken, + resource, + requestedScopes, +}: { + accessToken: string; + resource: string | null; + requestedScopes: ScopeString[] | null; +}) => { + if (!resource) return null; + + const issuer = getOAuthIssuer(); + try { + const payload = await verifyAccessToken(accessToken, { + jwksUrl: `${issuer}/jwks`, + verifyOptions: { + audience: resource, + issuer, + }, + scopes: requestedScopes ?? undefined, + }); + + return tokenRecordFromResourceToken(payload as Record); + } catch { + return null; + } +}; + +export const getOAuthAccessTokenRecord = async ({ + db, + accessToken, + resource, + requestedScopes, +}: { + db: DrizzleCli; + accessToken: string; + resource: string | null; + requestedScopes: ScopeString[] | null; +}) => { + const hashedToken = await hashOAuthToken(accessToken); + const tokenValues = [...new Set([hashedToken, accessToken])]; + const tokenRecord = + (await oauthAccessTokenRepo.getValidByTokenValues({ db, tokenValues })) ?? + (await verifyResourceAccessToken({ + accessToken, + resource, + requestedScopes, + })); + + if (!tokenRecord) { + throw new RecaseError({ + message: "Invalid or expired access token", + code: ErrCode.InvalidRequest, + statusCode: 401, + }); + } + + if (requestedScopes) { + const { allowed, missing } = checkScopes( + requestedScopes, + tokenRecord.scopes, + ); + if (!allowed) { + throw new RecaseError({ + message: `Insufficient scopes. Missing: ${missing.join(", ")}`, + code: ErrCode.InsufficientScopes, + statusCode: 403, + }); + } + } + + const userId = tokenRecord.userId; + if (!userId) { + throw new RecaseError({ + message: "Token missing user information", + code: ErrCode.InvalidRequest, + statusCode: 401, + }); + } + + const orgId = tokenRecord.referenceId; + if (!orgId) { + throw new RecaseError({ + message: "No organization found. Please select an organization.", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + return tokenRecord as ResourceAccessTokenRecord & { + userId: string; + referenceId: string; + }; +}; + +export const getExternalOAuthApiKeyForToken = async ({ + db, + tokenRecord, + requestedScopes, +}: { + db: DrizzleCli; + tokenRecord: ResourceAccessTokenRecord & { + userId: string; + referenceId: string; + }; + requestedScopes: ScopeString[] | null; +}) => { + const isAtmnClient = await isAtmnOAuthClientId({ + db, + clientId: tokenRecord.clientId, + }); + if (isAtmnClient) return null; + + const consent = await oauthConsentRepo.getForClientUserOrg({ + db, + clientId: tokenRecord.clientId, + userId: tokenRecord.userId, + referenceId: tokenRecord.referenceId, + }); + + if (!consent) { + throw new RecaseError({ + message: "OAuth consent not found", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const env = consent.env ?? AppEnv.Sandbox; + const scopes = requestedScopes ?? (tokenRecord.scopes as ScopeString[]); + const apiKey = await getOrCreateOAuthConsentApiKey({ + db, + consent, + tokenRecord, + env, + scopes, + }); + + return { + apiKey, + env, + orgId: tokenRecord.referenceId, + userId: tokenRecord.userId, + clientId: tokenRecord.clientId, + scopes, + }; +}; + +export const scopesFromOAuthScopeString = (scope: unknown) => { + if (typeof scope !== "string") return null; + return parseRequestedScopes(scope.split(/\s+/).filter(Boolean)); +}; diff --git a/server/src/internal/auth/oauth/oauthConsentApiKey.ts b/server/src/internal/auth/oauth/oauthConsentApiKey.ts new file mode 100644 index 000000000..10dfa43d6 --- /dev/null +++ b/server/src/internal/auth/oauth/oauthConsentApiKey.ts @@ -0,0 +1,136 @@ +import { AppEnv, type ScopeString } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { + ApiKeyPrefix, + createKey, + hashApiKey, +} from "@/internal/dev/api-keys/apiKeyUtils.js"; +import type { ResourceAccessTokenRecord } from "@/internal/dev/cli/oauthApiKeyUtils.js"; +import { decryptData, encryptData } from "@/utils/encryptUtils.js"; +import { + type OAuthConsentApiKeyRecord, + oauthApiKeyRepo, + oauthClientRepo, + oauthConsentRepo, +} from "../repos/index.js"; + +const isKeyForEnv = (apiKey: string, env: AppEnv) => { + const prefix = env === AppEnv.Live ? ApiKeyPrefix.Live : ApiKeyPrefix.Sandbox; + return apiKey.startsWith(`${prefix}_`); +}; + +const getOAuthClientApiKeyName = async ({ + db, + clientId, +}: { + db: DrizzleCli; + clientId: string; +}) => { + const client = await oauthClientRepo.getByClientId({ db, clientId }); + + return `OAuth Key - ${client?.name || clientId.slice(0, 8)}`; +}; + +const createConsentApiKey = async ({ + db, + consent, + tokenRecord, + env, + scopes, +}: { + db: DrizzleCli; + consent: OAuthConsentApiKeyRecord; + tokenRecord: ResourceAccessTokenRecord; + env: AppEnv; + scopes: ScopeString[]; +}) => { + const prefix = env === AppEnv.Live ? ApiKeyPrefix.Live : ApiKeyPrefix.Sandbox; + const keyName = await getOAuthClientApiKeyName({ + db, + clientId: tokenRecord.clientId, + }); + const apiKey = await createKey({ + db, + env, + name: keyName, + orgId: tokenRecord.referenceId!, + userId: tokenRecord.userId ?? undefined, + prefix, + meta: { + oauth_consent_id: consent.id, + oauth_client_id: tokenRecord.clientId, + oauth_redirect_uri: consent.redirectUri, + created_via: "oauth", + generatedAt: new Date().toISOString(), + env, + }, + scopes, + }); + + const hashedKey = hashApiKey(apiKey); + const apiKeyId = await oauthApiKeyRepo.getIdByHashedKey({ db, hashedKey }); + await oauthConsentRepo.updateApiKey({ + db, + consentId: consent.id, + env, + oauthApiKeyId: apiKeyId, + oauthApiKey: encryptData(apiKey), + }); + + return apiKey; +}; + +export const getOrCreateOAuthConsentApiKey = async ({ + db, + consent, + tokenRecord, + env, + scopes, +}: { + db: DrizzleCli; + consent: OAuthConsentApiKeyRecord; + tokenRecord: ResourceAccessTokenRecord; + env: AppEnv; + scopes: ScopeString[]; +}) => { + let existingApiKey: string | null = null; + + if (consent.oauthApiKey) { + try { + existingApiKey = decryptData(consent.oauthApiKey); + } catch { + existingApiKey = null; + } + } + + if (existingApiKey && isKeyForEnv(existingApiKey, env)) { + const keyName = await getOAuthClientApiKeyName({ + db, + clientId: tokenRecord.clientId, + }); + const apiKeyId = await oauthApiKeyRepo.updateLinkedScopes({ + db, + apiKeyId: consent.oauthApiKeyId, + apiKey: existingApiKey, + scopes, + name: keyName, + }); + + if (apiKeyId) { + await oauthConsentRepo.updateApiKeyId({ + db, + consentId: consent.id, + oauthApiKeyId: apiKeyId, + }); + return existingApiKey; + } + } + + await oauthApiKeyRepo.deleteLinked({ + db, + apiKeyId: consent.oauthApiKeyId, + apiKey: existingApiKey, + }); + + return createConsentApiKey({ db, consent, tokenRecord, env, scopes }); +}; diff --git a/server/src/internal/auth/oauth/oauthRouter.ts b/server/src/internal/auth/oauth/oauthRouter.ts new file mode 100644 index 000000000..6e514e2fa --- /dev/null +++ b/server/src/internal/auth/oauth/oauthRouter.ts @@ -0,0 +1,55 @@ +import { + oauthProviderAuthServerMetadata, + oauthProviderOpenIdConfigMetadata, +} from "@better-auth/oauth-provider"; +import { type Context, Hono } from "hono"; +import { rateLimiter } from "hono-rate-limiter"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { auth } from "@/utils/auth.js"; +import { handleGetOAuthClient } from "./handleGetOAuthClient.js"; +import { handleMcpOAuthRegistration } from "./handleMcpOAuthRegistration.js"; +import { handleOAuthConsentWithEnv } from "./handleOAuthConsentWithEnv.js"; +import { handleOAuthTokenWithApiKey } from "./handleOAuthTokenWithApiKey.js"; +import { handleInternalMcpOAuthAuthorize } from "./internalMcpOAuthClients.js"; + +export const oauthRouter = new Hono(); + +const getClientLookupRateLimitKey = (c: Context) => + c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? + c.req.header("x-real-ip") ?? + c.req.header("cf-connecting-ip") ?? + "unknown"; + +const oauthClientLookupLimiter = rateLimiter({ + windowMs: 60 * 1000, + limit: process.env.NODE_ENV === "development" ? 1000 : 60, + standardHeaders: "draft-6", + keyGenerator: getClientLookupRateLimitKey, +}); + +oauthRouter.get("/api/auth/.well-known/openid-configuration", (c) => { + return oauthProviderOpenIdConfigMetadata(auth)(c.req.raw); +}); + +oauthRouter.get("/.well-known/oauth-authorization-server", (c) => { + return oauthProviderAuthServerMetadata(auth)(c.req.raw); +}); + +oauthRouter.get("/api/auth/.well-known/oauth-authorization-server", (c) => { + return oauthProviderAuthServerMetadata(auth)(c.req.raw); +}); + +oauthRouter.get("/.well-known/oauth-authorization-server/api/auth", (c) => { + return oauthProviderAuthServerMetadata(auth)(c.req.raw); +}); + +oauthRouter.post("/api/auth/oauth2/consent", handleOAuthConsentWithEnv); +oauthRouter.post("/api/auth/oauth2/token", handleOAuthTokenWithApiKey); +oauthRouter.get("/api/auth/oauth2/authorize", handleInternalMcpOAuthAuthorize); +oauthRouter.post("/api/auth/oauth2/register", handleMcpOAuthRegistration); + +oauthRouter.get( + "/oauth/client/:client_id", + oauthClientLookupLimiter, + handleGetOAuthClient, +); diff --git a/server/src/internal/auth/repos/index.ts b/server/src/internal/auth/repos/index.ts new file mode 100644 index 000000000..39aa72a56 --- /dev/null +++ b/server/src/internal/auth/repos/index.ts @@ -0,0 +1,11 @@ +export { oauthAccessTokenRepo } from "./oauthAccessTokenRepo.js"; +export { oauthApiKeyRepo } from "./oauthApiKeyRepo.js"; +export { + type OAuthClientRecord, + oauthClientRepo, +} from "./oauthClientRepo.js"; +export { + type OAuthConsentApiKeyRecord, + oauthConsentRepo, +} from "./oauthConsentRepo.js"; +export { oauthRefreshTokenRepo } from "./oauthRefreshTokenRepo.js"; diff --git a/server/src/internal/auth/repos/oauthAccessTokenRepo.ts b/server/src/internal/auth/repos/oauthAccessTokenRepo.ts new file mode 100644 index 000000000..674f6d2ea --- /dev/null +++ b/server/src/internal/auth/repos/oauthAccessTokenRepo.ts @@ -0,0 +1,49 @@ +import { oauthAccessToken } from "@autumn/shared"; +import { and, eq, gt, inArray, isNull } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; + +export const getValidOAuthAccessTokenByTokenValues = async ({ + db, + tokenValues, +}: { + db: DrizzleCli; + tokenValues: string[]; +}) => { + const [token] = await db + .select() + .from(oauthAccessToken) + .where( + and( + inArray(oauthAccessToken.token, tokenValues), + gt(oauthAccessToken.expiresAt, new Date()), + ), + ) + .limit(1); + + return token ?? null; +}; + +export const deleteOAuthAccessTokensByClientAndReference = async ({ + db, + clientId, + referenceId, +}: { + db: DrizzleCli; + clientId: string; + referenceId: string | null; +}) => + db + .delete(oauthAccessToken) + .where( + and( + eq(oauthAccessToken.clientId, clientId), + referenceId + ? eq(oauthAccessToken.referenceId, referenceId) + : isNull(oauthAccessToken.referenceId), + ), + ); + +export const oauthAccessTokenRepo = { + getValidByTokenValues: getValidOAuthAccessTokenByTokenValues, + deleteByClientAndReference: deleteOAuthAccessTokensByClientAndReference, +}; diff --git a/server/src/internal/auth/repos/oauthApiKeyRepo.ts b/server/src/internal/auth/repos/oauthApiKeyRepo.ts new file mode 100644 index 000000000..fbd758518 --- /dev/null +++ b/server/src/internal/auth/repos/oauthApiKeyRepo.ts @@ -0,0 +1,107 @@ +import { apiKeys } from "@autumn/shared"; +import { eq, or, sql } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; +import { clearSecretKeyCache } from "@/internal/dev/api-keys/cacheApiKeyUtils.js"; + +export const deleteOAuthLinkedApiKey = async ({ + db, + apiKeyId, + apiKey, +}: { + db: DrizzleCli; + apiKeyId: string | null; + apiKey: string | null; +}) => { + const hashedKey = apiKey ? hashApiKey(apiKey) : null; + const conditions = [ + apiKeyId ? eq(apiKeys.id, apiKeyId) : null, + hashedKey ? eq(apiKeys.hashed_key, hashedKey) : null, + ].filter((condition) => condition !== null); + + if (conditions.length > 0) { + await db.delete(apiKeys).where(or(...conditions)); + } + + if (hashedKey) { + await clearSecretKeyCache({ hashedKey }); + } +}; + +export const listOAuthApiKeysByConsentId = async ({ + db, + consentId, +}: { + db: DrizzleCli; + consentId: string; +}) => + db + .select({ + id: apiKeys.id, + prefix: apiKeys.prefix, + env: apiKeys.env, + name: apiKeys.name, + hashed_key: apiKeys.hashed_key, + }) + .from(apiKeys) + .where(sql`${apiKeys.meta}->>'oauth_consent_id' = ${consentId}`); + +export const deleteOAuthApiKeyById = async ({ + db, + apiKeyId, +}: { + db: DrizzleCli; + apiKeyId: string; +}) => db.delete(apiKeys).where(eq(apiKeys.id, apiKeyId)); + +export const updateOAuthLinkedApiKeyScopes = async ({ + db, + apiKeyId, + apiKey, + scopes, + name, +}: { + db: DrizzleCli; + apiKeyId: string | null; + apiKey: string; + scopes: string[]; + name: string; +}) => { + const hashedKey = hashApiKey(apiKey); + const conditions = [ + apiKeyId ? eq(apiKeys.id, apiKeyId) : null, + eq(apiKeys.hashed_key, hashedKey), + ].filter((condition) => condition !== null); + + const [updatedKey] = await db + .update(apiKeys) + .set({ name, scopes }) + .where(or(...conditions)) + .returning({ id: apiKeys.id }); + + return updatedKey?.id ?? null; +}; + +export const getApiKeyIdByHashedKey = async ({ + db, + hashedKey, +}: { + db: DrizzleCli; + hashedKey: string; +}) => { + const [keyRecord] = await db + .select({ id: apiKeys.id }) + .from(apiKeys) + .where(eq(apiKeys.hashed_key, hashedKey)) + .limit(1); + + return keyRecord?.id ?? null; +}; + +export const oauthApiKeyRepo = { + listByConsentId: listOAuthApiKeysByConsentId, + deleteById: deleteOAuthApiKeyById, + deleteLinked: deleteOAuthLinkedApiKey, + updateLinkedScopes: updateOAuthLinkedApiKeyScopes, + getIdByHashedKey: getApiKeyIdByHashedKey, +}; diff --git a/server/src/internal/auth/repos/oauthClientRepo.ts b/server/src/internal/auth/repos/oauthClientRepo.ts new file mode 100644 index 000000000..95ddccc9a --- /dev/null +++ b/server/src/internal/auth/repos/oauthClientRepo.ts @@ -0,0 +1,141 @@ +import { oauthClient } from "@autumn/shared"; +import { desc, eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; + +export type OAuthClientRecord = { + id: string; + clientId: string; + name: string | null; + redirectUris: string[]; + scopes: string[] | null; + metadata: unknown; + createdAt: Date | null; +}; + +const oauthClientSelect = { + id: oauthClient.id, + clientId: oauthClient.clientId, + name: oauthClient.name, + redirectUris: oauthClient.redirectUris, + scopes: oauthClient.scopes, + metadata: oauthClient.metadata, + createdAt: oauthClient.createdAt, +}; + +export const listOAuthClients = async ({ db }: { db: DrizzleCli }) => + db.select(oauthClientSelect).from(oauthClient); + +export const listOAuthClientsForAdmin = async ({ db }: { db: DrizzleCli }) => + db + .select({ + id: oauthClient.id, + clientId: oauthClient.clientId, + name: oauthClient.name, + redirectUris: oauthClient.redirectUris, + public: oauthClient.public, + disabled: oauthClient.disabled, + skipConsent: oauthClient.skipConsent, + scopes: oauthClient.scopes, + tokenEndpointAuthMethod: oauthClient.tokenEndpointAuthMethod, + grantTypes: oauthClient.grantTypes, + responseTypes: oauthClient.responseTypes, + createdAt: oauthClient.createdAt, + updatedAt: oauthClient.updatedAt, + }) + .from(oauthClient) + .orderBy(desc(oauthClient.createdAt)); + +export const getOAuthClientByClientId = async ({ + db, + clientId, +}: { + db: DrizzleCli; + clientId: string; +}) => { + const [client] = await db + .select(oauthClientSelect) + .from(oauthClient) + .where(eq(oauthClient.clientId, clientId)) + .limit(1); + + return client ?? null; +}; + +export const updateOAuthClientById = async ({ + db, + id, + updates, +}: { + db: DrizzleCli; + id: string; + updates: { + name: string; + redirectUris: string[]; + scopes: string[]; + tokenEndpointAuthMethod: string; + grantTypes: string[]; + responseTypes: string[]; + public: boolean; + type: string; + metadata: unknown; + updatedAt: Date; + }; +}) => { + const [client] = await db + .update(oauthClient) + .set(updates) + .where(eq(oauthClient.id, id)) + .returning(oauthClientSelect); + + return client ?? null; +}; + +export const upsertOAuthClient = async ({ + db, + insert, + update, +}: { + db: DrizzleCli; + insert: { + id: string; + clientId: string; + name: string; + redirectUris: string[]; + scopes: string[]; + tokenEndpointAuthMethod: string; + grantTypes: string[]; + responseTypes: string[]; + public: boolean; + type: string; + metadata: unknown; + createdAt: Date; + updatedAt: Date; + }; + update: { + name: string; + redirectUris: string[]; + scopes: string[]; + tokenEndpointAuthMethod: string; + grantTypes: string[]; + responseTypes: string[]; + public: boolean; + type: string; + metadata: unknown; + updatedAt: Date; + }; +}) => { + await db.insert(oauthClient).values(insert).onConflictDoUpdate({ + target: oauthClient.clientId, + set: update, + }); + + return getOAuthClientByClientId({ db, clientId: insert.clientId }); +}; + +export const oauthClientRepo = { + list: listOAuthClients, + listForAdmin: listOAuthClientsForAdmin, + getByClientId: getOAuthClientByClientId, + updateById: updateOAuthClientById, + upsert: upsertOAuthClient, +}; diff --git a/server/src/internal/auth/repos/oauthConsentRepo.ts b/server/src/internal/auth/repos/oauthConsentRepo.ts new file mode 100644 index 000000000..2cb7651d4 --- /dev/null +++ b/server/src/internal/auth/repos/oauthConsentRepo.ts @@ -0,0 +1,164 @@ +import { type AppEnv, oauthConsent } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; + +export type OAuthConsentApiKeyRecord = { + id: string; + env: AppEnv | null; + oauthApiKeyId: string | null; + oauthApiKey: string | null; + redirectUri: string | null; +}; + +export const listOAuthConsentsByReferenceId = async ({ + db, + referenceId, +}: { + db: DrizzleCli; + referenceId: string; +}) => + db + .select({ + id: oauthConsent.id, + clientId: oauthConsent.clientId, + userId: oauthConsent.userId, + referenceId: oauthConsent.referenceId, + scopes: oauthConsent.scopes, + createdAt: oauthConsent.createdAt, + updatedAt: oauthConsent.updatedAt, + }) + .from(oauthConsent) + .where(eq(oauthConsent.referenceId, referenceId)); + +export const getOAuthConsentOwner = async ({ + db, + consentId, +}: { + db: DrizzleCli; + consentId: string; +}) => { + const [consent] = await db + .select({ + id: oauthConsent.id, + clientId: oauthConsent.clientId, + referenceId: oauthConsent.referenceId, + }) + .from(oauthConsent) + .where(eq(oauthConsent.id, consentId)) + .limit(1); + + return consent ?? null; +}; + +export const updateOAuthConsentEnv = async ({ + db, + clientId, + userId, + referenceId, + env, + redirectUri, +}: { + db: DrizzleCli; + clientId: string; + userId: string; + referenceId: string; + env: AppEnv; + redirectUri: string | null; +}) => + db + .update(oauthConsent) + .set({ env, redirectUri, updatedAt: new Date() }) + .where( + and( + eq(oauthConsent.clientId, clientId), + eq(oauthConsent.userId, userId), + eq(oauthConsent.referenceId, referenceId), + ), + ); + +export const getOAuthConsentForClientUserOrg = async ({ + db, + clientId, + userId, + referenceId, +}: { + db: DrizzleCli; + clientId: string; + userId: string; + referenceId: string; +}) => { + const [consent] = await db + .select({ + id: oauthConsent.id, + env: oauthConsent.env, + oauthApiKeyId: oauthConsent.oauthApiKeyId, + oauthApiKey: oauthConsent.oauthApiKey, + redirectUri: oauthConsent.redirectUri, + }) + .from(oauthConsent) + .where( + and( + eq(oauthConsent.clientId, clientId), + eq(oauthConsent.userId, userId), + eq(oauthConsent.referenceId, referenceId), + ), + ) + .limit(1); + + return consent ?? null; +}; + +export const updateOAuthConsentApiKey = async ({ + db, + consentId, + env, + oauthApiKeyId, + oauthApiKey, +}: { + db: DrizzleCli; + consentId: string; + env: AppEnv; + oauthApiKeyId: string | null; + oauthApiKey: string; +}) => + db + .update(oauthConsent) + .set({ + env, + oauthApiKeyId, + oauthApiKey, + updatedAt: new Date(), + }) + .where(eq(oauthConsent.id, consentId)); + +export const updateOAuthConsentApiKeyId = async ({ + db, + consentId, + oauthApiKeyId, +}: { + db: DrizzleCli; + consentId: string; + oauthApiKeyId: string; +}) => + db + .update(oauthConsent) + .set({ oauthApiKeyId, updatedAt: new Date() }) + .where(eq(oauthConsent.id, consentId)); + +export const deleteOAuthConsentById = async ({ + db, + consentId, +}: { + db: DrizzleCli; + consentId: string; +}) => db.delete(oauthConsent).where(eq(oauthConsent.id, consentId)); + +export const oauthConsentRepo = { + listByReferenceId: listOAuthConsentsByReferenceId, + getOwner: getOAuthConsentOwner, + updateEnv: updateOAuthConsentEnv, + getForClientUserOrg: getOAuthConsentForClientUserOrg, + updateApiKey: updateOAuthConsentApiKey, + updateApiKeyId: updateOAuthConsentApiKeyId, + deleteById: deleteOAuthConsentById, +}; diff --git a/server/src/internal/auth/repos/oauthRefreshTokenRepo.ts b/server/src/internal/auth/repos/oauthRefreshTokenRepo.ts new file mode 100644 index 000000000..c92fc5bac --- /dev/null +++ b/server/src/internal/auth/repos/oauthRefreshTokenRepo.ts @@ -0,0 +1,27 @@ +import { oauthRefreshToken } from "@autumn/shared"; +import { and, eq, isNull } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; + +export const deleteOAuthRefreshTokensByClientAndReference = async ({ + db, + clientId, + referenceId, +}: { + db: DrizzleCli; + clientId: string; + referenceId: string | null; +}) => + db + .delete(oauthRefreshToken) + .where( + and( + eq(oauthRefreshToken.clientId, clientId), + referenceId + ? eq(oauthRefreshToken.referenceId, referenceId) + : isNull(oauthRefreshToken.referenceId), + ), + ); + +export const oauthRefreshTokenRepo = { + deleteByClientAndReference: deleteOAuthRefreshTokensByClientAndReference, +}; diff --git a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts index 292a48173..f089160b7 100644 --- a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts +++ b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts @@ -1,28 +1,16 @@ -import { - AppEnv, - checkScopes, - ErrCode, - oauthAccessToken, - oauthConsent, - RecaseError, - type ScopeString, - Scopes, -} from "@autumn/shared"; -import { verifyAccessToken } from "better-auth/oauth2"; -import { and, eq, gt } from "drizzle-orm"; +import { AppEnv, ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; -import { hashOAuthToken } from "@/utils/oauthUtils.js"; +import { + getExternalOAuthApiKeyForToken, + getOAuthAccessTokenRecord, +} from "@/internal/auth/oauth/oauthAccessTokenApiKey.js"; import { ApiKeyPrefix, createKey } from "../../api-keys/apiKeyUtils.js"; import { type OAuthApiKeyRequestBody, OAuthApiKeyRequestBodySchema, parseRequestedScopes, - tokenRecordFromResourceToken, } from "../oauthApiKeyUtils.js"; -const getOAuthIssuer = () => - `${process.env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`; - const parseBody = (rawBody: string): OAuthApiKeyRequestBody => { let body: unknown = {}; if (rawBody) { @@ -47,34 +35,6 @@ const parseBody = (rawBody: string): OAuthApiKeyRequestBody => { }); }; -const verifyResourceAccessToken = async ({ - accessToken, - resource, - requestedScopes, -}: { - accessToken: string; - resource: string | null; - requestedScopes: ScopeString[] | null; -}) => { - if (!resource) return null; - - const issuer = getOAuthIssuer(); - try { - const payload = await verifyAccessToken(accessToken, { - jwksUrl: `${issuer}/jwks`, - verifyOptions: { - audience: resource, - issuer, - }, - scopes: requestedScopes ?? undefined, - }); - - return tokenRecordFromResourceToken(payload as Record); - } catch { - return null; - } -}; - /** * Create API keys from an OAuth access token. * Called by the CLI after completing the OAuth flow. @@ -105,90 +65,41 @@ export const handleCreateOAuthApiKeys = createRoute({ const accessToken = authHeader.substring(7); - // Better-auth stores opaque tokens as SHA-256 hashes in base64url format - const hashedToken = await hashOAuthToken(accessToken); - - // Look up the token in the oauth_access_token table - const tokenRecords = await db - .select() - .from(oauthAccessToken) - .where( - and( - eq(oauthAccessToken.token, hashedToken), - gt(oauthAccessToken.expiresAt, new Date()), - ), - ) - .limit(1); - - const tokenRecord = - tokenRecords[0] ?? - (await verifyResourceAccessToken({ - accessToken, - resource, - requestedScopes, - })); - - if (!tokenRecord) { - throw new RecaseError({ - message: "Invalid or expired access token", - code: ErrCode.InvalidRequest, - statusCode: 401, - }); - } - - if (requestedScopes) { - const { allowed, missing } = checkScopes( - requestedScopes, - tokenRecord.scopes, - ); - if (!allowed) { - throw new RecaseError({ - message: `Insufficient scopes. Missing: ${missing.join(", ")}`, - code: ErrCode.InsufficientScopes, - statusCode: 403, - }); - } - } - + const tokenRecord = await getOAuthAccessTokenRecord({ + db, + accessToken, + resource, + requestedScopes, + }); const userId = tokenRecord.userId; - if (!userId) { - throw new RecaseError({ - message: "Token missing user information", - code: ErrCode.InvalidRequest, - statusCode: 401, - }); - } - - // Get the org ID from the referenceId field (set by consentReferenceId) const orgId = tokenRecord.referenceId; - if (!orgId) { - throw new RecaseError({ - message: "No organization found. Please select an organization.", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } - const clientId = tokenRecord.clientId; - // Look up the OAuth consent to get its ID for linking API keys - const consentRecords = await db - .select({ id: oauthConsent.id }) - .from(oauthConsent) - .where( - and( - eq(oauthConsent.clientId, clientId), - eq(oauthConsent.userId, userId), - eq(oauthConsent.referenceId, orgId), - ), - ) - .limit(1); - - const consentId = consentRecords[0]?.id || null; + const externalApiKey = await getExternalOAuthApiKeyForToken({ + db, + tokenRecord, + requestedScopes, + }); + if (externalApiKey) { + return c.json({ + sandbox_key: + externalApiKey.env === AppEnv.Sandbox + ? externalApiKey.apiKey + : undefined, + prod_key: + externalApiKey.env === AppEnv.Live + ? externalApiKey.apiKey + : undefined, + org_id: orgId, + user_id: userId, + client_id: clientId, + scopes: externalApiKey.scopes, + }); + } // Build meta with consent linkage const meta = { - oauth_consent_id: consentId, + oauth_consent_id: null, created_via: "oauth", generatedAt: new Date().toISOString(), }; diff --git a/server/src/internal/misc/consent/handlers/handleGetConsentApiKeys.ts b/server/src/internal/misc/consent/handlers/handleGetConsentApiKeys.ts index 0a43f4a99..35717703b 100644 --- a/server/src/internal/misc/consent/handlers/handleGetConsentApiKeys.ts +++ b/server/src/internal/misc/consent/handlers/handleGetConsentApiKeys.ts @@ -1,7 +1,10 @@ -import { apiKeys, oauthConsent, Scopes } from "@autumn/shared"; -import { eq, sql } from "drizzle-orm"; +import { Scopes } from "@autumn/shared"; import { z } from "zod/v4"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { + oauthApiKeyRepo, + oauthConsentRepo, +} from "@/internal/auth/repos/index.js"; /** * Get API keys linked to a specific OAuth consent. @@ -26,35 +29,28 @@ export const handleGetConsentApiKeys = createRoute({ return c.json({ error: "No organization found" }, 400); } - // First verify the consent belongs to this org - const consentRecords = await db - .select({ id: oauthConsent.id, referenceId: oauthConsent.referenceId }) - .from(oauthConsent) - .where(eq(oauthConsent.id, consent_id)) - .limit(1); + const consent = await oauthConsentRepo.getOwner({ + db, + consentId: consent_id, + }); - if (consentRecords.length === 0) { + if (!consent) { return c.json({ error: "Consent not found" }, 404); } - if (consentRecords[0].referenceId !== org.id) { + if (consent.referenceId !== org.id) { return c.json( { error: "Consent does not belong to this organization" }, 403, ); } - // Query API keys where meta->>'oauth_consent_id' = consent_id - // Only return prefix, env, name - NOT the hashed key - const keys = await db - .select({ - id: apiKeys.id, - prefix: apiKeys.prefix, - env: apiKeys.env, - name: apiKeys.name, + const keys = ( + await oauthApiKeyRepo.listByConsentId({ + db, + consentId: consent_id, }) - .from(apiKeys) - .where(sql`${apiKeys.meta}->>'oauth_consent_id' = ${consent_id}`); + ).map(({ hashed_key: _hashedKey, ...key }) => key); return c.json({ apiKeys: keys }); }, diff --git a/server/src/internal/misc/consent/handlers/handleGetOrgConsents.ts b/server/src/internal/misc/consent/handlers/handleGetOrgConsents.ts index 74ca921df..b3bdee9eb 100644 --- a/server/src/internal/misc/consent/handlers/handleGetOrgConsents.ts +++ b/server/src/internal/misc/consent/handlers/handleGetOrgConsents.ts @@ -1,6 +1,6 @@ -import { ErrCode, oauthConsent, RecaseError, Scopes } from "@autumn/shared"; -import { eq } from "drizzle-orm"; +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { oauthConsentRepo } from "@/internal/auth/repos/index.js"; /** * Get OAuth consents for the current organization. @@ -26,19 +26,10 @@ export const handleGetOrgConsents = createRoute({ }); } - // Query consents where referenceId matches the current org - const consents = await db - .select({ - id: oauthConsent.id, - clientId: oauthConsent.clientId, - userId: oauthConsent.userId, - referenceId: oauthConsent.referenceId, - scopes: oauthConsent.scopes, - createdAt: oauthConsent.createdAt, - updatedAt: oauthConsent.updatedAt, - }) - .from(oauthConsent) - .where(eq(oauthConsent.referenceId, org.id)); + const consents = await oauthConsentRepo.listByReferenceId({ + db, + referenceId: org.id, + }); return c.json({ consents }); }, diff --git a/server/src/internal/misc/consent/handlers/handleRevokeConsent.ts b/server/src/internal/misc/consent/handlers/handleRevokeConsent.ts index ea06ab7aa..27c0470ea 100644 --- a/server/src/internal/misc/consent/handlers/handleRevokeConsent.ts +++ b/server/src/internal/misc/consent/handlers/handleRevokeConsent.ts @@ -1,15 +1,12 @@ -import { - apiKeys, - ErrCode, - oauthAccessToken, - oauthConsent, - oauthRefreshToken, - RecaseError, - Scopes, -} from "@autumn/shared"; -import { and, eq, sql } from "drizzle-orm"; +import { ErrCode, RecaseError, Scopes } from "@autumn/shared"; import { z } from "zod/v4"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { + oauthAccessTokenRepo, + oauthApiKeyRepo, + oauthConsentRepo, + oauthRefreshTokenRepo, +} from "@/internal/auth/repos/index.js"; import { clearSecretKeyCache } from "../../../dev/api-keys/cacheApiKeyUtils.js"; /** @@ -42,18 +39,11 @@ export const handleRevokeConsent = createRoute({ }); } - // 1. Get the consent and verify it belongs to this org - const consentRecords = await db - .select({ - id: oauthConsent.id, - clientId: oauthConsent.clientId, - referenceId: oauthConsent.referenceId, - }) - .from(oauthConsent) - .where(eq(oauthConsent.id, consent_id)) - .limit(1); - - if (consentRecords.length === 0) { + const consent = await oauthConsentRepo.getOwner({ + db, + consentId: consent_id, + }); + if (!consent) { throw new RecaseError({ message: "Consent not found", code: "not_found", @@ -61,8 +51,6 @@ export const handleRevokeConsent = createRoute({ }); } - const consent = consentRecords[0]; - if (consent.referenceId !== org.id) { throw new RecaseError({ message: "Consent does not belong to this organization", @@ -73,51 +61,38 @@ export const handleRevokeConsent = createRoute({ const { clientId, referenceId } = consent; - // 2. Get API keys linked to this consent (for cache invalidation and response) - const linkedKeys = await db - .select({ - id: apiKeys.id, - prefix: apiKeys.prefix, - hashed_key: apiKeys.hashed_key, - }) - .from(apiKeys) - .where(sql`${apiKeys.meta}->>'oauth_consent_id' = ${consent_id}`); + const linkedKeys = await oauthApiKeyRepo.listByConsentId({ + db, + consentId: consent_id, + }); const deletedKeyPrefixes = linkedKeys.map((k) => k.prefix).filter(Boolean); // 3. Delete API keys and invalidate their cache for (const key of linkedKeys) { - // Delete from database - await db.delete(apiKeys).where(eq(apiKeys.id, key.id)); + await oauthApiKeyRepo.deleteById({ db, apiKeyId: key.id }); - // Invalidate cache if (key.hashed_key) { await clearSecretKeyCache({ hashedKey: key.hashed_key }); } } // 4. Delete access tokens for this client + org - await db - .delete(oauthAccessToken) - .where( - and( - eq(oauthAccessToken.clientId, clientId), - eq(oauthAccessToken.referenceId, referenceId), - ), - ); + await oauthAccessTokenRepo.deleteByClientAndReference({ + db, + clientId, + referenceId, + }); // 5. Delete refresh tokens for this client + org - await db - .delete(oauthRefreshToken) - .where( - and( - eq(oauthRefreshToken.clientId, clientId), - eq(oauthRefreshToken.referenceId, referenceId), - ), - ); + await oauthRefreshTokenRepo.deleteByClientAndReference({ + db, + clientId, + referenceId, + }); // 6. Delete the consent - await db.delete(oauthConsent).where(eq(oauthConsent.id, consent_id)); + await oauthConsentRepo.deleteById({ db, consentId: consent_id }); return c.json({ success: true, diff --git a/shared/db/auth-schema.ts b/shared/db/auth-schema.ts index 56d0e5f89..68e678d3f 100644 --- a/shared/db/auth-schema.ts +++ b/shared/db/auth-schema.ts @@ -9,6 +9,7 @@ import { text, timestamp, } from "drizzle-orm/pg-core"; +import type { AppEnv } from "../models/genModels/genEnums.js"; import { type Organization, organizations, @@ -246,6 +247,10 @@ export const oauthConsent = pgTable("oauth_consent", { userId: text("user_id").references(() => user.id, { onDelete: "cascade" }), referenceId: text("reference_id"), scopes: text("scopes").array().notNull(), + env: text("env").$type(), + redirectUri: text("redirect_uri"), + oauthApiKeyId: text("oauth_api_key_id"), + oauthApiKey: text("oauth_api_key"), createdAt: timestamp("created_at", { withTimezone: true }), updatedAt: timestamp("updated_at", { withTimezone: true }), }).enableRLS(); diff --git a/shared/drizzle/0006_sad_madrox.sql b/shared/drizzle/0006_sad_madrox.sql new file mode 100644 index 000000000..9b97ef87b --- /dev/null +++ b/shared/drizzle/0006_sad_madrox.sql @@ -0,0 +1,4 @@ +ALTER TABLE "oauth_consent" ADD COLUMN "env" text;--> statement-breakpoint +ALTER TABLE "oauth_consent" ADD COLUMN "redirect_uri" text;--> statement-breakpoint +ALTER TABLE "oauth_consent" ADD COLUMN "oauth_api_key_id" text;--> statement-breakpoint +ALTER TABLE "oauth_consent" ADD COLUMN "oauth_api_key" text; \ No newline at end of file diff --git a/shared/drizzle/meta/0006_snapshot.json b/shared/drizzle/meta/0006_snapshot.json new file mode 100644 index 000000000..25b04efbc --- /dev/null +++ b/shared/drizzle/meta/0006_snapshot.json @@ -0,0 +1,7383 @@ +{ + "id": "eadc8c95-3f6f-4643-abc3-e90cd56d5ed1", + "prevId": "3ee43a45-bd02-43e2-a2d1-2080d51b5674", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.actions": { + "name": "actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_actions_on_internal_entity_id": { + "name": "idx_actions_on_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "actions_org_id_fkey": { + "name": "actions_org_id_fkey", + "tableFrom": "actions", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "actions_customer_id_fkey": { + "name": "actions_customer_id_fkey", + "tableFrom": "actions", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "actions_entity_id_fkey": { + "name": "actions_entity_id_fkey", + "tableFrom": "actions", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hashed_key": { + "name": "hashed_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_org_id_fkey": { + "name": "api_keys_org_id_fkey", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_hashed_key_key": { + "name": "api_keys_hashed_key_key", + "nullsNotDistinct": false, + "columns": [ + "hashed_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_topup_limit_states": { + "name": "auto_topup_limit_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchase_window_ends_at": { + "name": "purchase_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "purchase_count": { + "name": "purchase_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempt_window_ends_at": { + "name": "attempt_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_attempt_window_ends_at": { + "name": "failed_attempt_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "failed_attempt_count": { + "name": "failed_attempt_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "last_failed_attempt_at": { + "name": "last_failed_attempt_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + } + }, + "indexes": { + "auto_topup_limits_org_env_internal_customer_feature_unique": { + "name": "auto_topup_limits_org_env_internal_customer_feature_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_topup_limits_org_id_fkey": { + "name": "auto_topup_limits_org_id_fkey", + "tableFrom": "auto_topup_limit_states", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_topup_limits_internal_customer_id_fkey": { + "name": "auto_topup_limits_internal_customer_id_fkey", + "tableFrom": "auto_topup_limit_states", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_approvals": { + "name": "chat_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_ts": { + "name": "message_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_args": { + "name": "tool_args", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "preview": { + "name": "preview", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "decided_at": { + "name": "decided_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "decided_by_provider_user_id": { + "name": "decided_by_provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_approvals_org_id_fkey": { + "name": "chat_approvals_org_id_fkey", + "tableFrom": "chat_approvals", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_installations": { + "name": "chat_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bot_access_token": { + "name": "bot_access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_env": { + "name": "default_env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_api_key_id": { + "name": "sandbox_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_api_key": { + "name": "sandbox_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_api_key_id": { + "name": "live_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_api_key": { + "name": "live_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_by_user_id": { + "name": "installed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_by_provider_user_id": { + "name": "installed_by_provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_installations_org_id_fkey": { + "name": "chat_installations_org_id_fkey", + "tableFrom": "chat_installations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_installations_org_provider_key": { + "name": "chat_installations_org_provider_key", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "provider" + ] + }, + "chat_installations_provider_workspace_key": { + "name": "chat_installations_provider_workspace_key", + "nullsNotDistinct": false, + "columns": [ + "provider", + "workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_results": { + "name": "chat_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.checkouts": { + "name": "checkouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "params_version": { + "name": "params_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_checkouts_stripe_invoice_id": { + "name": "idx_checkouts_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_entitlements": { + "name": "customer_entitlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "customer_product_id": { + "name": "customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entitlement_id": { + "name": "entitlement_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unlimited": { + "name": "unlimited", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "next_reset_at": { + "name": "next_reset_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "usage_allowed": { + "name": "usage_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "adjustment": { + "name": "adjustment", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "additional_balance": { + "name": "additional_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "cache_version": { + "name": "cache_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_entitlements_product_id": { + "name": "idx_customer_entitlements_product_id", + "columns": [ + { + "expression": "customer_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_internal_customer_id": { + "name": "idx_customer_entitlements_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "idx_customer_entitlements_internal_customer_id_btree": { + "name": "idx_customer_entitlements_internal_customer_id_btree", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_entitlement_id": { + "name": "idx_customer_entitlements_entitlement_id", + "columns": [ + { + "expression": "entitlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_internal_entity_id": { + "name": "idx_customer_entitlements_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "idx_customer_entitlements_on_next_reset_at": { + "name": "idx_customer_entitlements_on_next_reset_at", + "columns": [ + { + "expression": "next_reset_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_loose_customer_expires": { + "name": "idx_customer_entitlements_loose_customer_expires", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customer_entitlements\".\"customer_product_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlements_internal_feature_id_fkey": { + "name": "entitlements_internal_feature_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "features", + "columnsFrom": [ + "internal_feature_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_entitlements_internal_entity_id_fkey": { + "name": "customer_entitlements_internal_entity_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_entitlements_customer_product_id_fkey": { + "name": "customer_entitlements_customer_product_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "customer_products", + "columnsFrom": [ + "customer_product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "customer_entitlements_entitlement_id_fkey": { + "name": "customer_entitlements_entitlement_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "entitlements", + "columnsFrom": [ + "entitlement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_prices": { + "name": "customer_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_product_id": { + "name": "customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_prices_product_id": { + "name": "idx_customer_prices_product_id", + "columns": [ + { + "expression": "customer_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_prices_price_id": { + "name": "idx_customer_prices_price_id", + "columns": [ + { + "expression": "price_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_prices_internal_customer_id": { + "name": "idx_customer_prices_internal_customer_id", + "columns": [ + { + "expression": "\"internal_customer_id\" COLLATE \"C\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customer_prices\".\"internal_customer_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_prices_customer_product_id_fkey": { + "name": "customer_prices_customer_product_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "customer_products", + "columnsFrom": [ + "customer_product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_prices_internal_customer_id_fkey": { + "name": "customer_prices_internal_customer_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_prices_price_id_fkey": { + "name": "customer_prices_price_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "prices", + "columnsFrom": [ + "price_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_products": { + "name": "customer_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "canceled": { + "name": "canceled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "access_starts_at": { + "name": "access_starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "free_trial_id": { + "name": "free_trial_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "billing_cycle_anchor_resets_at": { + "name": "billing_cycle_anchor_resets_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "collection_method": { + "name": "collection_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'charge_automatically'" + }, + "subscription_ids": { + "name": "subscription_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scheduled_ids": { + "name": "scheduled_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_version": { + "name": "billing_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_version": { + "name": "api_version", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "api_semver": { + "name": "api_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_customer_product_id": { + "name": "previous_customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_trial_end": { + "name": "on_trial_end", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_products_customer_status": { + "name": "idx_customer_products_customer_status", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_on_internal_entity_id": { + "name": "idx_customer_products_on_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_on_internal_product_id": { + "name": "idx_customer_products_on_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_subscription_ids": { + "name": "idx_customer_products_subscription_ids", + "columns": [ + { + "expression": "subscription_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customer_products_scheduled_ids": { + "name": "idx_customer_products_scheduled_ids", + "columns": [ + { + "expression": "scheduled_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customer_products_stripe_checkout_session_id": { + "name": "idx_customer_products_stripe_checkout_session_id", + "columns": [ + { + "expression": "stripe_checkout_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_revenuecat_processor": { + "name": "idx_customer_products_revenuecat_processor", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "(\"customer_products\".\"processor\" ->> 'type') = 'revenuecat'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_products_free_trial_id_fkey": { + "name": "customer_products_free_trial_id_fkey", + "tableFrom": "customer_products", + "tableTo": "free_trials", + "columnsFrom": [ + "free_trial_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "customer_products_internal_customer_id_fkey": { + "name": "customer_products_internal_customer_id_fkey", + "tableFrom": "customer_products", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "customer_products_internal_product_id_fkey": { + "name": "customer_products_internal_product_id_fkey", + "tableFrom": "customer_products", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "customer_products_internal_entity_id_fkey": { + "name": "customer_products_internal_entity_id_fkey", + "tableFrom": "customer_products", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processors": { + "name": "processors", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "send_email_receipts": { + "name": "send_email_receipts", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_topups": { + "name": "auto_topups", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "spend_limits": { + "name": "spend_limits", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "overage_allowed": { + "name": "overage_allowed", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "customers_email_null_id_unique": { + "name": "customers_email_null_id_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"customers\".\"id\" IS NULL AND \"customers\".\"email\" IS NOT NULL AND \"customers\".\"email\" != ''", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_org_env_fingerprint": { + "name": "idx_customers_org_env_fingerprint", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"fingerprint\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processor_id": { + "name": "idx_customers_processor_id", + "columns": [ + { + "expression": "(\"processor\" ->> 'id')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_composite": { + "name": "idx_customers_composite", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_org_env_internal_id": { + "name": "idx_customers_org_env_internal_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"internal_id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_email_trgm": { + "name": "idx_customers_email_trgm", + "columns": [ + { + "expression": "\"email\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_name_trgm": { + "name": "idx_customers_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_id_trgm": { + "name": "idx_customers_id_trgm", + "columns": [ + { + "expression": "\"id\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"id\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_org_id_env_created_at": { + "name": "idx_customers_org_id_env_created_at", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_cursor": { + "name": "idx_customers_cursor", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processors_revenuecat": { + "name": "idx_customers_processors_revenuecat", + "columns": [ + { + "expression": "(\"processors\" ->> 'revenuecat')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processors_vercel": { + "name": "idx_customers_processors_vercel", + "columns": [ + { + "expression": "(\"processors\" ->> 'vercel')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_org_id_fkey": { + "name": "customers_org_id_fkey", + "tableFrom": "customers", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cus_id_constraint": { + "name": "cus_id_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "id", + "env" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.entities": { + "name": "entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spend_limits": { + "name": "spend_limits", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "overage_allowed": { + "name": "overage_allowed", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_entities_internal_customer_id": { + "name": "idx_entities_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_customer_internal_desc": { + "name": "idx_entities_customer_internal_desc", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"internal_id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_org_env_id": { + "name": "idx_entities_org_env_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_cursor": { + "name": "idx_entities_cursor", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entities_internal_customer_id_fkey": { + "name": "entities_internal_customer_id_fkey", + "tableFrom": "entities", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entities_internal_feature_id_fkey": { + "name": "entities_internal_feature_id_fkey", + "tableFrom": "entities", + "tableTo": "features", + "columnsFrom": [ + "internal_feature_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entities_org_id_fkey": { + "name": "entities_org_id_fkey", + "tableFrom": "entities", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entity_id_constraint": { + "name": "entity_id_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "env", + "internal_customer_id", + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entitlements": { + "name": "entitlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_reward_id": { + "name": "internal_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "allowance_type": { + "name": "allowance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowance": { + "name": "allowance", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "interval": { + "name": "interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_count": { + "name": "interval_count", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "carry_from_previous": { + "name": "carry_from_previous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entity_feature_id": { + "name": "entity_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "expiry_duration": { + "name": "expiry_duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_length": { + "name": "expiry_length", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "rollover": { + "name": "rollover", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_entitlements_internal_product_id": { + "name": "idx_entitlements_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_internal_reward_id": { + "name": "idx_entitlements_internal_reward_id", + "columns": [ + { + "expression": "internal_reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_reward_feature": { + "name": "idx_entitlements_reward_feature", + "columns": [ + { + "expression": "internal_reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_feature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_internal_reward_id_c_partial": { + "name": "idx_entitlements_internal_reward_id_c_partial", + "columns": [ + { + "expression": "\"internal_reward_id\" COLLATE \"C\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"entitlements\".\"internal_reward_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlements_internal_feature_id_fkey": { + "name": "entitlements_internal_feature_id_fkey", + "tableFrom": "entitlements", + "tableTo": "features", + "columnsFrom": [ + "internal_feature_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entitlements_internal_product_id_fkey": { + "name": "entitlements_internal_product_id_fkey", + "tableFrom": "entitlements", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "entitlements_internal_reward_id_fkey": { + "name": "entitlements_internal_reward_id_fkey", + "tableFrom": "entitlements", + "tableTo": "rewards", + "columnsFrom": [ + "internal_reward_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entitlements_id_key": { + "name": "entitlements_id_key", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "set_usage": { + "name": "set_usage", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deductions": { + "name": "deductions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_events_internal_customer_id": { + "name": "idx_events_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_events_internal_entity_id": { + "name": "idx_events_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_events_customer_non_usage_ts": { + "name": "idx_events_customer_non_usage_ts", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"timestamp\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"events\".\"set_usage\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_internal_customer_id_fkey": { + "name": "events_internal_customer_id_fkey", + "tableFrom": "events", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_event_constraint": { + "name": "unique_event_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "env", + "customer_id", + "event_name", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.features": { + "name": "features", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "display": { + "name": "display", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "event_names": { + "name": "event_names", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "features_org_id_fkey": { + "name": "features_org_id_fkey", + "tableFrom": "features", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "feature_id_constraint": { + "name": "feature_id_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "id", + "env" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_trials": { + "name": "free_trials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'day'" + }, + "length": { + "name": "length", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unique_fingerprint": { + "name": "unique_fingerprint", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "card_required": { + "name": "card_required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "on_end": { + "name": "on_end", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_free_trials_internal_product_id": { + "name": "idx_free_trials_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_trials_internal_product_id_fkey": { + "name": "free_trials_internal_product_id_fkey", + "tableFrom": "free_trials", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organizations_id_fk": { + "name": "invitation_organization_id_organizations_id_fk", + "tableFrom": "invitation", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.invoice_line_items": { + "name": "invoice_line_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_item_id": { + "name": "stripe_invoice_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_item_id": { + "name": "stripe_subscription_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_product_id": { + "name": "stripe_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_discountable": { + "name": "stripe_discountable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "amount_after_discounts": { + "name": "amount_after_discounts", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "stripe_quantity": { + "name": "stripe_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "total_quantity": { + "name": "total_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "paid_quantity": { + "name": "paid_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_source": { + "name": "description_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_timing": { + "name": "billing_timing", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prorated": { + "name": "prorated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_product_ids": { + "name": "customer_product_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_price_ids": { + "name": "customer_price_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_entitlement_ids": { + "name": "customer_entitlement_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_period_start": { + "name": "effective_period_start", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "effective_period_end": { + "name": "effective_period_end", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "discounts": { + "name": "discounts", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "invoice_line_items_invoice_id_fkey": { + "name": "invoice_line_items_invoice_id_fkey", + "tableFrom": "invoice_line_items", + "tableTo": "invoices", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoice_line_items_stripe_id_unique": { + "name": "invoice_line_items_stripe_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_templates": { + "name": "invoice_templates", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "footer": { + "name": "footer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "net_terms_days": { + "name": "net_terms_days", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_invoice_templates_org_id": { + "name": "idx_invoice_templates_org_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoice_templates_org_id_fkey": { + "name": "invoice_templates_org_id_fkey", + "tableFrom": "invoice_templates", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoice_templates_id_unique": { + "name": "invoice_templates_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoices": { + "name": "invoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "product_ids": { + "name": "product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "internal_product_ids": { + "name": "internal_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_type": { + "name": "processor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "hosted_invoice_url": { + "name": "hosted_invoice_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total": { + "name": "total", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "refunded_amount": { + "name": "refunded_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "discounts": { + "name": "discounts", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "items": { + "name": "items", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "idx_invoices_customer_created": { + "name": "idx_invoices_customer_created", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_internal_entity_id": { + "name": "idx_invoices_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"invoices\".\"internal_entity_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoices_internal_customer_id_fkey": { + "name": "invoices_internal_customer_id_fkey", + "tableFrom": "invoices", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invoices_internal_entity_id_fkey": { + "name": "invoices_internal_entity_id_fkey", + "tableFrom": "invoices", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoices_stripe_id_key": { + "name": "invoices_stripe_id_key", + "nullsNotDistinct": false, + "columns": [ + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organizations_id_fk": { + "name": "member_organization_id_organizations_id_fk", + "tableFrom": "member", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_errors": { + "name": "migration_errors", + "schema": "", + "columns": { + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_job_id": { + "name": "migration_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "migration_customers_internal_customer_id_fkey": { + "name": "migration_customers_internal_customer_id_fkey", + "tableFrom": "migration_errors", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_customers_migration_job_id_fkey": { + "name": "migration_customers_migration_job_id_fkey", + "tableFrom": "migration_errors", + "tableTo": "migration_jobs", + "columnsFrom": [ + "migration_job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "migration_errors_pkey": { + "name": "migration_errors_pkey", + "columns": [ + "internal_customer_id", + "migration_job_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_item_runs": { + "name": "migration_item_runs", + "schema": "", + "columns": { + "migration_item_run_id": { + "name": "migration_item_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_internal_id": { + "name": "migration_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_run_id": { + "name": "migration_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migration_item_runs_live_unique": { + "name": "migration_item_runs_live_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_item_runs\".\"dry_run\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_item_runs_dry_run_unique": { + "name": "migration_item_runs_dry_run_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_item_runs\".\"dry_run\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_item_runs_customer_recent_idx": { + "name": "migration_item_runs_customer_recent_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"updated_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"migration_item_runs\".\"item_kind\" = 'customer'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_jobs": { + "name": "migration_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_step": { + "name": "current_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_internal_product_id": { + "name": "from_internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_internal_product_id": { + "name": "to_internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "step_details": { + "name": "step_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "migration_jobs_from_internal_product_id_fkey": { + "name": "migration_jobs_from_internal_product_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "products", + "columnsFrom": [ + "from_internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_jobs_org_id_fkey": { + "name": "migration_jobs_org_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_jobs_to_internal_product_id_fkey": { + "name": "migration_jobs_to_internal_product_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "products", + "columnsFrom": [ + "to_internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_runs": { + "name": "migration_runs", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_internal_id": { + "name": "migration_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "lazy_run": { + "name": "lazy_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_run_id": { + "name": "trigger_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "only_ids": { + "name": "only_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "target_limit": { + "name": "target_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migration_runs_active_per_migration_unique": { + "name": "migration_runs_active_per_migration_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_runs\".\"status\" IN ('queued', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migration_runs_migration_internal_id_fkey": { + "name": "migration_runs_migration_internal_id_fkey", + "tableFrom": "migration_runs", + "tableTo": "migrations", + "columnsFrom": [ + "migration_internal_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_runs_org_id_fkey": { + "name": "migration_runs_org_id_fkey", + "tableFrom": "migration_runs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migrations": { + "name": "migrations", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "prepared_state": { + "name": "prepared_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "no_billing_changes": { + "name": "no_billing_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "retry_failed": { + "name": "retry_failed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migrations_org_env_id_unique": { + "name": "migrations_org_env_id_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migrations_org_id_fkey": { + "name": "migrations_org_id_fkey", + "tableFrom": "migrations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_api_key_id": { + "name": "oauth_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_api_key": { + "name": "oauth_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_currency": { + "name": "default_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'usd'" + }, + "stripe_connected": { + "name": "stripe_connected", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "stripe_config": { + "name": "stripe_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "test_stripe_connect": { + "name": "test_stripe_connect", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "live_stripe_connect": { + "name": "live_stripe_connect", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "processor_configs": { + "name": "processor_configs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "test_pkey": { + "name": "test_pkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_pkey": { + "name": "live_pkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "svix_config": { + "name": "svix_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "deployed": { + "name": "deployed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "redis_config": { + "name": "redis_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_organizations_name_trgm": { + "name": "idx_organizations_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organizations\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_organizations_slug_trgm": { + "name": "idx_organizations_slug_trgm", + "columns": [ + { + "expression": "\"slug\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organizations\".\"slug\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_organizations_created_at_id": { + "name": "idx_organizations_created_at_id", + "columns": [ + { + "expression": "\"createdAt\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + }, + "organizations_test_pkey_key": { + "name": "organizations_test_pkey_key", + "nullsNotDistinct": false, + "columns": [ + "test_pkey" + ] + }, + "organizations_live_pkey_key": { + "name": "organizations_live_pkey_key", + "nullsNotDistinct": false, + "columns": [ + "live_pkey" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialId_idx": { + "name": "passkey_credentialId_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "passkey_credential_id_unique": { + "name": "passkey_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.prices": { + "name": "prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier_behavior": { + "name": "tier_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entitlement_id": { + "name": "entitlement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "proration_config": { + "name": "proration_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + } + }, + "indexes": { + "idx_prices_internal_product_id": { + "name": "idx_prices_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_prices_entitlement_id": { + "name": "idx_prices_entitlement_id", + "columns": [ + { + "expression": "entitlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prices_entitlement_id_fkey": { + "name": "prices_entitlement_id_fkey", + "tableFrom": "prices", + "tableTo": "entitlements", + "columnsFrom": [ + "entitlement_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "prices_internal_product_id_fkey": { + "name": "prices_internal_product_id_fkey", + "tableFrom": "prices", + "tableTo": "products", + "columnsFrom": [ + "internal_product_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "prices_id_key": { + "name": "prices_id_key", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_add_on": { + "name": "is_add_on", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "version": { + "name": "version", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "base_variant_id": { + "name": "base_variant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_products_org_env_id_version": { + "name": "idx_products_org_env_id_version", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "products_org_id_fkey": { + "name": "products_org_id_fkey", + "tableFrom": "products", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_product": { + "name": "unique_product", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "id", + "env", + "version" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": false + }, + "internal_reward_program_id": { + "name": "internal_reward_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_referral_codes_internal_customer_id": { + "name": "idx_referral_codes_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_codes_internal_customer_id_fkey": { + "name": "referral_codes_internal_customer_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_codes_internal_reward_program_id_fkey": { + "name": "referral_codes_internal_reward_program_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "reward_programs", + "columnsFrom": [ + "internal_reward_program_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_codes_org_id_fkey": { + "name": "referral_codes_org_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_codes_pkey": { + "name": "referral_codes_pkey", + "columns": [ + "code", + "org_id", + "env" + ] + } + }, + "uniqueConstraints": { + "referral_codes_id_key": { + "name": "referral_codes_id_key", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replaceables": { + "name": "replaceables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cus_ent_id": { + "name": "cus_ent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "from_entity_id": { + "name": "from_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delete_next_cycle": { + "name": "delete_next_cycle", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_replaceables_cus_ent_id": { + "name": "idx_replaceables_cus_ent_id", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "replaceables_cus_ent_id_fkey": { + "name": "replaceables_cus_ent_id_fkey", + "tableFrom": "replaceables", + "tableTo": "customer_entitlements", + "columnsFrom": [ + "cus_ent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.revenuecat_mappings": { + "name": "revenuecat_mappings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autumn_product_id": { + "name": "autumn_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revenuecat_product_ids": { + "name": "revenuecat_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "revenuecat_mappings_org_id_fkey": { + "name": "revenuecat_mappings_org_id_fkey", + "tableFrom": "revenuecat_mappings", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revenuecat_mappings_pkey": { + "name": "revenuecat_mappings_pkey", + "columns": [ + "org_id", + "env", + "autumn_product_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reward_programs": { + "name": "reward_programs", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "internal_reward_id": { + "name": "internal_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unlimited_redemptions": { + "name": "unlimited_redemptions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "when": { + "name": "when", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'immediately'" + }, + "product_ids": { + "name": "product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{\"\"}'" + }, + "exclude_trial": { + "name": "exclude_trial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "received_by": { + "name": "received_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reward_triggers_internal_reward_id_fkey": { + "name": "reward_triggers_internal_reward_id_fkey", + "tableFrom": "reward_programs", + "tableTo": "rewards", + "columnsFrom": [ + "internal_reward_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_triggers_org_id_fkey": { + "name": "reward_triggers_org_id_fkey", + "tableFrom": "reward_programs", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reward_redemptions": { + "name": "reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": false + }, + "triggered": { + "name": "triggered", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "internal_reward_program_id": { + "name": "internal_reward_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied": { + "name": "applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "redeemer_applied": { + "name": "redeemer_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "referral_code_id": { + "name": "referral_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_internal_id": { + "name": "reward_internal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promo_code": { + "name": "promo_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_reward_redemptions_referral_code_id": { + "name": "idx_reward_redemptions_referral_code_id", + "columns": [ + { + "expression": "referral_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reward_redemptions_reward_internal_id": { + "name": "idx_reward_redemptions_reward_internal_id", + "columns": [ + { + "expression": "reward_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reward_redemptions_customer_reward": { + "name": "idx_reward_redemptions_customer_reward", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reward_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reward_redemptions_internal_customer_id_fkey": { + "name": "reward_redemptions_internal_customer_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_redemptions_internal_reward_program_id_fkey": { + "name": "reward_redemptions_internal_reward_program_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "reward_programs", + "columnsFrom": [ + "internal_reward_program_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_redemptions_referral_code_id_fkey": { + "name": "reward_redemptions_referral_code_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "referral_codes", + "columnsFrom": [ + "referral_code_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rewards": { + "name": "rewards", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discount_config": { + "name": "discount_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "free_product_config": { + "name": "free_product_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "free_product_id": { + "name": "free_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promo_codes": { + "name": "promo_codes", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "coupons_org_id_fkey": { + "name": "coupons_org_id_fkey", + "tableFrom": "rewards", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollovers": { + "name": "rollovers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cus_ent_id": { + "name": "cus_ent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_rollovers_cus_ent_id": { + "name": "idx_rollovers_cus_ent_id", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rollovers_cus_ent_expires": { + "name": "idx_rollovers_cus_ent_expires", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rollover_cus_ent_id_fkey": { + "name": "rollover_cus_ent_id_fkey", + "tableFrom": "rollovers", + "tableTo": "customer_entitlements", + "columnsFrom": [ + "cus_ent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.phases": { + "name": "phases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "customer_product_ids": { + "name": "customer_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "phases_schedule_id_fkey": { + "name": "phases_schedule_id_fkey", + "tableFrom": "phases", + "tableTo": "schedules", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "phases_schedule_id_starts_at_key": { + "name": "phases_schedule_id_starts_at_key", + "nullsNotDistinct": false, + "columns": [ + "schedule_id", + "starts_at" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedules": { + "name": "schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "schedules_customer_scope_unique": { + "name": "schedules_customer_scope_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"schedules\".\"internal_entity_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedules_entity_scope_unique": { + "name": "schedules_entity_scope_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"schedules\".\"internal_entity_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_schedules_internal_customer_id": { + "name": "idx_schedules_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_schedules_internal_entity_id": { + "name": "idx_schedules_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedules_org_id_fkey": { + "name": "schedules_org_id_fkey", + "tableFrom": "schedules", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedules_internal_customer_id_fkey": { + "name": "schedules_internal_customer_id_fkey", + "tableFrom": "schedules", + "tableTo": "customers", + "columnsFrom": [ + "internal_customer_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedules_internal_entity_id_fkey": { + "name": "schedules_internal_entity_id_fkey", + "tableFrom": "schedules", + "tableTo": "entities", + "columnsFrom": [ + "internal_entity_id" + ], + "columnsTo": [ + "internal_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "usage_features": { + "name": "usage_features", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "subscriptions_org_id_fkey": { + "name": "subscriptions_org_id_fkey", + "tableFrom": "subscriptions", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_stripe_id_key": { + "name": "subscriptions_stripe_id_key", + "nullsNotDistinct": false, + "columns": [ + "stripe_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_name_trgm": { + "name": "idx_user_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_user_email_trgm": { + "name": "idx_user_email_trgm", + "columns": [ + { + "expression": "\"email\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_user_created_at_id": { + "name": "idx_user_created_at_id", + "columns": [ + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_created_by_fkey": { + "name": "user_created_by_fkey", + "tableFrom": "user", + "tableTo": "organizations", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_resources": { + "name": "vercel_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "vercel_resources_installation_name_unique_idx": { + "name": "vercel_resources_installation_name_unique_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status <> 'uninstalled'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vercel_resources_org_id_fkey": { + "name": "vercel_resources_org_id_fkey", + "tableFrom": "vercel_resources", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/shared/drizzle/meta/_journal.json b/shared/drizzle/meta/_journal.json index 9d13cade1..dd96a6ad8 100644 --- a/shared/drizzle/meta/_journal.json +++ b/shared/drizzle/meta/_journal.json @@ -43,6 +43,13 @@ "when": 1780582242747, "tag": "0005_fresh_runaways", "breakpoints": true + }, + { + "idx": 6, + "version": "7", + "when": 1780584591419, + "tag": "0006_sad_madrox", + "breakpoints": true } ] } \ No newline at end of file diff --git a/vite/src/views/auth/Consent.tsx b/vite/src/views/auth/Consent.tsx index 1bdbeb41b..ca223aa04 100644 --- a/vite/src/views/auth/Consent.tsx +++ b/vite/src/views/auth/Consent.tsx @@ -1,27 +1,31 @@ -import { type GroupedPermission, groupAndFormatScopes } from "@autumn/shared"; import { - Check, - ChevronDown, - Clock, - ExternalLink, - Shield, - X, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; + AppEnv, + type GroupedPermission, + groupAndFormatScopes, +} from "@autumn/shared"; +import { Check, Clock, ExternalLink, Shield, X } from "lucide-react"; +import { useEffect, useId, useState } from "react"; import { useSearchParams } from "react-router"; import { toast } from "sonner"; import { CustomToaster } from "@/components/general/CustomToaster"; import { Button } from "@/components/v2/buttons/Button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/v2/selects/Select"; import { authClient, useListOrganizations, useSession, } from "@/lib/auth-client"; -import { cn } from "@/lib/utils"; interface ClientInfo { client_id: string; client_name?: string; + is_atmn?: boolean; client_uri?: string; logo_uri?: string; policy_uri?: string; @@ -90,11 +94,11 @@ const OrgLogo = ({ org }: { org: { name: string; logo?: string | null } }) => { const firstLetter = org?.name?.charAt(0).toUpperCase() || "A"; return ( -
+
{org.logo ? ( {org.name} ) : ( - + {firstLetter} )} @@ -102,11 +106,43 @@ const OrgLogo = ({ org }: { org: { name: string; logo?: string | null } }) => { ); }; +const getConsentRedirectUrl = (data: unknown) => { + if (!data || typeof data !== "object") return null; + const response = data as Record; + + return [response.url, response.uri, response.redirectTo].find( + (value): value is string => typeof value === "string" && value.length > 0, + ); +}; + +const isExternalAppRedirect = (redirectUrl: string) => { + if (!URL.canParse(redirectUrl)) return false; + const protocol = new URL(redirectUrl).protocol; + return protocol !== "http:" && protocol !== "https:"; +}; + +const openConsentRedirect = ({ + onExternalRedirectFallback, + redirectUrl, +}: { + onExternalRedirectFallback: () => void; + redirectUrl: string; +}) => { + const shouldShowFallback = isExternalAppRedirect(redirectUrl); + window.location.href = redirectUrl; + + if (shouldShowFallback) { + window.setTimeout(onExternalRedirectFallback, 1200); + } +}; + export const Consent = () => { const [searchParams] = useSearchParams(); const { data: session } = useSession(); const { data: orgs } = useListOrganizations(); const { data: activeOrganization } = authClient.useActiveOrganization(); + const errorIconMaskId = useId(); + const consentIconMaskId = useId(); const [clientInfo, setClientInfo] = useState(null); const [groupedPermissions, setGroupedPermissions] = useState< @@ -115,31 +151,19 @@ export const Consent = () => { const [jokeScope] = useState(() => getRandomJokeScope()); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); - const [orgDropdownOpen, setOrgDropdownOpen] = useState(false); + const [pendingRedirectUrl, setPendingRedirectUrl] = useState( + null, + ); + const [selectedEnv, setSelectedEnv] = useState(AppEnv.Live); const [switchingOrg, setSwitchingOrg] = useState(false); - const orgDropdownRef = useRef(null); const clientId = searchParams.get("client_id"); + const redirectUri = searchParams.get("redirect_uri"); const requestedScopes = searchParams.get("scope")?.split(" ") || []; // Get the current org (active or first available) const currentOrg = activeOrganization || orgs?.[0]; - // Close dropdown when clicking outside - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - orgDropdownRef.current && - !orgDropdownRef.current.contains(event.target as Node) - ) { - setOrgDropdownOpen(false); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - const handleSwitchOrg = async (orgId: string) => { setSwitchingOrg(true); try { @@ -163,15 +187,21 @@ export const Consent = () => { try { // Fetch client name from our own endpoint - const response = await fetch( + const clientInfoUrl = new URL( `${import.meta.env.VITE_BACKEND_URL}/oauth/client/${encodeURIComponent(clientId)}`, ); + if (redirectUri) { + clientInfoUrl.searchParams.set("redirect_uri", redirectUri); + } + + const response = await fetch(clientInfoUrl.toString()); if (response.ok) { const data = await response.json(); setClientInfo({ client_id: clientId, client_name: data.name || "Unknown Application", + is_atmn: data.is_atmn === true, }); } else { console.error("Error fetching client info:", response.status); @@ -179,6 +209,7 @@ export const Consent = () => { setClientInfo({ client_id: clientId, client_name: "External Application", + is_atmn: false, }); } } catch (error) { @@ -187,6 +218,7 @@ export const Consent = () => { setClientInfo({ client_id: clientId, client_name: "External Application", + is_atmn: false, }); } @@ -197,10 +229,11 @@ export const Consent = () => { } fetchClientInfo(); - }, [clientId, requestedScopes.join(",")]); + }, [clientId, redirectUri, requestedScopes.join(",")]); const handleAuthorize = async () => { setIsSubmitting(true); + setPendingRedirectUrl(null); try { // Use the original requested scopes const grantedScopes = requestedScopes.join(" "); @@ -208,6 +241,13 @@ export const Consent = () => { const { data, error } = await authClient.oauth2.consent({ accept: true, scope: grantedScopes, + client_id: clientId, + redirect_uri: redirectUri, + env: clientInfo.is_atmn ? undefined : selectedEnv, + } as Parameters[0] & { + client_id: string | null; + redirect_uri: string | null; + env?: AppEnv; }); if (error) { @@ -216,12 +256,20 @@ export const Consent = () => { return; } - // Handle redirect - server returns { redirect: true, uri: "..." } - if (data?.uri) { - window.location.href = data.uri; - } else if (data?.redirectTo) { - window.location.href = data.redirectTo; + const redirectUrl = getConsentRedirectUrl(data); + if (redirectUrl) { + if (isExternalAppRedirect(redirectUrl)) { + setPendingRedirectUrl(redirectUrl); + } + openConsentRedirect({ + redirectUrl, + onExternalRedirectFallback: () => setIsSubmitting(false), + }); + return; } + + toast.error("Authorization failed"); + setIsSubmitting(false); } catch (error) { console.error("Authorization error:", error); toast.error("Authorization failed. Please try again."); @@ -231,6 +279,7 @@ export const Consent = () => { const handleCancel = async () => { setIsSubmitting(true); + setPendingRedirectUrl(null); try { const { data, error } = await authClient.oauth2.consent({ accept: false, @@ -242,11 +291,20 @@ export const Consent = () => { return; } - if (data?.uri) { - window.location.href = data.uri; - } else if (data?.redirectTo) { - window.location.href = data.redirectTo; + const redirectUrl = getConsentRedirectUrl(data); + if (redirectUrl) { + if (isExternalAppRedirect(redirectUrl)) { + setPendingRedirectUrl(redirectUrl); + } + openConsentRedirect({ + redirectUrl, + onExternalRedirectFallback: () => setIsSubmitting(false), + }); + return; } + + toast.error("Failed to cancel. Please close this window."); + setIsSubmitting(false); } catch (error) { console.error("Cancel error:", error); toast.error("Failed to cancel. Please close this window."); @@ -254,6 +312,11 @@ export const Consent = () => { } }; + const handleOpenPendingRedirect = () => { + if (!pendingRedirectUrl) return; + window.location.href = pendingRedirectUrl; + }; + if (isLoading) { return (
@@ -271,12 +334,27 @@ export const Consent = () => {
- - - - +

@@ -296,12 +374,27 @@ export const Consent = () => {
{/* Logo */}
- - - - +
@@ -323,70 +416,62 @@ export const Consent = () => { )}
- {/* Organization Selector */} - {currentOrg && ( -
-
-

- Organization -

-
-
- - - {/* Dropdown */} - {orgDropdownOpen && orgs && orgs.length >= 2 && ( -
- {orgs - .filter((org) => org.id !== currentOrg.id) - .map((org) => ( -
{/* Action Buttons */} + {pendingRedirectUrl && !isSubmitting && ( +

+ If {clientInfo.client_name} did not open, use the button below. +

+ )}
From 7c6d7d6d608112bb8c4a76a2df352675a1d8ecf1 Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 17:08:30 +0100 Subject: [PATCH 08/12] fix: mcp oauth --- .../auth/oauth/oauthAccessTokenApiKey.ts | 4 +- .../internal/auth/oauth/oauthConsentApiKey.ts | 79 +- .../internal/auth/repos/oauthApiKeyRepo.ts | 143 +- .../internal/auth/repos/oauthConsentRepo.ts | 20 - .../tests/unit/auth/oauthApiKeyRepo.test.ts | 80 + shared/db/auth-schema.ts | 1 - shared/drizzle/0006_sad_madrox.sql | 3 +- shared/drizzle/meta/0006_snapshot.json | 14368 ++++++++-------- 8 files changed, 7197 insertions(+), 7501 deletions(-) create mode 100644 server/tests/unit/auth/oauthApiKeyRepo.test.ts diff --git a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts index 81d0ddb10..5a767e2c2 100644 --- a/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts +++ b/server/src/internal/auth/oauth/oauthAccessTokenApiKey.ts @@ -15,7 +15,7 @@ import { import { hashOAuthToken } from "@/utils/oauthUtils.js"; import { oauthAccessTokenRepo, oauthConsentRepo } from "../repos/index.js"; import { isAtmnOAuthClientId } from "./atmnOAuthClients.js"; -import { getOrCreateOAuthConsentApiKey } from "./oauthConsentApiKey.js"; +import { rotateOAuthConsentApiKey } from "./oauthConsentApiKey.js"; const getOAuthIssuer = () => `${process.env.BETTER_AUTH_URL?.replace(/\/$/, "") ?? ""}/api/auth`; @@ -150,7 +150,7 @@ export const getExternalOAuthApiKeyForToken = async ({ const env = consent.env ?? AppEnv.Sandbox; const scopes = requestedScopes ?? (tokenRecord.scopes as ScopeString[]); - const apiKey = await getOrCreateOAuthConsentApiKey({ + const apiKey = await rotateOAuthConsentApiKey({ db, consent, tokenRecord, diff --git a/server/src/internal/auth/oauth/oauthConsentApiKey.ts b/server/src/internal/auth/oauth/oauthConsentApiKey.ts index 10dfa43d6..cfa9eccbe 100644 --- a/server/src/internal/auth/oauth/oauthConsentApiKey.ts +++ b/server/src/internal/auth/oauth/oauthConsentApiKey.ts @@ -6,7 +6,6 @@ import { hashApiKey, } from "@/internal/dev/api-keys/apiKeyUtils.js"; import type { ResourceAccessTokenRecord } from "@/internal/dev/cli/oauthApiKeyUtils.js"; -import { decryptData, encryptData } from "@/utils/encryptUtils.js"; import { type OAuthConsentApiKeyRecord, oauthApiKeyRepo, @@ -14,9 +13,9 @@ import { oauthConsentRepo, } from "../repos/index.js"; -const isKeyForEnv = (apiKey: string, env: AppEnv) => { - const prefix = env === AppEnv.Live ? ApiKeyPrefix.Live : ApiKeyPrefix.Sandbox; - return apiKey.startsWith(`${prefix}_`); +type OAuthApiKeyTokenRecord = ResourceAccessTokenRecord & { + userId: string; + referenceId: string; }; const getOAuthClientApiKeyName = async ({ @@ -40,7 +39,7 @@ const createConsentApiKey = async ({ }: { db: DrizzleCli; consent: OAuthConsentApiKeyRecord; - tokenRecord: ResourceAccessTokenRecord; + tokenRecord: OAuthApiKeyTokenRecord; env: AppEnv; scopes: ScopeString[]; }) => { @@ -53,7 +52,7 @@ const createConsentApiKey = async ({ db, env, name: keyName, - orgId: tokenRecord.referenceId!, + orgId: tokenRecord.referenceId, userId: tokenRecord.userId ?? undefined, prefix, meta: { @@ -69,18 +68,21 @@ const createConsentApiKey = async ({ const hashedKey = hashApiKey(apiKey); const apiKeyId = await oauthApiKeyRepo.getIdByHashedKey({ db, hashedKey }); + if (!apiKeyId) { + throw new Error("OAuth API key was not persisted"); + } + await oauthConsentRepo.updateApiKey({ db, consentId: consent.id, env, oauthApiKeyId: apiKeyId, - oauthApiKey: encryptData(apiKey), }); - return apiKey; + return { apiKey, apiKeyId }; }; -export const getOrCreateOAuthConsentApiKey = async ({ +export const rotateOAuthConsentApiKey = async ({ db, consent, tokenRecord, @@ -89,48 +91,31 @@ export const getOrCreateOAuthConsentApiKey = async ({ }: { db: DrizzleCli; consent: OAuthConsentApiKeyRecord; - tokenRecord: ResourceAccessTokenRecord; + tokenRecord: OAuthApiKeyTokenRecord; env: AppEnv; scopes: ScopeString[]; }) => { - let existingApiKey: string | null = null; - - if (consent.oauthApiKey) { - try { - existingApiKey = decryptData(consent.oauthApiKey); - } catch { - existingApiKey = null; - } - } - - if (existingApiKey && isKeyForEnv(existingApiKey, env)) { - const keyName = await getOAuthClientApiKeyName({ - db, - clientId: tokenRecord.clientId, - }); - const apiKeyId = await oauthApiKeyRepo.updateLinkedScopes({ - db, - apiKeyId: consent.oauthApiKeyId, - apiKey: existingApiKey, - scopes, - name: keyName, - }); - - if (apiKeyId) { - await oauthConsentRepo.updateApiKeyId({ - db, - consentId: consent.id, - oauthApiKeyId: apiKeyId, - }); - return existingApiKey; - } - } - - await oauthApiKeyRepo.deleteLinked({ + const previousApiKeyId = consent.oauthApiKeyId; + const { apiKey, apiKeyId } = await createConsentApiKey({ db, - apiKeyId: consent.oauthApiKeyId, - apiKey: existingApiKey, + consent, + tokenRecord, + env, + scopes, }); - return createConsentApiKey({ db, consent, tokenRecord, env, scopes }); + if (previousApiKeyId && previousApiKeyId !== apiKeyId) { + await oauthApiKeyRepo.deleteConsentLinked({ + db, + apiKeyId: previousApiKeyId, + consentId: consent.id, + clientId: tokenRecord.clientId, + redirectUri: consent.redirectUri, + orgId: tokenRecord.referenceId, + userId: tokenRecord.userId, + env, + }); + } + + return apiKey; }; diff --git a/server/src/internal/auth/repos/oauthApiKeyRepo.ts b/server/src/internal/auth/repos/oauthApiKeyRepo.ts index fbd758518..dd74ba4e2 100644 --- a/server/src/internal/auth/repos/oauthApiKeyRepo.ts +++ b/server/src/internal/auth/repos/oauthApiKeyRepo.ts @@ -1,31 +1,109 @@ -import { apiKeys } from "@autumn/shared"; -import { eq, or, sql } from "drizzle-orm"; +import { type AppEnv, apiKeys } from "@autumn/shared"; +import { eq, sql } from "drizzle-orm"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; import { clearSecretKeyCache } from "@/internal/dev/api-keys/cacheApiKeyUtils.js"; -export const deleteOAuthLinkedApiKey = async ({ +type OAuthApiKeyRecord = { + id: string; + orgId: string | null; + userId: string | null; + env: string | null; + hashedKey: string | null; + meta: unknown; +}; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +export const isOAuthConsentLinkedApiKey = ({ + apiKey, + consentId, + clientId, + redirectUri, + orgId, + userId, + env, +}: { + apiKey: OAuthApiKeyRecord; + consentId: string; + clientId: string; + redirectUri: string | null; + orgId: string; + userId: string; + env: AppEnv; +}) => { + if ( + apiKey.orgId !== orgId || + apiKey.userId !== userId || + apiKey.env !== env || + !isRecord(apiKey.meta) + ) { + return false; + } + + return ( + apiKey.meta.created_via === "oauth" && + apiKey.meta.oauth_consent_id === consentId && + apiKey.meta.oauth_client_id === clientId && + apiKey.meta.oauth_redirect_uri === redirectUri && + apiKey.meta.env === env + ); +}; + +export const deleteOAuthConsentLinkedApiKey = async ({ db, apiKeyId, - apiKey, + consentId, + clientId, + redirectUri, + orgId, + userId, + env, }: { db: DrizzleCli; - apiKeyId: string | null; - apiKey: string | null; + apiKeyId: string; + consentId: string; + clientId: string; + redirectUri: string | null; + orgId: string; + userId: string; + env: AppEnv; }) => { - const hashedKey = apiKey ? hashApiKey(apiKey) : null; - const conditions = [ - apiKeyId ? eq(apiKeys.id, apiKeyId) : null, - hashedKey ? eq(apiKeys.hashed_key, hashedKey) : null, - ].filter((condition) => condition !== null); + const [apiKey] = await db + .select({ + id: apiKeys.id, + orgId: apiKeys.org_id, + userId: apiKeys.user_id, + env: apiKeys.env, + hashedKey: apiKeys.hashed_key, + meta: apiKeys.meta, + }) + .from(apiKeys) + .where(eq(apiKeys.id, apiKeyId)) + .limit(1); - if (conditions.length > 0) { - await db.delete(apiKeys).where(or(...conditions)); + if (!apiKey) return { deleted: false, reason: "not_found" as const }; + + if ( + !isOAuthConsentLinkedApiKey({ + apiKey, + consentId, + clientId, + redirectUri, + orgId, + userId, + env, + }) + ) { + return { deleted: false, reason: "guard_failed" as const }; } - if (hashedKey) { - await clearSecretKeyCache({ hashedKey }); - } + await db.delete(apiKeys).where(eq(apiKeys.id, apiKeyId)); + + if (apiKey.hashedKey) + await clearSecretKeyCache({ hashedKey: apiKey.hashedKey }); + + return { deleted: true, reason: null }; }; export const listOAuthApiKeysByConsentId = async ({ @@ -54,34 +132,6 @@ export const deleteOAuthApiKeyById = async ({ apiKeyId: string; }) => db.delete(apiKeys).where(eq(apiKeys.id, apiKeyId)); -export const updateOAuthLinkedApiKeyScopes = async ({ - db, - apiKeyId, - apiKey, - scopes, - name, -}: { - db: DrizzleCli; - apiKeyId: string | null; - apiKey: string; - scopes: string[]; - name: string; -}) => { - const hashedKey = hashApiKey(apiKey); - const conditions = [ - apiKeyId ? eq(apiKeys.id, apiKeyId) : null, - eq(apiKeys.hashed_key, hashedKey), - ].filter((condition) => condition !== null); - - const [updatedKey] = await db - .update(apiKeys) - .set({ name, scopes }) - .where(or(...conditions)) - .returning({ id: apiKeys.id }); - - return updatedKey?.id ?? null; -}; - export const getApiKeyIdByHashedKey = async ({ db, hashedKey, @@ -101,7 +151,6 @@ export const getApiKeyIdByHashedKey = async ({ export const oauthApiKeyRepo = { listByConsentId: listOAuthApiKeysByConsentId, deleteById: deleteOAuthApiKeyById, - deleteLinked: deleteOAuthLinkedApiKey, - updateLinkedScopes: updateOAuthLinkedApiKeyScopes, + deleteConsentLinked: deleteOAuthConsentLinkedApiKey, getIdByHashedKey: getApiKeyIdByHashedKey, }; diff --git a/server/src/internal/auth/repos/oauthConsentRepo.ts b/server/src/internal/auth/repos/oauthConsentRepo.ts index 2cb7651d4..cd01a9240 100644 --- a/server/src/internal/auth/repos/oauthConsentRepo.ts +++ b/server/src/internal/auth/repos/oauthConsentRepo.ts @@ -6,7 +6,6 @@ export type OAuthConsentApiKeyRecord = { id: string; env: AppEnv | null; oauthApiKeyId: string | null; - oauthApiKey: string | null; redirectUri: string | null; }; @@ -92,7 +91,6 @@ export const getOAuthConsentForClientUserOrg = async ({ id: oauthConsent.id, env: oauthConsent.env, oauthApiKeyId: oauthConsent.oauthApiKeyId, - oauthApiKey: oauthConsent.oauthApiKey, redirectUri: oauthConsent.redirectUri, }) .from(oauthConsent) @@ -113,38 +111,21 @@ export const updateOAuthConsentApiKey = async ({ consentId, env, oauthApiKeyId, - oauthApiKey, }: { db: DrizzleCli; consentId: string; env: AppEnv; oauthApiKeyId: string | null; - oauthApiKey: string; }) => db .update(oauthConsent) .set({ env, oauthApiKeyId, - oauthApiKey, updatedAt: new Date(), }) .where(eq(oauthConsent.id, consentId)); -export const updateOAuthConsentApiKeyId = async ({ - db, - consentId, - oauthApiKeyId, -}: { - db: DrizzleCli; - consentId: string; - oauthApiKeyId: string; -}) => - db - .update(oauthConsent) - .set({ oauthApiKeyId, updatedAt: new Date() }) - .where(eq(oauthConsent.id, consentId)); - export const deleteOAuthConsentById = async ({ db, consentId, @@ -159,6 +140,5 @@ export const oauthConsentRepo = { updateEnv: updateOAuthConsentEnv, getForClientUserOrg: getOAuthConsentForClientUserOrg, updateApiKey: updateOAuthConsentApiKey, - updateApiKeyId: updateOAuthConsentApiKeyId, deleteById: deleteOAuthConsentById, }; diff --git a/server/tests/unit/auth/oauthApiKeyRepo.test.ts b/server/tests/unit/auth/oauthApiKeyRepo.test.ts new file mode 100644 index 000000000..6a2aabc25 --- /dev/null +++ b/server/tests/unit/auth/oauthApiKeyRepo.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, test } from "bun:test"; +import { AppEnv } from "@autumn/shared"; +import { isOAuthConsentLinkedApiKey } from "@/internal/auth/repos/oauthApiKeyRepo.js"; + +type GuardApiKey = Parameters[0]["apiKey"]; + +const oauthMeta = { + created_via: "oauth", + oauth_consent_id: "consent_123", + oauth_client_id: "autumn_mcp_cursor", + oauth_redirect_uri: "cursor://oauth/callback", + env: AppEnv.Sandbox, +}; + +const baseApiKey: GuardApiKey = { + id: "key_123", + orgId: "org_123", + userId: "user_123", + env: AppEnv.Sandbox, + hashedKey: "hashed", + meta: oauthMeta, +}; + +const matchesConsent = (apiKey: GuardApiKey) => + isOAuthConsentLinkedApiKey({ + apiKey, + consentId: "consent_123", + clientId: "autumn_mcp_cursor", + redirectUri: "cursor://oauth/callback", + orgId: "org_123", + userId: "user_123", + env: AppEnv.Sandbox, + }); + +describe("isOAuthConsentLinkedApiKey", () => { + test("accepts an OAuth-created key linked to the same consent", () => { + expect(matchesConsent(baseApiKey)).toBe(true); + }); + + test("rejects a user-created key even if it is the stored api key id", () => { + expect( + matchesConsent({ + ...baseApiKey, + meta: { created_via: "dashboard" }, + }), + ).toBe(false); + }); + + test("rejects an OAuth key linked to a different consent", () => { + expect( + matchesConsent({ + ...baseApiKey, + meta: { + ...oauthMeta, + oauth_consent_id: "consent_other", + }, + }), + ).toBe(false); + }); + + test("rejects an OAuth key linked to a different redirect URI", () => { + expect( + isOAuthConsentLinkedApiKey({ + apiKey: baseApiKey, + consentId: "consent_123", + clientId: "autumn_mcp_cursor", + redirectUri: "cursor://oauth/other-callback", + orgId: "org_123", + userId: "user_123", + env: AppEnv.Sandbox, + }), + ).toBe(false); + }); + + test("rejects an OAuth key with different ownership or env", () => { + expect(matchesConsent({ ...baseApiKey, orgId: "org_other" })).toBe(false); + expect(matchesConsent({ ...baseApiKey, userId: "user_other" })).toBe(false); + expect(matchesConsent({ ...baseApiKey, env: AppEnv.Live })).toBe(false); + }); +}); diff --git a/shared/db/auth-schema.ts b/shared/db/auth-schema.ts index 68e678d3f..4052f85f6 100644 --- a/shared/db/auth-schema.ts +++ b/shared/db/auth-schema.ts @@ -250,7 +250,6 @@ export const oauthConsent = pgTable("oauth_consent", { env: text("env").$type(), redirectUri: text("redirect_uri"), oauthApiKeyId: text("oauth_api_key_id"), - oauthApiKey: text("oauth_api_key"), createdAt: timestamp("created_at", { withTimezone: true }), updatedAt: timestamp("updated_at", { withTimezone: true }), }).enableRLS(); diff --git a/shared/drizzle/0006_sad_madrox.sql b/shared/drizzle/0006_sad_madrox.sql index 9b97ef87b..895b1fe67 100644 --- a/shared/drizzle/0006_sad_madrox.sql +++ b/shared/drizzle/0006_sad_madrox.sql @@ -1,4 +1,3 @@ ALTER TABLE "oauth_consent" ADD COLUMN "env" text;--> statement-breakpoint ALTER TABLE "oauth_consent" ADD COLUMN "redirect_uri" text;--> statement-breakpoint -ALTER TABLE "oauth_consent" ADD COLUMN "oauth_api_key_id" text;--> statement-breakpoint -ALTER TABLE "oauth_consent" ADD COLUMN "oauth_api_key" text; \ No newline at end of file +ALTER TABLE "oauth_consent" ADD COLUMN "oauth_api_key_id" text; diff --git a/shared/drizzle/meta/0006_snapshot.json b/shared/drizzle/meta/0006_snapshot.json index 25b04efbc..c4b1f542f 100644 --- a/shared/drizzle/meta/0006_snapshot.json +++ b/shared/drizzle/meta/0006_snapshot.json @@ -1,7383 +1,6987 @@ { - "id": "eadc8c95-3f6f-4643-abc3-e90cd56d5ed1", - "prevId": "3ee43a45-bd02-43e2-a2d1-2080d51b5674", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "account_userId_idx": { - "name": "account_userId_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "account_user_id_user_id_fk": { - "name": "account_user_id_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.actions": { - "name": "actions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "request_id": { - "name": "request_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "org_slug": { - "name": "org_slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "entity_id": { - "name": "entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_entity_id": { - "name": "internal_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "auth_type": { - "name": "auth_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "method": { - "name": "method", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "properties": { - "name": "properties", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_actions_on_internal_entity_id": { - "name": "idx_actions_on_internal_entity_id", - "columns": [ - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "actions_org_id_fkey": { - "name": "actions_org_id_fkey", - "tableFrom": "actions", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "actions_customer_id_fkey": { - "name": "actions_customer_id_fkey", - "tableFrom": "actions", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "actions_entity_id_fkey": { - "name": "actions_entity_id_fkey", - "tableFrom": "actions", - "tableTo": "entities", - "columnsFrom": [ - "internal_entity_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.api_keys": { - "name": "api_keys", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "hashed_key": { - "name": "hashed_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "meta": { - "name": "meta", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "api_keys_org_id_fkey": { - "name": "api_keys_org_id_fkey", - "tableFrom": "api_keys", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "api_keys_hashed_key_key": { - "name": "api_keys_hashed_key_key", - "nullsNotDistinct": false, - "columns": [ - "hashed_key" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.auto_topup_limit_states": { - "name": "auto_topup_limit_states", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "feature_id": { - "name": "feature_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "purchase_window_ends_at": { - "name": "purchase_window_ends_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "purchase_count": { - "name": "purchase_count", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "attempt_window_ends_at": { - "name": "attempt_window_ends_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "attempt_count": { - "name": "attempt_count", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "failed_attempt_window_ends_at": { - "name": "failed_attempt_window_ends_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "failed_attempt_count": { - "name": "failed_attempt_count", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "last_attempt_at": { - "name": "last_attempt_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "last_failed_attempt_at": { - "name": "last_failed_attempt_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - } - }, - "indexes": { - "auto_topup_limits_org_env_internal_customer_feature_unique": { - "name": "auto_topup_limits_org_env_internal_customer_feature_unique", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "feature_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "auto_topup_limits_org_id_fkey": { - "name": "auto_topup_limits_org_id_fkey", - "tableFrom": "auto_topup_limit_states", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "auto_topup_limits_internal_customer_id_fkey": { - "name": "auto_topup_limits_internal_customer_id_fkey", - "tableFrom": "auto_topup_limit_states", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chat_approvals": { - "name": "chat_approvals", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider": { - "name": "provider", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "channel_id": { - "name": "channel_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "message_ts": { - "name": "message_ts", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "provider_user_id": { - "name": "provider_user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "run_id": { - "name": "run_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tool_call_id": { - "name": "tool_call_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tool_name": { - "name": "tool_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "tool_args": { - "name": "tool_args", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "preview": { - "name": "preview", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "expires_at": { - "name": "expires_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "decided_at": { - "name": "decided_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "decided_by_provider_user_id": { - "name": "decided_by_provider_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "chat_approvals_org_id_fkey": { - "name": "chat_approvals_org_id_fkey", - "tableFrom": "chat_approvals", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chat_installations": { - "name": "chat_installations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider": { - "name": "provider", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_id": { - "name": "workspace_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "workspace_name": { - "name": "workspace_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "bot_user_id": { - "name": "bot_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "bot_access_token": { - "name": "bot_access_token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "scopes": { - "name": "scopes", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "default_env": { - "name": "default_env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "sandbox_api_key_id": { - "name": "sandbox_api_key_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sandbox_api_key": { - "name": "sandbox_api_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "live_api_key_id": { - "name": "live_api_key_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "live_api_key": { - "name": "live_api_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "installed_by_user_id": { - "name": "installed_by_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "installed_by_provider_user_id": { - "name": "installed_by_provider_user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - } - }, - "indexes": {}, - "foreignKeys": { - "chat_installations_org_id_fkey": { - "name": "chat_installations_org_id_fkey", - "tableFrom": "chat_installations", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "chat_installations_org_provider_key": { - "name": "chat_installations_org_provider_key", - "nullsNotDistinct": false, - "columns": [ - "org_id", - "provider" - ] - }, - "chat_installations_provider_workspace_key": { - "name": "chat_installations_provider_workspace_key", - "nullsNotDistinct": false, - "columns": [ - "provider", - "workspace_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chat_results": { - "name": "chat_results", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.checkouts": { - "name": "checkouts", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "params": { - "name": "params", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "params_version": { - "name": "params_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "response": { - "name": "response", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "stripe_invoice_id": { - "name": "stripe_invoice_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "expires_at": { - "name": "expires_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "completed_at": { - "name": "completed_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_checkouts_stripe_invoice_id": { - "name": "idx_checkouts_stripe_invoice_id", - "columns": [ - { - "expression": "stripe_invoice_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.customer_entitlements": { - "name": "customer_entitlements", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "customer_product_id": { - "name": "customer_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "entitlement_id": { - "name": "entitlement_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text COLLATE \"C\"", - "primaryKey": false, - "notNull": true - }, - "internal_entity_id": { - "name": "internal_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_feature_id": { - "name": "internal_feature_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "unlimited": { - "name": "unlimited", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "balance": { - "name": "balance", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "next_reset_at": { - "name": "next_reset_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "usage_allowed": { - "name": "usage_allowed", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "adjustment": { - "name": "adjustment", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "additional_balance": { - "name": "additional_balance", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "entities": { - "name": "entities", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "cache_version": { - "name": "cache_version", - "type": "integer", - "primaryKey": false, - "notNull": false, - "default": 0 - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "feature_id": { - "name": "feature_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "external_id": { - "name": "external_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_customer_entitlements_product_id": { - "name": "idx_customer_entitlements_product_id", - "columns": [ - { - "expression": "customer_product_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_entitlements_internal_customer_id": { - "name": "idx_customer_entitlements_internal_customer_id", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "hash", - "with": {} - }, - "idx_customer_entitlements_internal_customer_id_btree": { - "name": "idx_customer_entitlements_internal_customer_id_btree", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_entitlements_entitlement_id": { - "name": "idx_customer_entitlements_entitlement_id", - "columns": [ - { - "expression": "entitlement_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_entitlements_internal_entity_id": { - "name": "idx_customer_entitlements_internal_entity_id", - "columns": [ - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "hash", - "with": {} - }, - "idx_customer_entitlements_on_next_reset_at": { - "name": "idx_customer_entitlements_on_next_reset_at", - "columns": [ - { - "expression": "next_reset_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_entitlements_loose_customer_expires": { - "name": "idx_customer_entitlements_loose_customer_expires", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"customer_entitlements\".\"customer_product_id\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "entitlements_internal_feature_id_fkey": { - "name": "entitlements_internal_feature_id_fkey", - "tableFrom": "customer_entitlements", - "tableTo": "features", - "columnsFrom": [ - "internal_feature_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "customer_entitlements_internal_entity_id_fkey": { - "name": "customer_entitlements_internal_entity_id_fkey", - "tableFrom": "customer_entitlements", - "tableTo": "entities", - "columnsFrom": [ - "internal_entity_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "customer_entitlements_customer_product_id_fkey": { - "name": "customer_entitlements_customer_product_id_fkey", - "tableFrom": "customer_entitlements", - "tableTo": "customer_products", - "columnsFrom": [ - "customer_product_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "customer_entitlements_entitlement_id_fkey": { - "name": "customer_entitlements_entitlement_id_fkey", - "tableFrom": "customer_entitlements", - "tableTo": "entitlements", - "columnsFrom": [ - "entitlement_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.customer_prices": { - "name": "customer_prices", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "price_id": { - "name": "price_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "options": { - "name": "options", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "customer_product_id": { - "name": "customer_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_customer_prices_product_id": { - "name": "idx_customer_prices_product_id", - "columns": [ - { - "expression": "customer_product_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_prices_price_id": { - "name": "idx_customer_prices_price_id", - "columns": [ - { - "expression": "price_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_prices_internal_customer_id": { - "name": "idx_customer_prices_internal_customer_id", - "columns": [ - { - "expression": "\"internal_customer_id\" COLLATE \"C\"", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"customer_prices\".\"internal_customer_id\" IS NOT NULL", - "concurrently": true, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "customer_prices_customer_product_id_fkey": { - "name": "customer_prices_customer_product_id_fkey", - "tableFrom": "customer_prices", - "tableTo": "customer_products", - "columnsFrom": [ - "customer_product_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "customer_prices_internal_customer_id_fkey": { - "name": "customer_prices_internal_customer_id_fkey", - "tableFrom": "customer_prices", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "customer_prices_price_id_fkey": { - "name": "customer_prices_price_id_fkey", - "tableFrom": "customer_prices", - "tableTo": "prices", - "columnsFrom": [ - "price_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.customer_products": { - "name": "customer_products", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_product_id": { - "name": "internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_entity_id": { - "name": "internal_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "processor": { - "name": "processor", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "canceled": { - "name": "canceled", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "canceled_at": { - "name": "canceled_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "ended_at": { - "name": "ended_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "starts_at": { - "name": "starts_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "access_starts_at": { - "name": "access_starts_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "options": { - "name": "options", - "type": "jsonb[]", - "primaryKey": false, - "notNull": false - }, - "product_id": { - "name": "product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "free_trial_id": { - "name": "free_trial_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "trial_ends_at": { - "name": "trial_ends_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "billing_cycle_anchor_resets_at": { - "name": "billing_cycle_anchor_resets_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "collection_method": { - "name": "collection_method", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'charge_automatically'" - }, - "subscription_ids": { - "name": "subscription_ids", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "scheduled_ids": { - "name": "scheduled_ids", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "quantity": { - "name": "quantity", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": 1 - }, - "is_custom": { - "name": "is_custom", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "entity_id": { - "name": "entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "billing_version": { - "name": "billing_version", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "api_version": { - "name": "api_version", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "api_semver": { - "name": "api_semver", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "external_id": { - "name": "external_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_checkout_session_id": { - "name": "stripe_checkout_session_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "previous_customer_product_id": { - "name": "previous_customer_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "on_trial_end": { - "name": "on_trial_end", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_customer_products_customer_status": { - "name": "idx_customer_products_customer_status", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_products_on_internal_entity_id": { - "name": "idx_customer_products_on_internal_entity_id", - "columns": [ - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_products_on_internal_product_id": { - "name": "idx_customer_products_on_internal_product_id", - "columns": [ - { - "expression": "internal_product_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_products_subscription_ids": { - "name": "idx_customer_products_subscription_ids", - "columns": [ - { - "expression": "subscription_ids", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_customer_products_scheduled_ids": { - "name": "idx_customer_products_scheduled_ids", - "columns": [ - { - "expression": "scheduled_ids", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_customer_products_stripe_checkout_session_id": { - "name": "idx_customer_products_stripe_checkout_session_id", - "columns": [ - { - "expression": "stripe_checkout_session_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customer_products_revenuecat_processor": { - "name": "idx_customer_products_revenuecat_processor", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "(\"customer_products\".\"processor\" ->> 'type') = 'revenuecat'", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "customer_products_free_trial_id_fkey": { - "name": "customer_products_free_trial_id_fkey", - "tableFrom": "customer_products", - "tableTo": "free_trials", - "columnsFrom": [ - "free_trial_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "customer_products_internal_customer_id_fkey": { - "name": "customer_products_internal_customer_id_fkey", - "tableFrom": "customer_products", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "customer_products_internal_product_id_fkey": { - "name": "customer_products_internal_product_id_fkey", - "tableFrom": "customer_products", - "tableTo": "products", - "columnsFrom": [ - "internal_product_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "customer_products_internal_entity_id_fkey": { - "name": "customer_products_internal_entity_id_fkey", - "tableFrom": "customer_products", - "tableTo": "entities", - "columnsFrom": [ - "internal_entity_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.customers": { - "name": "customers", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "fingerprint": { - "name": "fingerprint", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "processor": { - "name": "processor", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "processors": { - "name": "processors", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "send_email_receipts": { - "name": "send_email_receipts", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "auto_topups": { - "name": "auto_topups", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "spend_limits": { - "name": "spend_limits", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "usage_alerts": { - "name": "usage_alerts", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "overage_allowed": { - "name": "overage_allowed", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - } - }, - "indexes": { - "customers_email_null_id_unique": { - "name": "customers_email_null_id_unique", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "lower(\"email\")", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"customers\".\"id\" IS NULL AND \"customers\".\"email\" IS NOT NULL AND \"customers\".\"email\" != ''", - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_org_env_fingerprint": { - "name": "idx_customers_org_env_fingerprint", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "fingerprint", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"customers\".\"fingerprint\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_processor_id": { - "name": "idx_customers_processor_id", - "columns": [ - { - "expression": "(\"processor\" ->> 'id')", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_composite": { - "name": "idx_customers_composite", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_org_env_internal_id": { - "name": "idx_customers_org_env_internal_id", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"internal_id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_email_trgm": { - "name": "idx_customers_email_trgm", - "columns": [ - { - "expression": "\"email\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"customers\".\"email\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_customers_name_trgm": { - "name": "idx_customers_name_trgm", - "columns": [ - { - "expression": "\"name\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"customers\".\"name\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_customers_id_trgm": { - "name": "idx_customers_id_trgm", - "columns": [ - { - "expression": "\"id\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"customers\".\"id\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_customers_org_id_env_created_at": { - "name": "idx_customers_org_id_env_created_at", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"created_at\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_cursor": { - "name": "idx_customers_cursor", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"created_at\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "\"id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_processors_revenuecat": { - "name": "idx_customers_processors_revenuecat", - "columns": [ - { - "expression": "(\"processors\" ->> 'revenuecat')", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_customers_processors_vercel": { - "name": "idx_customers_processors_vercel", - "columns": [ - { - "expression": "(\"processors\" ->> 'vercel')", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "customers_org_id_fkey": { - "name": "customers_org_id_fkey", - "tableFrom": "customers", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "cus_id_constraint": { - "name": "cus_id_constraint", - "nullsNotDistinct": false, - "columns": [ - "org_id", - "id", - "env" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.entities": { - "name": "entities", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "deleted": { - "name": "deleted", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "internal_feature_id": { - "name": "internal_feature_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "spend_limits": { - "name": "spend_limits", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "usage_alerts": { - "name": "usage_alerts", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "overage_allowed": { - "name": "overage_allowed", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "feature_id": { - "name": "feature_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_entities_internal_customer_id": { - "name": "idx_entities_internal_customer_id", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_entities_customer_internal_desc": { - "name": "idx_entities_customer_internal_desc", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"internal_id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_entities_org_env_id": { - "name": "idx_entities_org_env_id", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_entities_cursor": { - "name": "idx_entities_cursor", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"created_at\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "\"id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "entities_internal_customer_id_fkey": { - "name": "entities_internal_customer_id_fkey", - "tableFrom": "entities", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "entities_internal_feature_id_fkey": { - "name": "entities_internal_feature_id_fkey", - "tableFrom": "entities", - "tableTo": "features", - "columnsFrom": [ - "internal_feature_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "entities_org_id_fkey": { - "name": "entities_org_id_fkey", - "tableFrom": "entities", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "entity_id_constraint": { - "name": "entity_id_constraint", - "nullsNotDistinct": false, - "columns": [ - "org_id", - "env", - "internal_customer_id", - "id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.entitlements": { - "name": "entitlements", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "internal_feature_id": { - "name": "internal_feature_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_product_id": { - "name": "internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_reward_id": { - "name": "internal_reward_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_custom": { - "name": "is_custom", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "allowance_type": { - "name": "allowance_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "allowance": { - "name": "allowance", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "interval": { - "name": "interval", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "interval_count": { - "name": "interval_count", - "type": "numeric", - "primaryKey": false, - "notNull": false, - "default": 1 - }, - "carry_from_previous": { - "name": "carry_from_previous", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "entity_feature_id": { - "name": "entity_feature_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "feature_id": { - "name": "feature_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "usage_limit": { - "name": "usage_limit", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "expiry_duration": { - "name": "expiry_duration", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expiry_length": { - "name": "expiry_length", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "rollover": { - "name": "rollover", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_entitlements_internal_product_id": { - "name": "idx_entitlements_internal_product_id", - "columns": [ - { - "expression": "internal_product_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_entitlements_internal_reward_id": { - "name": "idx_entitlements_internal_reward_id", - "columns": [ - { - "expression": "internal_reward_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_entitlements_reward_feature": { - "name": "idx_entitlements_reward_feature", - "columns": [ - { - "expression": "internal_reward_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "internal_feature_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_entitlements_internal_reward_id_c_partial": { - "name": "idx_entitlements_internal_reward_id_c_partial", - "columns": [ - { - "expression": "\"internal_reward_id\" COLLATE \"C\"", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"entitlements\".\"internal_reward_id\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "entitlements_internal_feature_id_fkey": { - "name": "entitlements_internal_feature_id_fkey", - "tableFrom": "entitlements", - "tableTo": "features", - "columnsFrom": [ - "internal_feature_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "entitlements_internal_product_id_fkey": { - "name": "entitlements_internal_product_id_fkey", - "tableFrom": "entitlements", - "tableTo": "products", - "columnsFrom": [ - "internal_product_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - }, - "entitlements_internal_reward_id_fkey": { - "name": "entitlements_internal_reward_id_fkey", - "tableFrom": "entitlements", - "tableTo": "rewards", - "columnsFrom": [ - "internal_reward_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "entitlements_id_key": { - "name": "entitlements_id_key", - "nullsNotDistinct": false, - "columns": [ - "id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.events": { - "name": "events", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "org_slug": { - "name": "org_slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "event_name": { - "name": "event_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "idempotency_key": { - "name": "idempotency_key", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "value": { - "name": "value", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "set_usage": { - "name": "set_usage", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "entity_id": { - "name": "entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_entity_id": { - "name": "internal_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_product_id": { - "name": "internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "properties": { - "name": "properties", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "deductions": { - "name": "deductions", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_events_internal_customer_id": { - "name": "idx_events_internal_customer_id", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_events_internal_entity_id": { - "name": "idx_events_internal_entity_id", - "columns": [ - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_events_customer_non_usage_ts": { - "name": "idx_events_customer_non_usage_ts", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"timestamp\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "\"id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"events\".\"set_usage\" = false", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "events_internal_customer_id_fkey": { - "name": "events_internal_customer_id_fkey", - "tableFrom": "events", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_event_constraint": { - "name": "unique_event_constraint", - "nullsNotDistinct": false, - "columns": [ - "org_id", - "env", - "customer_id", - "event_name", - "idempotency_key" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.features": { - "name": "features", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "display": { - "name": "display", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "archived": { - "name": "archived", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "event_names": { - "name": "event_names", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - } - }, - "indexes": {}, - "foreignKeys": { - "features_org_id_fkey": { - "name": "features_org_id_fkey", - "tableFrom": "features", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "feature_id_constraint": { - "name": "feature_id_constraint", - "nullsNotDistinct": false, - "columns": [ - "org_id", - "id", - "env" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.free_trials": { - "name": "free_trials", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "internal_product_id": { - "name": "internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "duration": { - "name": "duration", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'day'" - }, - "length": { - "name": "length", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "unique_fingerprint": { - "name": "unique_fingerprint", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "is_custom": { - "name": "is_custom", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "card_required": { - "name": "card_required", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "on_end": { - "name": "on_end", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_free_trials_internal_product_id": { - "name": "idx_free_trials_internal_product_id", - "columns": [ - { - "expression": "internal_product_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "free_trials_internal_product_id_fkey": { - "name": "free_trials_internal_product_id_fkey", - "tableFrom": "free_trials", - "tableTo": "products", - "columnsFrom": [ - "internal_product_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invitation": { - "name": "invitation", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "inviter_id": { - "name": "inviter_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "invitation_organizationId_idx": { - "name": "invitation_organizationId_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "invitation_email_idx": { - "name": "invitation_email_idx", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "invitation_organization_id_organizations_id_fk": { - "name": "invitation_organization_id_organizations_id_fk", - "tableFrom": "invitation", - "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invitation_inviter_id_user_id_fk": { - "name": "invitation_inviter_id_user_id_fk", - "tableFrom": "invitation", - "tableTo": "user", - "columnsFrom": [ - "inviter_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.invoice_line_items": { - "name": "invoice_line_items", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "invoice_id": { - "name": "invoice_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_id": { - "name": "stripe_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_invoice_id": { - "name": "stripe_invoice_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_invoice_item_id": { - "name": "stripe_invoice_item_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_subscription_item_id": { - "name": "stripe_subscription_item_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_product_id": { - "name": "stripe_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_price_id": { - "name": "stripe_price_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_discountable": { - "name": "stripe_discountable", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "amount": { - "name": "amount", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "amount_after_discounts": { - "name": "amount_after_discounts", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'usd'" - }, - "stripe_quantity": { - "name": "stripe_quantity", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "total_quantity": { - "name": "total_quantity", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "paid_quantity": { - "name": "paid_quantity", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description_source": { - "name": "description_source", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "direction": { - "name": "direction", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "billing_timing": { - "name": "billing_timing", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "prorated": { - "name": "prorated", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "price_id": { - "name": "price_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "customer_product_ids": { - "name": "customer_product_ids", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "customer_price_ids": { - "name": "customer_price_ids", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "customer_entitlement_ids": { - "name": "customer_entitlement_ids", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'[]'::jsonb" - }, - "internal_product_id": { - "name": "internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "product_id": { - "name": "product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "internal_feature_id": { - "name": "internal_feature_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "feature_id": { - "name": "feature_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "effective_period_start": { - "name": "effective_period_start", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "effective_period_end": { - "name": "effective_period_end", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "discounts": { - "name": "discounts", - "type": "jsonb[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - } - }, - "indexes": {}, - "foreignKeys": { - "invoice_line_items_invoice_id_fkey": { - "name": "invoice_line_items_invoice_id_fkey", - "tableFrom": "invoice_line_items", - "tableTo": "invoices", - "columnsFrom": [ - "invoice_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "invoice_line_items_stripe_id_unique": { - "name": "invoice_line_items_stripe_id_unique", - "nullsNotDistinct": false, - "columns": [ - "stripe_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invoice_templates": { - "name": "invoice_templates", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "footer": { - "name": "footer", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "memo": { - "name": "memo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "net_terms_days": { - "name": "net_terms_days", - "type": "integer", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_invoice_templates_org_id": { - "name": "idx_invoice_templates_org_id", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "invoice_templates_org_id_fkey": { - "name": "invoice_templates_org_id_fkey", - "tableFrom": "invoice_templates", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "invoice_templates_id_unique": { - "name": "invoice_templates_id_unique", - "nullsNotDistinct": false, - "columns": [ - "id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.invoices": { - "name": "invoices", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "product_ids": { - "name": "product_ids", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "internal_product_ids": { - "name": "internal_product_ids", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_entity_id": { - "name": "internal_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_id": { - "name": "stripe_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "processor_type": { - "name": "processor_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'draft'" - }, - "hosted_invoice_url": { - "name": "hosted_invoice_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "total": { - "name": "total", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "amount_paid": { - "name": "amount_paid", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "refunded_amount": { - "name": "refunded_amount", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "currency": { - "name": "currency", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'usd'" - }, - "discounts": { - "name": "discounts", - "type": "jsonb[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "items": { - "name": "items", - "type": "jsonb[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - } - }, - "indexes": { - "idx_invoices_customer_created": { - "name": "idx_invoices_customer_created", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"created_at\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "\"id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_invoices_internal_entity_id": { - "name": "idx_invoices_internal_entity_id", - "columns": [ - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"invoices\".\"internal_entity_id\" IS NOT NULL", - "concurrently": true, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "invoices_internal_customer_id_fkey": { - "name": "invoices_internal_customer_id_fkey", - "tableFrom": "invoices", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "invoices_internal_entity_id_fkey": { - "name": "invoices_internal_entity_id_fkey", - "tableFrom": "invoices", - "tableTo": "entities", - "columnsFrom": [ - "internal_entity_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "invoices_stripe_id_key": { - "name": "invoices_stripe_id_key", - "nullsNotDistinct": false, - "columns": [ - "stripe_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.jwks": { - "name": "jwks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "public_key": { - "name": "public_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "private_key": { - "name": "private_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.member": { - "name": "member", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "organization_id": { - "name": "organization_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "member_organizationId_idx": { - "name": "member_organizationId_idx", - "columns": [ - { - "expression": "organization_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "member_userId_idx": { - "name": "member_userId_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "member_organization_id_organizations_id_fk": { - "name": "member_organization_id_organizations_id_fk", - "tableFrom": "member", - "tableTo": "organizations", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "member_user_id_user_id_fk": { - "name": "member_user_id_user_id_fk", - "tableFrom": "member", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.metadata": { - "name": "metadata", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "expires_at": { - "name": "expires_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_invoice_id": { - "name": "stripe_invoice_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_checkout_session_id": { - "name": "stripe_checkout_session_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.migration_errors": { - "name": "migration_errors", - "schema": "", - "columns": { - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "migration_job_id": { - "name": "migration_job_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "data": { - "name": "data", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "message": { - "name": "message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "migration_customers_internal_customer_id_fkey": { - "name": "migration_customers_internal_customer_id_fkey", - "tableFrom": "migration_errors", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "migration_customers_migration_job_id_fkey": { - "name": "migration_customers_migration_job_id_fkey", - "tableFrom": "migration_errors", - "tableTo": "migration_jobs", - "columnsFrom": [ - "migration_job_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "migration_errors_pkey": { - "name": "migration_errors_pkey", - "columns": [ - "internal_customer_id", - "migration_job_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.migration_item_runs": { - "name": "migration_item_runs", - "schema": "", - "columns": { - "migration_item_run_id": { - "name": "migration_item_run_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "migration_internal_id": { - "name": "migration_internal_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "migration_run_id": { - "name": "migration_run_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "dry_run": { - "name": "dry_run", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "item_kind": { - "name": "item_kind", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "item_id": { - "name": "item_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "timestamp": { - "name": "timestamp", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "migration_item_runs_live_unique": { - "name": "migration_item_runs_live_unique", - "columns": [ - { - "expression": "migration_internal_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "item_kind", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "item_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"migration_item_runs\".\"dry_run\" = false", - "concurrently": false, - "method": "btree", - "with": {} - }, - "migration_item_runs_dry_run_unique": { - "name": "migration_item_runs_dry_run_unique", - "columns": [ - { - "expression": "migration_internal_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "migration_run_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "item_kind", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "item_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"migration_item_runs\".\"dry_run\" = true", - "concurrently": false, - "method": "btree", - "with": {} - }, - "migration_item_runs_customer_recent_idx": { - "name": "migration_item_runs_customer_recent_idx", - "columns": [ - { - "expression": "item_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "\"updated_at\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"migration_item_runs\".\"item_kind\" = 'customer'", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.migration_jobs": { - "name": "migration_jobs", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "current_step": { - "name": "current_step", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "from_internal_product_id": { - "name": "from_internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "to_internal_product_id": { - "name": "to_internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "step_details": { - "name": "step_details", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "migration_jobs_from_internal_product_id_fkey": { - "name": "migration_jobs_from_internal_product_id_fkey", - "tableFrom": "migration_jobs", - "tableTo": "products", - "columnsFrom": [ - "from_internal_product_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "migration_jobs_org_id_fkey": { - "name": "migration_jobs_org_id_fkey", - "tableFrom": "migration_jobs", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "migration_jobs_to_internal_product_id_fkey": { - "name": "migration_jobs_to_internal_product_id_fkey", - "tableFrom": "migration_jobs", - "tableTo": "products", - "columnsFrom": [ - "to_internal_product_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.migration_runs": { - "name": "migration_runs", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "migration_internal_id": { - "name": "migration_internal_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "dry_run": { - "name": "dry_run", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "lazy_run": { - "name": "lazy_run", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "trigger_run_id": { - "name": "trigger_run_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "error_message": { - "name": "error_message", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "only_ids": { - "name": "only_ids", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "target_limit": { - "name": "target_limit", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "started_at": { - "name": "started_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "finished_at": { - "name": "finished_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "migration_runs_active_per_migration_unique": { - "name": "migration_runs_active_per_migration_unique", - "columns": [ - { - "expression": "migration_internal_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"migration_runs\".\"status\" IN ('queued', 'running')", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "migration_runs_migration_internal_id_fkey": { - "name": "migration_runs_migration_internal_id_fkey", - "tableFrom": "migration_runs", - "tableTo": "migrations", - "columnsFrom": [ - "migration_internal_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "migration_runs_org_id_fkey": { - "name": "migration_runs_org_id_fkey", - "tableFrom": "migration_runs", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.migrations": { - "name": "migrations", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "filter": { - "name": "filter", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "operations": { - "name": "operations", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "prepared_state": { - "name": "prepared_state", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "no_billing_changes": { - "name": "no_billing_changes", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "retry_failed": { - "name": "retry_failed", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "migrations_org_env_id_unique": { - "name": "migrations_org_env_id_unique", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "migrations_org_id_fkey": { - "name": "migrations_org_id_fkey", - "tableFrom": "migrations", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.oauth_access_token": { - "name": "oauth_access_token", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_id": { - "name": "refresh_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "type": "text[]", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "oauth_access_token_client_id_oauth_client_client_id_fk": { - "name": "oauth_access_token_client_id_oauth_client_client_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "oauth_client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "client_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_access_token_session_id_session_id_fk": { - "name": "oauth_access_token_session_id_session_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "oauth_access_token_user_id_user_id_fk": { - "name": "oauth_access_token_user_id_user_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { - "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", - "tableFrom": "oauth_access_token", - "tableTo": "oauth_refresh_token", - "columnsFrom": [ - "refresh_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "oauth_access_token_token_unique": { - "name": "oauth_access_token_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.oauth_client": { - "name": "oauth_client", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "client_secret": { - "name": "client_secret", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "disabled": { - "name": "disabled", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "skip_consent": { - "name": "skip_consent", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "enable_end_session": { - "name": "enable_end_session", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "uri": { - "name": "uri", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "contacts": { - "name": "contacts", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "tos": { - "name": "tos", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "policy": { - "name": "policy", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "software_id": { - "name": "software_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "software_version": { - "name": "software_version", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "software_statement": { - "name": "software_statement", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "redirect_uris": { - "name": "redirect_uris", - "type": "text[]", - "primaryKey": false, - "notNull": true - }, - "post_logout_redirect_uris": { - "name": "post_logout_redirect_uris", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "token_endpoint_auth_method": { - "name": "token_endpoint_auth_method", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "grant_types": { - "name": "grant_types", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "response_types": { - "name": "response_types", - "type": "text[]", - "primaryKey": false, - "notNull": false - }, - "public": { - "name": "public", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "oauth_client_user_id_user_id_fk": { - "name": "oauth_client_user_id_user_id_fk", - "tableFrom": "oauth_client", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "oauth_client_client_id_unique": { - "name": "oauth_client_client_id_unique", - "nullsNotDistinct": false, - "columns": [ - "client_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.oauth_consent": { - "name": "oauth_consent", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "type": "text[]", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "redirect_uri": { - "name": "redirect_uri", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "oauth_api_key_id": { - "name": "oauth_api_key_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "oauth_api_key": { - "name": "oauth_api_key", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "oauth_consent_client_id_oauth_client_client_id_fk": { - "name": "oauth_consent_client_id_oauth_client_client_id_fk", - "tableFrom": "oauth_consent", - "tableTo": "oauth_client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "client_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_consent_user_id_user_id_fk": { - "name": "oauth_consent_user_id_user_id_fk", - "tableFrom": "oauth_consent", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.oauth_refresh_token": { - "name": "oauth_refresh_token", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "client_id": { - "name": "client_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "reference_id": { - "name": "reference_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "revoked": { - "name": "revoked", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "auth_time": { - "name": "auth_time", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "scopes": { - "name": "scopes", - "type": "text[]", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "oauth_refresh_token_client_id_oauth_client_client_id_fk": { - "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", - "tableFrom": "oauth_refresh_token", - "tableTo": "oauth_client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "client_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "oauth_refresh_token_session_id_session_id_fk": { - "name": "oauth_refresh_token_session_id_session_id_fk", - "tableFrom": "oauth_refresh_token", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "oauth_refresh_token_user_id_user_id_fk": { - "name": "oauth_refresh_token_user_id_user_id_fk", - "tableFrom": "oauth_refresh_token", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.organizations": { - "name": "organizations", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "logo": { - "name": "logo", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "createdAt": { - "name": "createdAt", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "default_currency": { - "name": "default_currency", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'usd'" - }, - "stripe_connected": { - "name": "stripe_connected", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "stripe_config": { - "name": "stripe_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "test_stripe_connect": { - "name": "test_stripe_connect", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "live_stripe_connect": { - "name": "live_stripe_connect", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "processor_configs": { - "name": "processor_configs", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "test_pkey": { - "name": "test_pkey", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "live_pkey": { - "name": "live_pkey", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "svix_config": { - "name": "svix_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "onboarded": { - "name": "onboarded", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "deployed": { - "name": "deployed", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "redis_config": { - "name": "redis_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_organizations_name_trgm": { - "name": "idx_organizations_name_trgm", - "columns": [ - { - "expression": "\"name\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"organizations\".\"name\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_organizations_slug_trgm": { - "name": "idx_organizations_slug_trgm", - "columns": [ - { - "expression": "\"slug\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"organizations\".\"slug\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_organizations_created_at_id": { - "name": "idx_organizations_created_at_id", - "columns": [ - { - "expression": "\"createdAt\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "\"id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "organizations_slug_unique": { - "name": "organizations_slug_unique", - "nullsNotDistinct": false, - "columns": [ - "slug" - ] - }, - "organizations_test_pkey_key": { - "name": "organizations_test_pkey_key", - "nullsNotDistinct": false, - "columns": [ - "test_pkey" - ] - }, - "organizations_live_pkey_key": { - "name": "organizations_live_pkey_key", - "nullsNotDistinct": false, - "columns": [ - "live_pkey" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.passkey": { - "name": "passkey", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "public_key": { - "name": "public_key", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "credential_id": { - "name": "credential_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "counter": { - "name": "counter", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "device_type": { - "name": "device_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "backed_up": { - "name": "backed_up", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "transports": { - "name": "transports", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "aaguid": { - "name": "aaguid", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "passkey_userId_idx": { - "name": "passkey_userId_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "passkey_credentialId_idx": { - "name": "passkey_credentialId_idx", - "columns": [ - { - "expression": "credential_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "passkey_user_id_user_id_fk": { - "name": "passkey_user_id_user_id_fk", - "tableFrom": "passkey", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "passkey_credential_id_unique": { - "name": "passkey_credential_id_unique", - "nullsNotDistinct": false, - "columns": [ - "credential_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.prices": { - "name": "prices", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text COLLATE \"C\"", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_product_id": { - "name": "internal_product_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "billing_type": { - "name": "billing_type", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "tier_behavior": { - "name": "tier_behavior", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "is_custom": { - "name": "is_custom", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "entitlement_id": { - "name": "entitlement_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "proration_config": { - "name": "proration_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "null" - } - }, - "indexes": { - "idx_prices_internal_product_id": { - "name": "idx_prices_internal_product_id", - "columns": [ - { - "expression": "internal_product_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_prices_entitlement_id": { - "name": "idx_prices_entitlement_id", - "columns": [ - { - "expression": "entitlement_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "prices_entitlement_id_fkey": { - "name": "prices_entitlement_id_fkey", - "tableFrom": "prices", - "tableTo": "entitlements", - "columnsFrom": [ - "entitlement_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "prices_internal_product_id_fkey": { - "name": "prices_internal_product_id_fkey", - "tableFrom": "prices", - "tableTo": "products", - "columnsFrom": [ - "internal_product_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "prices_id_key": { - "name": "prices_id_key", - "nullsNotDistinct": false, - "columns": [ - "id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.products": { - "name": "products", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_add_on": { - "name": "is_add_on", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "is_default": { - "name": "is_default", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "group": { - "name": "group", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "''" - }, - "version": { - "name": "version", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "processor": { - "name": "processor", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "null" - }, - "base_variant_id": { - "name": "base_variant_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "archived": { - "name": "archived", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "config": { - "name": "config", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - } - }, - "indexes": { - "idx_products_org_env_id_version": { - "name": "idx_products_org_env_id_version", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "version", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "products_org_id_fkey": { - "name": "products_org_id_fkey", - "tableFrom": "products", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_product": { - "name": "unique_product", - "nullsNotDistinct": false, - "columns": [ - "org_id", - "id", - "env", - "version" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.referral_codes": { - "name": "referral_codes", - "schema": "", - "columns": { - "code": { - "name": "code", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text COLLATE \"C\"", - "primaryKey": false, - "notNull": false - }, - "internal_reward_program_id": { - "name": "internal_reward_program_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_referral_codes_internal_customer_id": { - "name": "idx_referral_codes_internal_customer_id", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "referral_codes_internal_customer_id_fkey": { - "name": "referral_codes_internal_customer_id_fkey", - "tableFrom": "referral_codes", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "referral_codes_internal_reward_program_id_fkey": { - "name": "referral_codes_internal_reward_program_id_fkey", - "tableFrom": "referral_codes", - "tableTo": "reward_programs", - "columnsFrom": [ - "internal_reward_program_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "referral_codes_org_id_fkey": { - "name": "referral_codes_org_id_fkey", - "tableFrom": "referral_codes", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "referral_codes_pkey": { - "name": "referral_codes_pkey", - "columns": [ - "code", - "org_id", - "env" - ] - } - }, - "uniqueConstraints": { - "referral_codes_id_key": { - "name": "referral_codes_id_key", - "nullsNotDistinct": false, - "columns": [ - "id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.replaceables": { - "name": "replaceables", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "cus_ent_id": { - "name": "cus_ent_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "bigint", - "primaryKey": false, - "notNull": true - }, - "from_entity_id": { - "name": "from_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "delete_next_cycle": { - "name": "delete_next_cycle", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_replaceables_cus_ent_id": { - "name": "idx_replaceables_cus_ent_id", - "columns": [ - { - "expression": "cus_ent_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "replaceables_cus_ent_id_fkey": { - "name": "replaceables_cus_ent_id_fkey", - "tableFrom": "replaceables", - "tableTo": "customer_entitlements", - "columnsFrom": [ - "cus_ent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.revenuecat_mappings": { - "name": "revenuecat_mappings", - "schema": "", - "columns": { - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "autumn_product_id": { - "name": "autumn_product_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "revenuecat_product_ids": { - "name": "revenuecat_product_ids", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - } - }, - "indexes": {}, - "foreignKeys": { - "revenuecat_mappings_org_id_fkey": { - "name": "revenuecat_mappings_org_id_fkey", - "tableFrom": "revenuecat_mappings", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "revenuecat_mappings_pkey": { - "name": "revenuecat_mappings_pkey", - "columns": [ - "org_id", - "env", - "autumn_product_id" - ] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.reward_programs": { - "name": "reward_programs", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "internal_reward_id": { - "name": "internal_reward_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "max_redemptions": { - "name": "max_redemptions", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "unlimited_redemptions": { - "name": "unlimited_redemptions", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "when": { - "name": "when", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'immediately'" - }, - "product_ids": { - "name": "product_ids", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "'{\"\"}'" - }, - "exclude_trial": { - "name": "exclude_trial", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "received_by": { - "name": "received_by", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "reward_triggers_internal_reward_id_fkey": { - "name": "reward_triggers_internal_reward_id_fkey", - "tableFrom": "reward_programs", - "tableTo": "rewards", - "columnsFrom": [ - "internal_reward_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "reward_triggers_org_id_fkey": { - "name": "reward_triggers_org_id_fkey", - "tableFrom": "reward_programs", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.reward_redemptions": { - "name": "reward_redemptions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text COLLATE \"C\"", - "primaryKey": false, - "notNull": false - }, - "triggered": { - "name": "triggered", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "internal_reward_program_id": { - "name": "internal_reward_program_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "applied": { - "name": "applied", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "redeemer_applied": { - "name": "redeemer_applied", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "referral_code_id": { - "name": "referral_code_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "reward_internal_id": { - "name": "reward_internal_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "promo_code": { - "name": "promo_code", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_reward_redemptions_referral_code_id": { - "name": "idx_reward_redemptions_referral_code_id", - "columns": [ - { - "expression": "referral_code_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_reward_redemptions_reward_internal_id": { - "name": "idx_reward_redemptions_reward_internal_id", - "columns": [ - { - "expression": "reward_internal_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_reward_redemptions_customer_reward": { - "name": "idx_reward_redemptions_customer_reward", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "reward_internal_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "reward_redemptions_internal_customer_id_fkey": { - "name": "reward_redemptions_internal_customer_id_fkey", - "tableFrom": "reward_redemptions", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "reward_redemptions_internal_reward_program_id_fkey": { - "name": "reward_redemptions_internal_reward_program_id_fkey", - "tableFrom": "reward_redemptions", - "tableTo": "reward_programs", - "columnsFrom": [ - "internal_reward_program_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "reward_redemptions_referral_code_id_fkey": { - "name": "reward_redemptions_referral_code_id_fkey", - "tableFrom": "reward_redemptions", - "tableTo": "referral_codes", - "columnsFrom": [ - "referral_code_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.rewards": { - "name": "rewards", - "schema": "", - "columns": { - "internal_id": { - "name": "internal_id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "id": { - "name": "id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "discount_config": { - "name": "discount_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "free_product_config": { - "name": "free_product_config", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "free_product_id": { - "name": "free_product_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "promo_codes": { - "name": "promo_codes", - "type": "jsonb[]", - "primaryKey": false, - "notNull": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "coupons_org_id_fkey": { - "name": "coupons_org_id_fkey", - "tableFrom": "rewards", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.rollovers": { - "name": "rollovers", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "cus_ent_id": { - "name": "cus_ent_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "balance": { - "name": "balance", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "usage": { - "name": "usage", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "entities": { - "name": "entities", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - } - }, - "indexes": { - "idx_rollovers_cus_ent_id": { - "name": "idx_rollovers_cus_ent_id", - "columns": [ - { - "expression": "cus_ent_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_rollovers_cus_ent_expires": { - "name": "idx_rollovers_cus_ent_expires", - "columns": [ - { - "expression": "cus_ent_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "expires_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "rollover_cus_ent_id_fkey": { - "name": "rollover_cus_ent_id_fkey", - "tableFrom": "rollovers", - "tableTo": "customer_entitlements", - "columnsFrom": [ - "cus_ent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "cascade" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.phases": { - "name": "phases", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "schedule_id": { - "name": "schedule_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "starts_at": { - "name": "starts_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - }, - "customer_product_ids": { - "name": "customer_product_ids", - "type": "text[]", - "primaryKey": false, - "notNull": true, - "default": "'{}'" - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "phases_schedule_id_fkey": { - "name": "phases_schedule_id_fkey", - "tableFrom": "phases", - "tableTo": "schedules", - "columnsFrom": [ - "schedule_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "phases_schedule_id_starts_at_key": { - "name": "phases_schedule_id_starts_at_key", - "nullsNotDistinct": false, - "columns": [ - "schedule_id", - "starts_at" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.schedules": { - "name": "schedules", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_customer_id": { - "name": "internal_customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "customer_id": { - "name": "customer_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "internal_entity_id": { - "name": "internal_entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "entity_id": { - "name": "entity_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "schedules_customer_scope_unique": { - "name": "schedules_customer_scope_unique", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"schedules\".\"internal_entity_id\" IS NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "schedules_entity_scope_unique": { - "name": "schedules_entity_scope_unique", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "\"schedules\".\"internal_entity_id\" IS NOT NULL", - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_schedules_internal_customer_id": { - "name": "idx_schedules_internal_customer_id", - "columns": [ - { - "expression": "internal_customer_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_schedules_internal_entity_id": { - "name": "idx_schedules_internal_entity_id", - "columns": [ - { - "expression": "internal_entity_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "schedules_org_id_fkey": { - "name": "schedules_org_id_fkey", - "tableFrom": "schedules", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "schedules_internal_customer_id_fkey": { - "name": "schedules_internal_customer_id_fkey", - "tableFrom": "schedules", - "tableTo": "customers", - "columnsFrom": [ - "internal_customer_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "schedules_internal_entity_id_fkey": { - "name": "schedules_internal_entity_id_fkey", - "tableFrom": "schedules", - "tableTo": "entities", - "columnsFrom": [ - "internal_entity_id" - ], - "columnsTo": [ - "internal_id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "impersonated_by": { - "name": "impersonated_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "active_organization_id": { - "name": "active_organization_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "city": { - "name": "city", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "country": { - "name": "country", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "session_userId_idx": { - "name": "session_userId_idx", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - }, - "public.subscriptions": { - "name": "subscriptions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "stripe_id": { - "name": "stripe_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "stripe_schedule_id": { - "name": "stripe_schedule_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "numeric", - "primaryKey": false, - "notNull": true, - "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": false, - "default": "'{}'::jsonb" - }, - "usage_features": { - "name": "usage_features", - "type": "text[]", - "primaryKey": false, - "notNull": false, - "default": "'{}'" - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "current_period_start": { - "name": "current_period_start", - "type": "numeric", - "primaryKey": false, - "notNull": false - }, - "current_period_end": { - "name": "current_period_end", - "type": "numeric", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": { - "subscriptions_org_id_fkey": { - "name": "subscriptions_org_id_fkey", - "tableFrom": "subscriptions", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "subscriptions_stripe_id_key": { - "name": "subscriptions_stripe_id_key", - "nullsNotDistinct": false, - "columns": [ - "stripe_id" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "banned": { - "name": "banned", - "type": "boolean", - "primaryKey": false, - "notNull": false - }, - "ban_reason": { - "name": "ban_reason", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ban_expires": { - "name": "ban_expires", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_by": { - "name": "created_by", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_active_at": { - "name": "last_active_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_user_name_trgm": { - "name": "idx_user_name_trgm", - "columns": [ - { - "expression": "\"name\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"user\".\"name\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_user_email_trgm": { - "name": "idx_user_email_trgm", - "columns": [ - { - "expression": "\"email\" gin_trgm_ops", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "where": "\"user\".\"email\" IS NOT NULL", - "concurrently": false, - "method": "gin", - "with": {} - }, - "idx_user_created_at_id": { - "name": "idx_user_created_at_id", - "columns": [ - { - "expression": "\"created_at\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - }, - { - "expression": "\"id\" DESC", - "asc": true, - "isExpression": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "user_created_by_fkey": { - "name": "user_created_by_fkey", - "tableFrom": "user", - "tableTo": "organizations", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.vercel_resources": { - "name": "vercel_resources", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "org_id": { - "name": "org_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "env": { - "name": "env", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "installation_id": { - "name": "installation_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "metadata": { - "name": "metadata", - "type": "jsonb", - "primaryKey": false, - "notNull": true, - "default": "'{}'::jsonb" - } - }, - "indexes": { - "vercel_resources_installation_name_unique_idx": { - "name": "vercel_resources_installation_name_unique_idx", - "columns": [ - { - "expression": "org_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "env", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "installation_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "name", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": true, - "where": "status <> 'uninstalled'", - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "vercel_resources_org_id_fkey": { - "name": "vercel_resources_org_id_fkey", - "tableFrom": "vercel_resources", - "tableTo": "organizations", - "columnsFrom": [ - "org_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "verification_identifier_idx": { - "name": "verification_identifier_idx", - "columns": [ - { - "expression": "identifier", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": true - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} \ No newline at end of file + "id": "eadc8c95-3f6f-4643-abc3-e90cd56d5ed1", + "prevId": "3ee43a45-bd02-43e2-a2d1-2080d51b5674", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.actions": { + "name": "actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_actions_on_internal_entity_id": { + "name": "idx_actions_on_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "actions_org_id_fkey": { + "name": "actions_org_id_fkey", + "tableFrom": "actions", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "actions_customer_id_fkey": { + "name": "actions_customer_id_fkey", + "tableFrom": "actions", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "actions_entity_id_fkey": { + "name": "actions_entity_id_fkey", + "tableFrom": "actions", + "tableTo": "entities", + "columnsFrom": ["internal_entity_id"], + "columnsTo": ["internal_id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hashed_key": { + "name": "hashed_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_org_id_fkey": { + "name": "api_keys_org_id_fkey", + "tableFrom": "api_keys", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_hashed_key_key": { + "name": "api_keys_hashed_key_key", + "nullsNotDistinct": false, + "columns": ["hashed_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_topup_limit_states": { + "name": "auto_topup_limit_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purchase_window_ends_at": { + "name": "purchase_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "purchase_count": { + "name": "purchase_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempt_window_ends_at": { + "name": "attempt_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed_attempt_window_ends_at": { + "name": "failed_attempt_window_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "failed_attempt_count": { + "name": "failed_attempt_count", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "last_failed_attempt_at": { + "name": "last_failed_attempt_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + } + }, + "indexes": { + "auto_topup_limits_org_env_internal_customer_feature_unique": { + "name": "auto_topup_limits_org_env_internal_customer_feature_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "feature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auto_topup_limits_org_id_fkey": { + "name": "auto_topup_limits_org_id_fkey", + "tableFrom": "auto_topup_limit_states", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auto_topup_limits_internal_customer_id_fkey": { + "name": "auto_topup_limits_internal_customer_id_fkey", + "tableFrom": "auto_topup_limit_states", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_approvals": { + "name": "chat_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_ts": { + "name": "message_ts", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_args": { + "name": "tool_args", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "preview": { + "name": "preview", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "decided_at": { + "name": "decided_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "decided_by_provider_user_id": { + "name": "decided_by_provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "chat_approvals_org_id_fkey": { + "name": "chat_approvals_org_id_fkey", + "tableFrom": "chat_approvals", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_installations": { + "name": "chat_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_name": { + "name": "workspace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bot_access_token": { + "name": "bot_access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "default_env": { + "name": "default_env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sandbox_api_key_id": { + "name": "sandbox_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_api_key": { + "name": "sandbox_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_api_key_id": { + "name": "live_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_api_key": { + "name": "live_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_by_user_id": { + "name": "installed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_by_provider_user_id": { + "name": "installed_by_provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + } + }, + "indexes": {}, + "foreignKeys": { + "chat_installations_org_id_fkey": { + "name": "chat_installations_org_id_fkey", + "tableFrom": "chat_installations", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chat_installations_org_provider_key": { + "name": "chat_installations_org_provider_key", + "nullsNotDistinct": false, + "columns": ["org_id", "provider"] + }, + "chat_installations_provider_workspace_key": { + "name": "chat_installations_provider_workspace_key", + "nullsNotDistinct": false, + "columns": ["provider", "workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_results": { + "name": "chat_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.checkouts": { + "name": "checkouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "params_version": { + "name": "params_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_checkouts_stripe_invoice_id": { + "name": "idx_checkouts_stripe_invoice_id", + "columns": [ + { + "expression": "stripe_invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_entitlements": { + "name": "customer_entitlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "customer_product_id": { + "name": "customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entitlement_id": { + "name": "entitlement_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unlimited": { + "name": "unlimited", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "next_reset_at": { + "name": "next_reset_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "usage_allowed": { + "name": "usage_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "adjustment": { + "name": "adjustment", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "additional_balance": { + "name": "additional_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "cache_version": { + "name": "cache_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_entitlements_product_id": { + "name": "idx_customer_entitlements_product_id", + "columns": [ + { + "expression": "customer_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_internal_customer_id": { + "name": "idx_customer_entitlements_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "idx_customer_entitlements_internal_customer_id_btree": { + "name": "idx_customer_entitlements_internal_customer_id_btree", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_entitlement_id": { + "name": "idx_customer_entitlements_entitlement_id", + "columns": [ + { + "expression": "entitlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_internal_entity_id": { + "name": "idx_customer_entitlements_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hash", + "with": {} + }, + "idx_customer_entitlements_on_next_reset_at": { + "name": "idx_customer_entitlements_on_next_reset_at", + "columns": [ + { + "expression": "next_reset_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_entitlements_loose_customer_expires": { + "name": "idx_customer_entitlements_loose_customer_expires", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customer_entitlements\".\"customer_product_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlements_internal_feature_id_fkey": { + "name": "entitlements_internal_feature_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "features", + "columnsFrom": ["internal_feature_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_entitlements_internal_entity_id_fkey": { + "name": "customer_entitlements_internal_entity_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "entities", + "columnsFrom": ["internal_entity_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_entitlements_customer_product_id_fkey": { + "name": "customer_entitlements_customer_product_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "customer_products", + "columnsFrom": ["customer_product_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "customer_entitlements_entitlement_id_fkey": { + "name": "customer_entitlements_entitlement_id_fkey", + "tableFrom": "customer_entitlements", + "tableTo": "entitlements", + "columnsFrom": ["entitlement_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_prices": { + "name": "customer_prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_product_id": { + "name": "customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_prices_product_id": { + "name": "idx_customer_prices_product_id", + "columns": [ + { + "expression": "customer_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_prices_price_id": { + "name": "idx_customer_prices_price_id", + "columns": [ + { + "expression": "price_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_prices_internal_customer_id": { + "name": "idx_customer_prices_internal_customer_id", + "columns": [ + { + "expression": "\"internal_customer_id\" COLLATE \"C\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customer_prices\".\"internal_customer_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_prices_customer_product_id_fkey": { + "name": "customer_prices_customer_product_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "customer_products", + "columnsFrom": ["customer_product_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_prices_internal_customer_id_fkey": { + "name": "customer_prices_internal_customer_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_prices_price_id_fkey": { + "name": "customer_prices_price_id_fkey", + "tableFrom": "customer_prices", + "tableTo": "prices", + "columnsFrom": ["price_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_products": { + "name": "customer_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "canceled": { + "name": "canceled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "access_starts_at": { + "name": "access_starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "free_trial_id": { + "name": "free_trial_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "billing_cycle_anchor_resets_at": { + "name": "billing_cycle_anchor_resets_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "collection_method": { + "name": "collection_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'charge_automatically'" + }, + "subscription_ids": { + "name": "subscription_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scheduled_ids": { + "name": "scheduled_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_version": { + "name": "billing_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_version": { + "name": "api_version", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "api_semver": { + "name": "api_semver", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_customer_product_id": { + "name": "previous_customer_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_trial_end": { + "name": "on_trial_end", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_customer_products_customer_status": { + "name": "idx_customer_products_customer_status", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_on_internal_entity_id": { + "name": "idx_customer_products_on_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_on_internal_product_id": { + "name": "idx_customer_products_on_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_subscription_ids": { + "name": "idx_customer_products_subscription_ids", + "columns": [ + { + "expression": "subscription_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customer_products_scheduled_ids": { + "name": "idx_customer_products_scheduled_ids", + "columns": [ + { + "expression": "scheduled_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customer_products_stripe_checkout_session_id": { + "name": "idx_customer_products_stripe_checkout_session_id", + "columns": [ + { + "expression": "stripe_checkout_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customer_products_revenuecat_processor": { + "name": "idx_customer_products_revenuecat_processor", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "(\"customer_products\".\"processor\" ->> 'type') = 'revenuecat'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_products_free_trial_id_fkey": { + "name": "customer_products_free_trial_id_fkey", + "tableFrom": "customer_products", + "tableTo": "free_trials", + "columnsFrom": ["free_trial_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "customer_products_internal_customer_id_fkey": { + "name": "customer_products_internal_customer_id_fkey", + "tableFrom": "customer_products", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "customer_products_internal_product_id_fkey": { + "name": "customer_products_internal_product_id_fkey", + "tableFrom": "customer_products", + "tableTo": "products", + "columnsFrom": ["internal_product_id"], + "columnsTo": ["internal_id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "customer_products_internal_entity_id_fkey": { + "name": "customer_products_internal_entity_id_fkey", + "tableFrom": "customer_products", + "tableTo": "entities", + "columnsFrom": ["internal_entity_id"], + "columnsTo": ["internal_id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customers": { + "name": "customers", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fingerprint": { + "name": "fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processors": { + "name": "processors", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "send_email_receipts": { + "name": "send_email_receipts", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_topups": { + "name": "auto_topups", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "spend_limits": { + "name": "spend_limits", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "overage_allowed": { + "name": "overage_allowed", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "customers_email_null_id_unique": { + "name": "customers_email_null_id_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"email\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"customers\".\"id\" IS NULL AND \"customers\".\"email\" IS NOT NULL AND \"customers\".\"email\" != ''", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_org_env_fingerprint": { + "name": "idx_customers_org_env_fingerprint", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"fingerprint\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processor_id": { + "name": "idx_customers_processor_id", + "columns": [ + { + "expression": "(\"processor\" ->> 'id')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_composite": { + "name": "idx_customers_composite", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_org_env_internal_id": { + "name": "idx_customers_org_env_internal_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"internal_id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_email_trgm": { + "name": "idx_customers_email_trgm", + "columns": [ + { + "expression": "\"email\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_name_trgm": { + "name": "idx_customers_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_id_trgm": { + "name": "idx_customers_id_trgm", + "columns": [ + { + "expression": "\"id\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"customers\".\"id\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_customers_org_id_env_created_at": { + "name": "idx_customers_org_id_env_created_at", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_cursor": { + "name": "idx_customers_cursor", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processors_revenuecat": { + "name": "idx_customers_processors_revenuecat", + "columns": [ + { + "expression": "(\"processors\" ->> 'revenuecat')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_customers_processors_vercel": { + "name": "idx_customers_processors_vercel", + "columns": [ + { + "expression": "(\"processors\" ->> 'vercel')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customers_org_id_fkey": { + "name": "customers_org_id_fkey", + "tableFrom": "customers", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cus_id_constraint": { + "name": "cus_id_constraint", + "nullsNotDistinct": false, + "columns": ["org_id", "id", "env"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.entities": { + "name": "entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spend_limits": { + "name": "spend_limits", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "usage_alerts": { + "name": "usage_alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "overage_allowed": { + "name": "overage_allowed", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_entities_internal_customer_id": { + "name": "idx_entities_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_customer_internal_desc": { + "name": "idx_entities_customer_internal_desc", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"internal_id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_org_env_id": { + "name": "idx_entities_org_env_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entities_cursor": { + "name": "idx_entities_cursor", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entities_internal_customer_id_fkey": { + "name": "entities_internal_customer_id_fkey", + "tableFrom": "entities", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entities_internal_feature_id_fkey": { + "name": "entities_internal_feature_id_fkey", + "tableFrom": "entities", + "tableTo": "features", + "columnsFrom": ["internal_feature_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entities_org_id_fkey": { + "name": "entities_org_id_fkey", + "tableFrom": "entities", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entity_id_constraint": { + "name": "entity_id_constraint", + "nullsNotDistinct": false, + "columns": ["org_id", "env", "internal_customer_id", "id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entitlements": { + "name": "entitlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_reward_id": { + "name": "internal_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "allowance_type": { + "name": "allowance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowance": { + "name": "allowance", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "interval": { + "name": "interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "interval_count": { + "name": "interval_count", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "carry_from_previous": { + "name": "carry_from_previous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entity_feature_id": { + "name": "entity_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "expiry_duration": { + "name": "expiry_duration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expiry_length": { + "name": "expiry_length", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "rollover": { + "name": "rollover", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_entitlements_internal_product_id": { + "name": "idx_entitlements_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_internal_reward_id": { + "name": "idx_entitlements_internal_reward_id", + "columns": [ + { + "expression": "internal_reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_reward_feature": { + "name": "idx_entitlements_reward_feature", + "columns": [ + { + "expression": "internal_reward_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_feature_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_entitlements_internal_reward_id_c_partial": { + "name": "idx_entitlements_internal_reward_id_c_partial", + "columns": [ + { + "expression": "\"internal_reward_id\" COLLATE \"C\"", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"entitlements\".\"internal_reward_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlements_internal_feature_id_fkey": { + "name": "entitlements_internal_feature_id_fkey", + "tableFrom": "entitlements", + "tableTo": "features", + "columnsFrom": ["internal_feature_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entitlements_internal_product_id_fkey": { + "name": "entitlements_internal_product_id_fkey", + "tableFrom": "entitlements", + "tableTo": "products", + "columnsFrom": ["internal_product_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "entitlements_internal_reward_id_fkey": { + "name": "entitlements_internal_reward_id_fkey", + "tableFrom": "entitlements", + "tableTo": "rewards", + "columnsFrom": ["internal_reward_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "entitlements_id_key": { + "name": "entitlements_id_key", + "nullsNotDistinct": false, + "columns": ["id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "set_usage": { + "name": "set_usage", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deductions": { + "name": "deductions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_events_internal_customer_id": { + "name": "idx_events_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_events_internal_entity_id": { + "name": "idx_events_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_events_customer_non_usage_ts": { + "name": "idx_events_customer_non_usage_ts", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"timestamp\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"events\".\"set_usage\" = false", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_internal_customer_id_fkey": { + "name": "events_internal_customer_id_fkey", + "tableFrom": "events", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_event_constraint": { + "name": "unique_event_constraint", + "nullsNotDistinct": false, + "columns": [ + "org_id", + "env", + "customer_id", + "event_name", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.features": { + "name": "features", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "display": { + "name": "display", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "event_names": { + "name": "event_names", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "features_org_id_fkey": { + "name": "features_org_id_fkey", + "tableFrom": "features", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "feature_id_constraint": { + "name": "feature_id_constraint", + "nullsNotDistinct": false, + "columns": ["org_id", "id", "env"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.free_trials": { + "name": "free_trials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'day'" + }, + "length": { + "name": "length", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unique_fingerprint": { + "name": "unique_fingerprint", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "card_required": { + "name": "card_required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "on_end": { + "name": "on_end", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_free_trials_internal_product_id": { + "name": "idx_free_trials_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "free_trials_internal_product_id_fkey": { + "name": "free_trials_internal_product_id_fkey", + "tableFrom": "free_trials", + "tableTo": "products", + "columnsFrom": ["internal_product_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organizations_id_fk": { + "name": "invitation_organization_id_organizations_id_fk", + "tableFrom": "invitation", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.invoice_line_items": { + "name": "invoice_line_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_item_id": { + "name": "stripe_invoice_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_item_id": { + "name": "stripe_subscription_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_product_id": { + "name": "stripe_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_discountable": { + "name": "stripe_discountable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "amount_after_discounts": { + "name": "amount_after_discounts", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "stripe_quantity": { + "name": "stripe_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "total_quantity": { + "name": "total_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "paid_quantity": { + "name": "paid_quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_source": { + "name": "description_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_timing": { + "name": "billing_timing", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prorated": { + "name": "prorated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "price_id": { + "name": "price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "customer_product_ids": { + "name": "customer_product_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_price_ids": { + "name": "customer_price_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "customer_entitlement_ids": { + "name": "customer_entitlement_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "internal_feature_id": { + "name": "internal_feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feature_id": { + "name": "feature_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_period_start": { + "name": "effective_period_start", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "effective_period_end": { + "name": "effective_period_end", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "discounts": { + "name": "discounts", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "invoice_line_items_invoice_id_fkey": { + "name": "invoice_line_items_invoice_id_fkey", + "tableFrom": "invoice_line_items", + "tableTo": "invoices", + "columnsFrom": ["invoice_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoice_line_items_stripe_id_unique": { + "name": "invoice_line_items_stripe_id_unique", + "nullsNotDistinct": false, + "columns": ["stripe_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoice_templates": { + "name": "invoice_templates", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "footer": { + "name": "footer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "net_terms_days": { + "name": "net_terms_days", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_invoice_templates_org_id": { + "name": "idx_invoice_templates_org_id", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoice_templates_org_id_fkey": { + "name": "invoice_templates_org_id_fkey", + "tableFrom": "invoice_templates", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoice_templates_id_unique": { + "name": "invoice_templates_id_unique", + "nullsNotDistinct": false, + "columns": ["id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invoices": { + "name": "invoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "product_ids": { + "name": "product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "internal_product_ids": { + "name": "internal_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processor_type": { + "name": "processor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "hosted_invoice_url": { + "name": "hosted_invoice_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total": { + "name": "total", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "amount_paid": { + "name": "amount_paid", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "refunded_amount": { + "name": "refunded_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "discounts": { + "name": "discounts", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "items": { + "name": "items", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "idx_invoices_customer_created": { + "name": "idx_invoices_customer_created", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invoices_internal_entity_id": { + "name": "idx_invoices_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"invoices\".\"internal_entity_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoices_internal_customer_id_fkey": { + "name": "invoices_internal_customer_id_fkey", + "tableFrom": "invoices", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invoices_internal_entity_id_fkey": { + "name": "invoices_internal_entity_id_fkey", + "tableFrom": "invoices", + "tableTo": "entities", + "columnsFrom": ["internal_entity_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoices_stripe_id_key": { + "name": "invoices_stripe_id_key", + "nullsNotDistinct": false, + "columns": ["stripe_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organizations_id_fk": { + "name": "member_organization_id_organizations_id_fk", + "tableFrom": "member", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.metadata": { + "name": "metadata", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_invoice_id": { + "name": "stripe_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_checkout_session_id": { + "name": "stripe_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_errors": { + "name": "migration_errors", + "schema": "", + "columns": { + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_job_id": { + "name": "migration_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "migration_customers_internal_customer_id_fkey": { + "name": "migration_customers_internal_customer_id_fkey", + "tableFrom": "migration_errors", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_customers_migration_job_id_fkey": { + "name": "migration_customers_migration_job_id_fkey", + "tableFrom": "migration_errors", + "tableTo": "migration_jobs", + "columnsFrom": ["migration_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "migration_errors_pkey": { + "name": "migration_errors_pkey", + "columns": ["internal_customer_id", "migration_job_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_item_runs": { + "name": "migration_item_runs", + "schema": "", + "columns": { + "migration_item_run_id": { + "name": "migration_item_run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_internal_id": { + "name": "migration_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_run_id": { + "name": "migration_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "item_kind": { + "name": "item_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migration_item_runs_live_unique": { + "name": "migration_item_runs_live_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_item_runs\".\"dry_run\" = false", + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_item_runs_dry_run_unique": { + "name": "migration_item_runs_dry_run_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_item_runs\".\"dry_run\" = true", + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_item_runs_customer_recent_idx": { + "name": "migration_item_runs_customer_recent_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"updated_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"migration_item_runs\".\"item_kind\" = 'customer'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_jobs": { + "name": "migration_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_step": { + "name": "current_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_internal_product_id": { + "name": "from_internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to_internal_product_id": { + "name": "to_internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "step_details": { + "name": "step_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "migration_jobs_from_internal_product_id_fkey": { + "name": "migration_jobs_from_internal_product_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "products", + "columnsFrom": ["from_internal_product_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_jobs_org_id_fkey": { + "name": "migration_jobs_org_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_jobs_to_internal_product_id_fkey": { + "name": "migration_jobs_to_internal_product_id_fkey", + "tableFrom": "migration_jobs", + "tableTo": "products", + "columnsFrom": ["to_internal_product_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_runs": { + "name": "migration_runs", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_internal_id": { + "name": "migration_internal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dry_run": { + "name": "dry_run", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "lazy_run": { + "name": "lazy_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_run_id": { + "name": "trigger_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "only_ids": { + "name": "only_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "target_limit": { + "name": "target_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migration_runs_active_per_migration_unique": { + "name": "migration_runs_active_per_migration_unique", + "columns": [ + { + "expression": "migration_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"migration_runs\".\"status\" IN ('queued', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migration_runs_migration_internal_id_fkey": { + "name": "migration_runs_migration_internal_id_fkey", + "tableFrom": "migration_runs", + "tableTo": "migrations", + "columnsFrom": ["migration_internal_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "migration_runs_org_id_fkey": { + "name": "migration_runs_org_id_fkey", + "tableFrom": "migration_runs", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migrations": { + "name": "migrations", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "operations": { + "name": "operations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "prepared_state": { + "name": "prepared_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "no_billing_changes": { + "name": "no_billing_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "retry_failed": { + "name": "retry_failed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "migrations_org_env_id_unique": { + "name": "migrations_org_env_id_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migrations_org_id_fkey": { + "name": "migrations_org_id_fkey", + "tableFrom": "migrations", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": ["refresh_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_api_key_id": { + "name": "oauth_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_currency": { + "name": "default_currency", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'usd'" + }, + "stripe_connected": { + "name": "stripe_connected", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "stripe_config": { + "name": "stripe_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "test_stripe_connect": { + "name": "test_stripe_connect", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "live_stripe_connect": { + "name": "live_stripe_connect", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "processor_configs": { + "name": "processor_configs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "test_pkey": { + "name": "test_pkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_pkey": { + "name": "live_pkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "svix_config": { + "name": "svix_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "onboarded": { + "name": "onboarded", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "deployed": { + "name": "deployed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "redis_config": { + "name": "redis_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_organizations_name_trgm": { + "name": "idx_organizations_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organizations\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_organizations_slug_trgm": { + "name": "idx_organizations_slug_trgm", + "columns": [ + { + "expression": "\"slug\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organizations\".\"slug\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_organizations_created_at_id": { + "name": "idx_organizations_created_at_id", + "columns": [ + { + "expression": "\"createdAt\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": ["slug"] + }, + "organizations_test_pkey_key": { + "name": "organizations_test_pkey_key", + "nullsNotDistinct": false, + "columns": ["test_pkey"] + }, + "organizations_live_pkey_key": { + "name": "organizations_live_pkey_key", + "nullsNotDistinct": false, + "columns": ["live_pkey"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialId_idx": { + "name": "passkey_credentialId_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "passkey_credential_id_unique": { + "name": "passkey_credential_id_unique", + "nullsNotDistinct": false, + "columns": ["credential_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.prices": { + "name": "prices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text COLLATE \"C\"", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_product_id": { + "name": "internal_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tier_behavior": { + "name": "tier_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "is_custom": { + "name": "is_custom", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "entitlement_id": { + "name": "entitlement_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "proration_config": { + "name": "proration_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + } + }, + "indexes": { + "idx_prices_internal_product_id": { + "name": "idx_prices_internal_product_id", + "columns": [ + { + "expression": "internal_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_prices_entitlement_id": { + "name": "idx_prices_entitlement_id", + "columns": [ + { + "expression": "entitlement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "prices_entitlement_id_fkey": { + "name": "prices_entitlement_id_fkey", + "tableFrom": "prices", + "tableTo": "entitlements", + "columnsFrom": ["entitlement_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "prices_internal_product_id_fkey": { + "name": "prices_internal_product_id_fkey", + "tableFrom": "prices", + "tableTo": "products", + "columnsFrom": ["internal_product_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "prices_id_key": { + "name": "prices_id_key", + "nullsNotDistinct": false, + "columns": ["id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.products": { + "name": "products", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_add_on": { + "name": "is_add_on", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "version": { + "name": "version", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "processor": { + "name": "processor", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "null" + }, + "base_variant_id": { + "name": "base_variant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_products_org_env_id_version": { + "name": "idx_products_org_env_id_version", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "products_org_id_fkey": { + "name": "products_org_id_fkey", + "tableFrom": "products", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_product": { + "name": "unique_product", + "nullsNotDistinct": false, + "columns": ["org_id", "id", "env", "version"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.referral_codes": { + "name": "referral_codes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": false + }, + "internal_reward_program_id": { + "name": "internal_reward_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_referral_codes_internal_customer_id": { + "name": "idx_referral_codes_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "referral_codes_internal_customer_id_fkey": { + "name": "referral_codes_internal_customer_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_codes_internal_reward_program_id_fkey": { + "name": "referral_codes_internal_reward_program_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "reward_programs", + "columnsFrom": ["internal_reward_program_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "referral_codes_org_id_fkey": { + "name": "referral_codes_org_id_fkey", + "tableFrom": "referral_codes", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_codes_pkey": { + "name": "referral_codes_pkey", + "columns": ["code", "org_id", "env"] + } + }, + "uniqueConstraints": { + "referral_codes_id_key": { + "name": "referral_codes_id_key", + "nullsNotDistinct": false, + "columns": ["id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.replaceables": { + "name": "replaceables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cus_ent_id": { + "name": "cus_ent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "from_entity_id": { + "name": "from_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delete_next_cycle": { + "name": "delete_next_cycle", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "idx_replaceables_cus_ent_id": { + "name": "idx_replaceables_cus_ent_id", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "replaceables_cus_ent_id_fkey": { + "name": "replaceables_cus_ent_id_fkey", + "tableFrom": "replaceables", + "tableTo": "customer_entitlements", + "columnsFrom": ["cus_ent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.revenuecat_mappings": { + "name": "revenuecat_mappings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "autumn_product_id": { + "name": "autumn_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revenuecat_product_ids": { + "name": "revenuecat_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": {}, + "foreignKeys": { + "revenuecat_mappings_org_id_fkey": { + "name": "revenuecat_mappings_org_id_fkey", + "tableFrom": "revenuecat_mappings", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revenuecat_mappings_pkey": { + "name": "revenuecat_mappings_pkey", + "columns": ["org_id", "env", "autumn_product_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reward_programs": { + "name": "reward_programs", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "internal_reward_id": { + "name": "internal_reward_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_redemptions": { + "name": "max_redemptions", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unlimited_redemptions": { + "name": "unlimited_redemptions", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "when": { + "name": "when", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'immediately'" + }, + "product_ids": { + "name": "product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{\"\"}'" + }, + "exclude_trial": { + "name": "exclude_trial", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "received_by": { + "name": "received_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "reward_triggers_internal_reward_id_fkey": { + "name": "reward_triggers_internal_reward_id_fkey", + "tableFrom": "reward_programs", + "tableTo": "rewards", + "columnsFrom": ["internal_reward_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_triggers_org_id_fkey": { + "name": "reward_triggers_org_id_fkey", + "tableFrom": "reward_programs", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reward_redemptions": { + "name": "reward_redemptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text COLLATE \"C\"", + "primaryKey": false, + "notNull": false + }, + "triggered": { + "name": "triggered", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "internal_reward_program_id": { + "name": "internal_reward_program_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "applied": { + "name": "applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "redeemer_applied": { + "name": "redeemer_applied", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "referral_code_id": { + "name": "referral_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reward_internal_id": { + "name": "reward_internal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promo_code": { + "name": "promo_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_reward_redemptions_referral_code_id": { + "name": "idx_reward_redemptions_referral_code_id", + "columns": [ + { + "expression": "referral_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reward_redemptions_reward_internal_id": { + "name": "idx_reward_redemptions_reward_internal_id", + "columns": [ + { + "expression": "reward_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_reward_redemptions_customer_reward": { + "name": "idx_reward_redemptions_customer_reward", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reward_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reward_redemptions_internal_customer_id_fkey": { + "name": "reward_redemptions_internal_customer_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_redemptions_internal_reward_program_id_fkey": { + "name": "reward_redemptions_internal_reward_program_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "reward_programs", + "columnsFrom": ["internal_reward_program_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reward_redemptions_referral_code_id_fkey": { + "name": "reward_redemptions_referral_code_id_fkey", + "tableFrom": "reward_redemptions", + "tableTo": "referral_codes", + "columnsFrom": ["referral_code_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rewards": { + "name": "rewards", + "schema": "", + "columns": { + "internal_id": { + "name": "internal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discount_config": { + "name": "discount_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "free_product_config": { + "name": "free_product_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "free_product_id": { + "name": "free_product_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promo_codes": { + "name": "promo_codes", + "type": "jsonb[]", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "coupons_org_id_fkey": { + "name": "coupons_org_id_fkey", + "tableFrom": "rewards", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rollovers": { + "name": "rollovers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "cus_ent_id": { + "name": "cus_ent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entities": { + "name": "entities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_rollovers_cus_ent_id": { + "name": "idx_rollovers_cus_ent_id", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_rollovers_cus_ent_expires": { + "name": "idx_rollovers_cus_ent_expires", + "columns": [ + { + "expression": "cus_ent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rollover_cus_ent_id_fkey": { + "name": "rollover_cus_ent_id_fkey", + "tableFrom": "rollovers", + "tableTo": "customer_entitlements", + "columnsFrom": ["cus_ent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.phases": { + "name": "phases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "customer_product_ids": { + "name": "customer_product_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "phases_schedule_id_fkey": { + "name": "phases_schedule_id_fkey", + "tableFrom": "phases", + "tableTo": "schedules", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "phases_schedule_id_starts_at_key": { + "name": "phases_schedule_id_starts_at_key", + "nullsNotDistinct": false, + "columns": ["schedule_id", "starts_at"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedules": { + "name": "schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_customer_id": { + "name": "internal_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_entity_id": { + "name": "internal_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "schedules_customer_scope_unique": { + "name": "schedules_customer_scope_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"schedules\".\"internal_entity_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "schedules_entity_scope_unique": { + "name": "schedules_entity_scope_unique", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"schedules\".\"internal_entity_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_schedules_internal_customer_id": { + "name": "idx_schedules_internal_customer_id", + "columns": [ + { + "expression": "internal_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_schedules_internal_entity_id": { + "name": "idx_schedules_internal_entity_id", + "columns": [ + { + "expression": "internal_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedules_org_id_fkey": { + "name": "schedules_org_id_fkey", + "tableFrom": "schedules", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedules_internal_customer_id_fkey": { + "name": "schedules_internal_customer_id_fkey", + "tableFrom": "schedules", + "tableTo": "customers", + "columnsFrom": ["internal_customer_id"], + "columnsTo": ["internal_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "schedules_internal_entity_id_fkey": { + "name": "schedules_internal_entity_id_fkey", + "tableFrom": "schedules", + "tableTo": "entities", + "columnsFrom": ["internal_entity_id"], + "columnsTo": ["internal_id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_id": { + "name": "stripe_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "ROUND(date_part('epoch', NOW()) * 1000)::BIGINT" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "usage_features": { + "name": "usage_features", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "subscriptions_org_id_fkey": { + "name": "subscriptions_org_id_fkey", + "tableFrom": "subscriptions", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "subscriptions_stripe_id_key": { + "name": "subscriptions_stripe_id_key", + "nullsNotDistinct": false, + "columns": ["stripe_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_user_name_trgm": { + "name": "idx_user_name_trgm", + "columns": [ + { + "expression": "\"name\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user\".\"name\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_user_email_trgm": { + "name": "idx_user_email_trgm", + "columns": [ + { + "expression": "\"email\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user\".\"email\" IS NOT NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "idx_user_created_at_id": { + "name": "idx_user_created_at_id", + "columns": [ + { + "expression": "\"created_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_created_by_fkey": { + "name": "user_created_by_fkey", + "tableFrom": "user", + "tableTo": "organizations", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vercel_resources": { + "name": "vercel_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "env": { + "name": "env", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "vercel_resources_installation_name_unique_idx": { + "name": "vercel_resources_installation_name_unique_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status <> 'uninstalled'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vercel_resources_org_id_fkey": { + "name": "vercel_resources_org_id_fkey", + "tableFrom": "vercel_resources", + "tableTo": "organizations", + "columnsFrom": ["org_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": true + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} From 1769bb26c417316d10e335f2b47f7105a8f6c697 Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 17:13:07 +0100 Subject: [PATCH 09/12] fix --- .../auth/actions/registerMcpOAuthClient.ts | 5 +- .../internal/auth/oauth/atmnOAuthClients.ts | 11 ++- .../auth/oauth/handleGetOAuthClient.ts | 13 +-- .../auth/oauth/handleOAuthTokenWithApiKey.ts | 85 ++++++++++++++++--- .../cli/handlers/handleCreateOAuthApiKeys.ts | 13 ++- .../tests/unit/auth/atmnOAuthClients.test.ts | 31 +++++++ 6 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 server/tests/unit/auth/atmnOAuthClients.test.ts diff --git a/server/src/internal/auth/actions/registerMcpOAuthClient.ts b/server/src/internal/auth/actions/registerMcpOAuthClient.ts index e687be395..59e650a54 100644 --- a/server/src/internal/auth/actions/registerMcpOAuthClient.ts +++ b/server/src/internal/auth/actions/registerMcpOAuthClient.ts @@ -236,12 +236,13 @@ export const registerMcpOAuthClient = async ({ return { error: "unsupported_mcp_client", status: 400 }; } - const cacheKey = `${info.type}:${[...redirectUris].sort().join("|")}`; + const requestedScopes = getRequestedScopes(scope); + const scopeKey = [...requestedScopes].sort().join(" "); + const cacheKey = `${info.type}:${[...redirectUris].sort().join("|")}:${scopeKey}`; const cached = getCachedRegistration(cacheKey); if (cached) return { body: cached as RegistrationResponse["body"], status: 200 }; - const requestedScopes = getRequestedScopes(scope); const clients = await oauthClientRepo.list({ db }); const existingClient = clients.find((client) => clientMatches({ client, info, redirectUris })) ?? diff --git a/server/src/internal/auth/oauth/atmnOAuthClients.ts b/server/src/internal/auth/oauth/atmnOAuthClients.ts index bcc860ee3..40673e395 100644 --- a/server/src/internal/auth/oauth/atmnOAuthClients.ts +++ b/server/src/internal/auth/oauth/atmnOAuthClients.ts @@ -24,10 +24,13 @@ const metadataMarksAtmn = (metadata: unknown) => { if (!metadataObject || typeof metadataObject !== "object") return false; - const values = Object.values(metadataObject as Record); - return values.some( - (value) => - typeof value === "string" && ["atmn", "autumn-cli"].includes(value), + const metadataRecord = metadataObject as Record; + return ( + metadataRecord.kind === "atmn" || + metadataRecord.client === "atmn" || + metadataRecord.clientType === "atmn" || + metadataRecord.client_type === "atmn" || + metadataRecord.source === "autumn-cli" ); }; diff --git a/server/src/internal/auth/oauth/handleGetOAuthClient.ts b/server/src/internal/auth/oauth/handleGetOAuthClient.ts index 72c0598e7..57d21503a 100644 --- a/server/src/internal/auth/oauth/handleGetOAuthClient.ts +++ b/server/src/internal/auth/oauth/handleGetOAuthClient.ts @@ -20,15 +20,18 @@ export const handleGetOAuthClient = async (c: Context) => { return c.json({ error: "Client not found" }, 404); } - const internalMcpName = getInternalMcpDisplayName({ - metadata: client.metadata, - redirectUri, - }); + const isInternalMcp = isInternalMcpOAuthClientRecord(client); + const internalMcpName = isInternalMcp + ? getInternalMcpDisplayName({ + metadata: client.metadata, + redirectUri, + }) + : null; return c.json({ client_id: client.clientId, name: internalMcpName || client.name || "Unknown Application", is_atmn: isAtmnOAuthClientRecord(client), - is_internal_mcp: isInternalMcpOAuthClientRecord(client), + is_internal_mcp: isInternalMcp, }); }; diff --git a/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts b/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts index c89458e9e..946d47eb8 100644 --- a/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts +++ b/server/src/internal/auth/oauth/handleOAuthTokenWithApiKey.ts @@ -11,6 +11,66 @@ import { const getString = (value: unknown) => typeof value === "string" && value.length > 0 ? value : null; +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const getTokenPayload = (body: Record) => { + const response = body.response; + if (isRecord(response)) return response; + return body; +}; + +const rewriteTokenBody = ({ + apiKey, + body, + scopes, +}: { + apiKey: string; + body: Record; + scopes: string[]; +}) => { + const response = body.response; + if (isRecord(response)) { + return { + ...body, + response: { + ...response, + access_token: apiKey, + scope: scopes.join(" "), + }, + }; + } + + return { + ...body, + access_token: apiKey, + scope: scopes.join(" "), + }; +}; + +const tokenResponseHeaders = (response?: Response) => { + const headers = new Headers(response?.headers); + headers.set("Content-Type", "application/json"); + headers.set("Cache-Control", "no-store"); + headers.set("Pragma", "no-cache"); + headers.delete("Content-Length"); + return headers; +}; + +const jsonTokenResponse = ({ + body, + response, + status, +}: { + body: unknown; + response?: Response; + status: number; +}) => + new Response(JSON.stringify(body), { + status, + headers: tokenResponseHeaders(response), + }); + const getResourceFromTokenRequest = async (request: Request) => { const contentType = request.headers.get("content-type") ?? ""; const rawBody = await request.text(); @@ -43,10 +103,11 @@ export const handleOAuthTokenWithApiKey = async (c: Context) => { return response; } - const accessToken = getString(body.access_token); + const tokenPayload = getTokenPayload(body); + const accessToken = getString(tokenPayload.access_token); if (!accessToken) return response; - const requestedScopes = scopesFromOAuthScopeString(body.scope); + const requestedScopes = scopesFromOAuthScopeString(tokenPayload.scope); let apiKeyResult: Awaited>; try { const tokenRecord = await getOAuthAccessTokenRecord({ @@ -62,21 +123,25 @@ export const handleOAuthTokenWithApiKey = async (c: Context) => { }); } catch (error) { if (error instanceof RecaseError) { - return c.json( - { + return jsonTokenResponse({ + body: { error: "invalid_grant", error_description: error.message, }, - error.statusCode as 400 | 401 | 403, - ); + status: error.statusCode, + }); } throw error; } if (!apiKeyResult) return response; - return c.json({ - ...body, - access_token: apiKeyResult.apiKey, - scope: apiKeyResult.scopes.join(" "), + return jsonTokenResponse({ + body: rewriteTokenBody({ + apiKey: apiKeyResult.apiKey, + body, + scopes: apiKeyResult.scopes, + }), + response, + status: response.status, }); }; diff --git a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts index f089160b7..7135434c2 100644 --- a/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts +++ b/server/src/internal/dev/cli/handlers/handleCreateOAuthApiKeys.ts @@ -4,6 +4,7 @@ import { getExternalOAuthApiKeyForToken, getOAuthAccessTokenRecord, } from "@/internal/auth/oauth/oauthAccessTokenApiKey.js"; +import { oauthConsentRepo } from "@/internal/auth/repos/index.js"; import { ApiKeyPrefix, createKey } from "../../api-keys/apiKeyUtils.js"; import { type OAuthApiKeyRequestBody, @@ -97,9 +98,17 @@ export const handleCreateOAuthApiKeys = createRoute({ }); } - // Build meta with consent linkage + const consent = await oauthConsentRepo.getForClientUserOrg({ + db, + clientId, + userId, + referenceId: orgId, + }); + const meta = { - oauth_consent_id: null, + oauth_consent_id: consent?.id ?? null, + oauth_client_id: clientId, + oauth_redirect_uri: consent?.redirectUri ?? null, created_via: "oauth", generatedAt: new Date().toISOString(), }; diff --git a/server/tests/unit/auth/atmnOAuthClients.test.ts b/server/tests/unit/auth/atmnOAuthClients.test.ts new file mode 100644 index 000000000..afa752f3a --- /dev/null +++ b/server/tests/unit/auth/atmnOAuthClients.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "bun:test"; +import { isAtmnOAuthClientRecord } from "@/internal/auth/oauth/atmnOAuthClients.js"; + +describe("isAtmnOAuthClientRecord", () => { + test("does not classify arbitrary metadata values as atmn", () => { + expect( + isAtmnOAuthClientRecord({ + clientId: "client_123", + name: "Third Party App", + metadata: { description: "connects to atmn projects" }, + }), + ).toBe(false); + }); + + test("classifies explicit atmn metadata and names", () => { + expect( + isAtmnOAuthClientRecord({ + clientId: "client_123", + name: "Third Party App", + metadata: { kind: "atmn" }, + }), + ).toBe(true); + + expect( + isAtmnOAuthClientRecord({ + clientId: "client_123", + name: "atmn", + }), + ).toBe(true); + }); +}); From 98b9eab141bbc03c4150e1c453fefed294a3642c Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 18:59:40 +0100 Subject: [PATCH 10/12] fix: rollovers --- ai | 2 +- .../handlePrepaidPrices.ts | 2 +- .../handleInvoiceCreated/handleUsagePrices.ts | 4 +- ...rocessConsumablePricesForInvoiceCreated.ts | 4 +- .../processPrepaidPricesForInvoiceCreated.ts | 7 +- .../getApiBalance/apiBalanceV2Utils.ts | 10 +- .../invoice-created-rollover-expiry.test.ts | 125 ++++++++++++++++++ ...t-customer-entity-rollover-granted.test.ts | 77 +++++++++++ .../integration/utils/expectBalanceCorrect.ts | 26 +++- ...epaid-consumable-rollover-scenario.test.ts | 22 ++- vite/src/services/customers/CusService.tsx | 2 +- 11 files changed, 258 insertions(+), 23 deletions(-) create mode 100644 server/tests/integration/billing/stripe-webhooks/invoice-created/invoice-created-rollover-expiry.test.ts create mode 100644 server/tests/integration/crud/customers/get-customer-entity-rollover-granted.test.ts diff --git a/ai b/ai index db7737aca..0e52f71fb 160000 --- a/ai +++ b/ai @@ -1 +1 @@ -Subproject commit db7737aca7d9d613fcc9a49f8aa8690253505e03 +Subproject commit 0e52f71fbd69e7a4a58b63863a8c4929bfd9ebf8 diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts index 8565fc0cd..d909817f4 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts @@ -79,7 +79,7 @@ export const handlePrepaidPrices = async ({ const rolloverUpdate = getRolloverUpdates({ cusEnt, - nextResetAt: end * 1000, + nextResetAt: start * 1000, }); if (notNullish(options?.upcoming_quantity)) { diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts index c2c46a484..da2a9c936 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts @@ -149,7 +149,7 @@ export const handleUsagePrices = async ({ allowance: ent.interval === EntInterval.Lifetime ? 0 : ent.allowance!, }); - const { end } = subToPeriodStartEnd({ sub: usageSub }); + const { start, end } = subToPeriodStartEnd({ sub: usageSub }); await CusEntService.update({ ctx, id: relatedCusEnt.id, @@ -162,7 +162,7 @@ export const handleUsagePrices = async ({ const rolloverUpdate = getRolloverUpdates({ cusEnt: relatedCusEnt, - nextResetAt: end * 1000, + nextResetAt: start * 1000, }); if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) { diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts index cfeda7de5..16f5abfde 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts @@ -9,9 +9,9 @@ import { eventContextToArrearLineItems } from "@/external/stripe/webhookHandlers import { lineItemsToCreateInvoiceItemsParams } from "@/internal/billing/v2/providers/stripe/utils/invoiceLines/lineItemsToCreateInvoiceItemsParams"; import { createStripeInvoiceItems } from "@/internal/billing/v2/providers/stripe/utils/invoices/stripeInvoiceOps"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService"; -import { deleteCachedFullCustomer } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/deleteCachedFullCustomer"; import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService"; import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils"; +import { deleteCachedFullCustomer } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/deleteCachedFullCustomer"; import { parseSkipOverageSubmissionFlag } from "@/internal/misc/featureFlags/parseSkipOverageSubmission"; import type { StripeWebhookContext } from "../../../webhookMiddlewares/stripeWebhookContext"; import type { InvoiceCreatedContext } from "../setupInvoiceCreatedContext"; @@ -116,7 +116,7 @@ export const processConsumablePricesForInvoiceCreated = async ({ updateCustomerEntitlements.forEach(async (update) => { const rolloverUpdates = getRolloverUpdates({ cusEnt: update.customerEntitlement, - nextResetAt: Date.now(), + nextResetAt: invoicePeriodEndMs, }); const fullCusEnt: FullCusEntWithProduct = { diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts index 5384211f5..5e2db5331 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts @@ -41,8 +41,7 @@ const processPrepaidPrice = async ({ const customerProduct = customerEntitlement.customer_product; - const { stripeSubscription, fullCustomer } = eventContext; - const { db } = ctx; + const { stripeSubscription } = eventContext; if (!options) return; const previousQuantity = options?.quantity ?? 0; @@ -60,11 +59,11 @@ const processPrepaidPrice = async ({ const ent = customerEntitlement.entitlement; - const { end } = subToPeriodStartEnd({ sub: stripeSubscription }); + const { start, end } = subToPeriodStartEnd({ sub: stripeSubscription }); const rolloverUpdate = getRolloverUpdates({ cusEnt: customerEntitlement, - nextResetAt: end * 1000, + nextResetAt: start * 1000, }); if (notNullish(options?.upcoming_quantity) && customerProduct) { diff --git a/server/src/internal/customers/cusUtils/getApiCustomerV2/getApiBalance/apiBalanceV2Utils.ts b/server/src/internal/customers/cusUtils/getApiCustomerV2/getApiBalance/apiBalanceV2Utils.ts index 2f3fdd5f9..e099d1cbb 100644 --- a/server/src/internal/customers/cusUtils/getApiCustomerV2/getApiBalance/apiBalanceV2Utils.ts +++ b/server/src/internal/customers/cusUtils/getApiCustomerV2/getApiBalance/apiBalanceV2Utils.ts @@ -37,13 +37,19 @@ export const mergeAggregatedBalanceIntoApiBalanceV2 = ({ const aggregatedRolloverBalance = aggregatedFeatureBalance.rollover_balance ?? 0; const aggregatedRolloverUsage = aggregatedFeatureBalance.rollover_usage ?? 0; + const aggregatedRolloverGrant = new Decimal(aggregatedRolloverBalance) + .add(aggregatedRolloverUsage) + .toNumber(); // Aggregate rows do not retain the full per-entity/per-product breakdown, so // the top-level summary is merged from the coarse aggregate values only. - const granted = new Decimal(aggregatedAllowance) + const baseGranted = new Decimal(aggregatedAllowance) .add(aggregatedPrepaidGrantFromOptions) .add(aggregatedAdjustment) .toNumber(); + const granted = new Decimal(baseGranted) + .add(aggregatedRolloverGrant) + .toNumber(); // Main remaining is floored at 0 (matches legacy behaviour). Rollover // remaining is added on top, since rollover balances are independent of @@ -57,7 +63,7 @@ export const mergeAggregatedBalanceIntoApiBalanceV2 = ({ .toNumber(); // Usage mirrors the entity-view formula: (granted - main balance) + rollover usage. - const usage = new Decimal(granted) + const usage = new Decimal(baseGranted) .sub(aggregatedBalance) .add(aggregatedRolloverUsage); diff --git a/server/tests/integration/billing/stripe-webhooks/invoice-created/invoice-created-rollover-expiry.test.ts b/server/tests/integration/billing/stripe-webhooks/invoice-created/invoice-created-rollover-expiry.test.ts new file mode 100644 index 000000000..ef73c3341 --- /dev/null +++ b/server/tests/integration/billing/stripe-webhooks/invoice-created/invoice-created-rollover-expiry.test.ts @@ -0,0 +1,125 @@ +// Red: usage-based rollovers expired from wall-clock time. +// Green: prepaid and usage-based one-month rollovers expire at next_reset_at. + +import { expect, test } from "bun:test"; +import { + type ApiCustomerV5, + type RolloverConfig, + RolloverExpiryDurationType, +} from "@autumn/shared"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; +import { + constructArrearItem, + constructPrepaidItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { expectBalanceCorrect } from "../../../utils/expectBalanceCorrect.js"; + +const rolloverConfig: RolloverConfig = { + max: null, + length: 1, + duration: RolloverExpiryDurationType.Month, +}; + +const expectOneMonthRolloverExpiresAtNextReset = ({ + customer, +}: { + customer: ApiCustomerV5; +}) => { + const balance = customer.balances[TestFeature.Messages]; + expect(balance).toBeDefined(); + expect(balance.next_reset_at).not.toBeNull(); + expect(balance.rollovers?.length ?? 0).toBeGreaterThan(0); + + const nextResetAt = balance.next_reset_at!; + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + nextResetAt, + positiveRolloverCount: 1, + }); + + const positiveRollovers = balance.rollovers!.filter( + (item) => item.balance > 0, + ); + const rollover = positiveRollovers[0]; + const expectedExpiry = nextResetAt; + const actualExpiry = rollover.expires_at; + const diff = Math.abs(actualExpiry - expectedExpiry); + + expect( + diff, + `Expected rollover to expire at ${new Date(expectedExpiry).toISOString()}, got ${new Date(actualExpiry).toISOString()}`, + ).toBeLessThanOrEqual(10 * 60 * 1000); +}; + +test.concurrent( + `${chalk.yellowBright("invoice.created rollover expiry: prepaid uses next reset boundary")}`, + async () => { + const prepaidItem = constructPrepaidItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + billingUnits: 100, + price: 10, + rolloverConfig, + }); + const pro = products.pro({ + id: "pro-prepaid-rollover-expiry", + items: [prepaidItem], + }); + + const { customerId, autumnV2_2 } = await initScenario({ + customerId: "invoice-created-prepaid-rollover-expiry", + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ + productId: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 200 }], + }), + s.track({ featureId: TestFeature.Messages, value: 50, timeout: 2000 }), + s.advanceToNextInvoice({ withPause: true }), + ], + }); + + const after = await autumnV2_2.customers.get(customerId); + expectOneMonthRolloverExpiresAtNextReset({ customer: after }); + }, +); + +test.concurrent( + `${chalk.yellowBright("invoice.created rollover expiry: usage-based uses next reset boundary")}`, + async () => { + const consumableItem = constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 200, + price: 0.1, + billingUnits: 1, + rolloverConfig, + }); + const pro = products.pro({ + id: "pro-consumable-rollover-expiry", + items: [consumableItem], + }); + + const { customerId, autumnV2_2 } = await initScenario({ + customerId: "invoice-created-consumable-rollover-expiry", + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ productId: pro.id }), + s.track({ featureId: TestFeature.Messages, value: 50, timeout: 2000 }), + s.advanceToNextInvoice({ withPause: true }), + ], + }); + + const after = await autumnV2_2.customers.get(customerId); + expectOneMonthRolloverExpiresAtNextReset({ customer: after }); + }, +); diff --git a/server/tests/integration/crud/customers/get-customer-entity-rollover-granted.test.ts b/server/tests/integration/crud/customers/get-customer-entity-rollover-granted.test.ts new file mode 100644 index 000000000..ad97b2f88 --- /dev/null +++ b/server/tests/integration/crud/customers/get-customer-entity-rollover-granted.test.ts @@ -0,0 +1,77 @@ +// Red: customer aggregation omitted rollover grant from entity-scoped products. +// Green: customer granted includes active entity rollover balance and usage. + +import { expect, test } from "bun:test"; +import { + type ApiCustomerV5, + type LimitedItem, + ProductItemInterval, + RolloverExpiryDurationType, +} from "@autumn/shared"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect.js"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { expireAllCusEntsForReset } from "@tests/utils/cusProductUtils/resetTestUtils.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; +import chalk from "chalk"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; + +test.concurrent( + `${chalk.yellowBright("get-customer: entity product rollovers contribute to granted")}`, + async () => { + const customerId = "get-customer-entity-rollover-granted"; + const rolloverConfig = { + max: 500, + length: 1, + duration: RolloverExpiryDurationType.Month, + }; + const creditsItem = constructFeatureItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + interval: ProductItemInterval.Month, + rolloverConfig, + }) as LimitedItem; + const base = products.base({ + id: "entity-product-rollover-granted", + items: [creditsItem], + }); + + const { autumnV1, autumnV2_2, ctx, entities } = await initScenario({ + customerId, + setup: [ + s.deleteCustomer({ customerId }), + s.customer({ paymentMethod: "success", testClock: false }), + s.products({ list: [base] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [s.billing.attach({ productId: base.id, entityIndex: 0 })], + }); + + await autumnV1.track({ + customer_id: customerId, + entity_id: entities[0].id, + feature_id: TestFeature.Credits, + value: 40, + }); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + await expireAllCusEntsForReset({ + ctx, + customerId, + featureId: TestFeature.Credits, + }); + await autumnV2_2.entities.get(customerId, entities[0].id); + + const after = await autumnV2_2.customers.get(customerId, { + skip_cache: "true", + }); + + expectBalanceCorrect({ + customer: after, + featureId: TestFeature.Credits, + remaining: 160, + usage: 0, + }); + expect(after.balances[TestFeature.Credits].granted).toBe(160); + }, +); diff --git a/server/tests/integration/utils/expectBalanceCorrect.ts b/server/tests/integration/utils/expectBalanceCorrect.ts index 096959591..baed0fc22 100644 --- a/server/tests/integration/utils/expectBalanceCorrect.ts +++ b/server/tests/integration/utils/expectBalanceCorrect.ts @@ -8,10 +8,10 @@ import { type ResetInterval, } from "@autumn/shared"; -const roundTo8Dp = (value: number) => - Math.round(value * 1e8) / 1e8; +const roundTo8Dp = (value: number) => Math.round(value * 1e8) / 1e8; type BucketExpectation = { + granted?: number; included_grant?: number; prepaid_grant?: number; remaining?: number; @@ -28,6 +28,7 @@ type BreakdownExpectation = Partial>; export const expectBalanceCorrect = ({ customer, featureId, + granted, remaining, planId, usage, @@ -35,10 +36,12 @@ export const expectBalanceCorrect = ({ toleranceMs = TEN_MINUTES_MS, breakdown, rollovers, + positiveRolloverCount, }: { customer: ApiCustomerV5 | ApiEntityV2; featureId: string; - remaining: number; + granted?: number; + remaining?: number; planId?: string | null; usage?: number; nextResetAt?: number | null; @@ -46,10 +49,18 @@ export const expectBalanceCorrect = ({ breakdown?: BreakdownExpectation; /** Expected rollovers in order (oldest first). Only specified fields are checked. */ rollovers?: Partial[]; + positiveRolloverCount?: number; }) => { const balance = customer.balances[featureId]; expect(balance).toBeDefined(); - expect(roundTo8Dp(balance.remaining)).toBe(roundTo8Dp(remaining)); + + if (typeof granted !== "undefined") { + expect(roundTo8Dp(balance.granted)).toBe(roundTo8Dp(granted)); + } + + if (typeof remaining !== "undefined") { + expect(roundTo8Dp(balance.remaining)).toBe(roundTo8Dp(remaining)); + } if (typeof planId !== "undefined") { expect(balance.breakdown?.[0]?.plan_id ?? null).toBe(planId); @@ -107,4 +118,11 @@ export const expectBalanceCorrect = ({ expect(actual![i]).toMatchObject(rollovers[i]); } } + + if (typeof positiveRolloverCount !== "undefined") { + const actual = balance.rollovers ?? []; + expect(actual.filter((item) => item.balance > 0).length).toBe( + positiveRolloverCount, + ); + } }; diff --git a/server/tests/scenarios/attach/prepaid-consumable-rollover-scenario.test.ts b/server/tests/scenarios/attach/prepaid-consumable-rollover-scenario.test.ts index e70b903ef..d7444c7c1 100644 --- a/server/tests/scenarios/attach/prepaid-consumable-rollover-scenario.test.ts +++ b/server/tests/scenarios/attach/prepaid-consumable-rollover-scenario.test.ts @@ -1,13 +1,14 @@ import { test } from "bun:test"; -import { RolloverExpiryDurationType } from "@autumn/shared"; +import { type ApiCustomerV5, RolloverExpiryDurationType } from "@autumn/shared"; +import { expectBalanceCorrect } from "@tests/integration/utils/expectBalanceCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { products } from "@tests/utils/fixtures/products"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; import { constructArrearItem, constructPrepaidItem, } from "@/utils/scriptUtils/constructItem"; -import { products } from "@tests/utils/fixtures/products"; -import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; -import chalk from "chalk"; -import { TestFeature } from "@tests/setup/v2Features"; /** * Scenario: Prepaid + Consumable messages on the same plan, both with rollovers. @@ -48,7 +49,7 @@ test(`${chalk.yellowBright("scenario: prepaid + consumable messages with rollove items: [prepaidMessages, consumableMessages], }); - await initScenario({ + const { autumnV2_2 } = await initScenario({ customerId: "combo-rollover", setup: [ s.customer({ paymentMethod: "success" }), @@ -63,4 +64,13 @@ test(`${chalk.yellowBright("scenario: prepaid + consumable messages with rollove s.advanceToNextInvoice(), ], }); + + const customer = + await autumnV2_2.customers.get("combo-rollover"); + + expectBalanceCorrect({ + customer, + featureId: TestFeature.Messages, + positiveRolloverCount: 2, + }); }); diff --git a/vite/src/services/customers/CusService.tsx b/vite/src/services/customers/CusService.tsx index 075c459a8..2f13b709f 100644 --- a/vite/src/services/customers/CusService.tsx +++ b/vite/src/services/customers/CusService.tsx @@ -120,7 +120,7 @@ export class CusService { axios: AxiosInstance; customer_id: string; }): Promise<{ success: boolean }> { - const { data } = await axios.post(`/customers/clear_cache`, { + const { data } = await axios.post(`/v1/customers/clear_cache`, { customer_id, }); return data; From 88154ba5b837732f38ae2c98839694127c411e68 Mon Sep 17 00:00:00 2001 From: johnyeo Date: Thu, 4 Jun 2026 19:14:57 +0100 Subject: [PATCH 11/12] fix: carry over status --- .../computeCustomPlanNewCustomerProduct.ts | 2 +- .../update-plan-op-states.test.ts | 92 +++++++++++++++++- ...pdate-processor-no-billing-changes.test.ts | 97 ++++++++++++++++--- .../utils/expectCustomerProductStatuses.ts | 73 ++++++++++++++ 4 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 server/tests/integration/billing/utils/expectCustomerProductStatuses.ts diff --git a/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts index 745cfb392..939be3716 100644 --- a/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts @@ -90,7 +90,7 @@ export const computeCustomPlanNewCustomerProduct = ({ ? { subscriptionId: params.processor_subscription_id } : {}), - ...(params.status ? { status: params.status } : {}), + status: params.status ?? currentCustomerProduct.status, onTrialEnd: trialContext?.onEnd ?? diff --git a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts index 28a4cf79d..4f1cbcc13 100644 --- a/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts +++ b/server/tests/integration/billing/migrations-v2/update-plan-operation/update-plan-op-states.test.ts @@ -9,19 +9,23 @@ */ import { expect, test } from "bun:test"; -import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared"; import { CusProductStatus, + findActiveCustomerProductById, customerPrices, customerProducts, customers, prices, + type ApiCustomerV3, + type ApiEntityV0, } from "@autumn/shared"; import { + expectCustomerProducts, expectProductCanceling, expectProductNotPresent, expectProductScheduled, } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectCustomerProductStatuses } from "@tests/integration/billing/utils/expectCustomerProductStatuses"; import { expectNoExpiredCustomerProducts } from "@tests/integration/billing/utils/expectNoExpiredCustomerProducts"; import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect"; import { TestFeature } from "@tests/setup/v2Features"; @@ -31,6 +35,8 @@ import { products } from "@tests/utils/fixtures/products"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; import { and, eq, isNull } from "drizzle-orm"; +import { CusService } from "@/internal/customers/CusService"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; import { runUpdatePlanMigration } from "../utils/runUpdatePlanMigration"; const getScheduledIds = async ({ @@ -111,6 +117,90 @@ const getCustomerProductPriceAmounts = async ({ .filter((amount): amount is number => typeof amount === "number") .sort((a, b) => a - b); +// Red: version update_plan replacement reset a past_due cusProduct to active. +// Green: the replacement inherits past_due while the old row expires. +test.concurrent(`${chalk.yellowBright("migrations update_plan states: past_due survives version update")}`, async () => { + const customerId = "migration-update-state-past-due"; + const pro = products.pro({ + id: "pro", + items: [items.monthlyMessages({ includedUsage: 500 })], + }); + + const { autumnV1, autumnV2_2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + const fullCustomerBefore = await CusService.getFull({ + ctx, + idOrInternalId: customerId, + }); + const cusProductBefore = findActiveCustomerProductById({ + fullCus: fullCustomerBefore, + productId: pro.id, + }); + expect(cusProductBefore).toBeDefined(); + + await CusProductService.update({ + ctx, + cusProductId: cusProductBefore!.id, + updates: { status: CusProductStatus.PastDue }, + }); + + const invoiceCountBefore = + (await autumnV1.customers.get(customerId)).invoices + ?.length ?? 0; + + await autumnV1.products.update(pro.id, { + items: [ + items.monthlyPrice({ price: 20 }), + items.monthlyMessages({ includedUsage: 600 }), + ], + }); + + await runUpdatePlanMigration({ + ctx, + migrationClient: autumnV2_2, + migrationId: `${customerId}-mig`, + customerId, + runOnServer: false, + filter: { customer: { plan: { plan_id: pro.id } } }, + operations: { + customer: [ + { + type: "update_plan", + plan_filter: { plan_id: pro.id }, + version: 2, + }, + ], + }, + }); + + const customerAfter = await autumnV1.customers.get(customerId); + await expectCustomerProducts({ + customer: customerAfter, + pastDue: [pro.id], + }); + + const { byStatus } = await expectCustomerProductStatuses({ + ctx, + customerId, + productId: pro.id, + expected: { + [CusProductStatus.PastDue]: 1, + [CusProductStatus.Expired]: 1, + }, + }); + + expect(byStatus[CusProductStatus.PastDue]?.[0]?.product.version).toBe(2); + expect(customerAfter.invoices?.length ?? 0).toBe(invoiceCountBefore); + await expectStripeSubscriptionCorrect({ ctx, customerId }); +}); + test.concurrent(`${chalk.yellowBright("migrations update_plan states: scheduled downgrade survives active plan price update")}`, async () => { const customerId = "migration-update-state-downgrade"; const pro = products.pro({ diff --git a/server/tests/integration/billing/update-subscription/params/update-processor-no-billing-changes.test.ts b/server/tests/integration/billing/update-subscription/params/update-processor-no-billing-changes.test.ts index 39e0ea717..718e8b1a3 100644 --- a/server/tests/integration/billing/update-subscription/params/update-processor-no-billing-changes.test.ts +++ b/server/tests/integration/billing/update-subscription/params/update-processor-no-billing-changes.test.ts @@ -1,14 +1,18 @@ import { expect, test } from "bun:test"; import { + findActiveCustomerProductById, CusProductStatus, type UpdateSubscriptionV1ParamsInput, } from "@autumn/shared"; +import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { expectCustomerProductStatuses } from "@tests/integration/billing/utils/expectCustomerProductStatuses"; import { items } from "@tests/utils/fixtures/items"; import { itemsV2 } from "@tests/utils/fixtures/itemsV2"; import { products } from "@tests/utils/fixtures/products"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; import { CusService } from "@/internal/customers/CusService"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; test(`${chalk.yellowBright("processor_subscription_id: attach with existing stripe subscription anchors reset cycle")}`, async () => {}); @@ -35,10 +39,10 @@ test(`${chalk.yellowBright("update no_billing_changes: customize preserves subsc ctx, idOrInternalId: customerId, }); - const cusProductBefore = fullCustomerBefore.customer_products.find( - (cp) => - cp.product_id === pro.id && cp.status === CusProductStatus.Active, - ); + const cusProductBefore = findActiveCustomerProductById({ + fullCus: fullCustomerBefore, + productId: pro.id, + }); expect(cusProductBefore).toBeDefined(); const originalSubIds = cusProductBefore?.subscription_ids ?? []; expect(originalSubIds.length).toBeGreaterThan(0); @@ -54,16 +58,85 @@ test(`${chalk.yellowBright("update no_billing_changes: customize preserves subsc }, }); - const fullCustomerAfter = await CusService.getFull({ + await expectCustomerProducts({ + customer: await autumnV2.customers.get(customerId), + active: [pro.id], + }); + + const { byStatus } = await expectCustomerProductStatuses({ + ctx, + customerId, + productId: pro.id, + expected: { + [CusProductStatus.Active]: 1, + }, + }); + expect(byStatus[CusProductStatus.Active]?.[0]?.subscription_ids).toEqual( + originalSubIds, + ); +}); + +// Red: replacement-style updates reset a past_due cusProduct to active. +// Green: the replacement inherits status and keeps the subscription link. +test(`${chalk.yellowBright("update no_billing_changes: replacement customize preserves past_due status")}`, async () => { + const customerId = "update-no-billing-preserves-past-due"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const priceItem = items.monthlyPrice({ price: 20 }); + const pro = products.base({ id: "pro", items: [messagesItem, priceItem] }); + + const { autumnV1, autumnV2, ctx } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [s.attach({ productId: pro.id })], + }); + + const fullCustomerBefore = await CusService.getFull({ ctx, idOrInternalId: customerId, }); - const activeProRows = fullCustomerAfter.customer_products.filter( - (cp) => - cp.product_id === pro.id && cp.status === CusProductStatus.Active, - ); - expect(activeProRows.length).toBe(1); - const cusProductAfter = activeProRows[0]; + const cusProductBefore = findActiveCustomerProductById({ + fullCus: fullCustomerBefore, + productId: pro.id, + }); + expect(cusProductBefore).toBeDefined(); + const originalSubIds = cusProductBefore?.subscription_ids ?? []; + expect(originalSubIds.length).toBeGreaterThan(0); - expect(cusProductAfter.subscription_ids).toEqual(originalSubIds); + await CusProductService.update({ + ctx, + cusProductId: cusProductBefore!.id, + updates: { status: CusProductStatus.PastDue }, + }); + + await autumnV2.subscriptions.update({ + customer_id: customerId, + plan_id: pro.id, + no_billing_changes: true, + customize: { + price: itemsV2.monthlyPrice({ amount: 20 }), + items: [itemsV2.monthlyMessages({ included: 250 })], + }, + }); + + await expectCustomerProducts({ + customer: await autumnV1.customers.get(customerId), + pastDue: [pro.id], + }); + + const { byStatus } = await expectCustomerProductStatuses({ + ctx, + customerId, + productId: pro.id, + expected: { + [CusProductStatus.PastDue]: 1, + [CusProductStatus.Expired]: 1, + }, + }); + expect(byStatus[CusProductStatus.PastDue]?.[0]?.subscription_ids).toEqual( + originalSubIds, + ); }); diff --git a/server/tests/integration/billing/utils/expectCustomerProductStatuses.ts b/server/tests/integration/billing/utils/expectCustomerProductStatuses.ts new file mode 100644 index 000000000..93688283c --- /dev/null +++ b/server/tests/integration/billing/utils/expectCustomerProductStatuses.ts @@ -0,0 +1,73 @@ +import { expect } from "bun:test"; +import { + CusProductStatus, + type FullCusProduct, + type CusProductStatus as CusProductStatusType, +} from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { CusService } from "@/internal/customers/CusService"; + +type CustomerProductStatusesResult = { + customerProducts: FullCusProduct[]; + byStatus: Partial>; +}; + +export const expectCustomerProductStatuses = async ({ + ctx, + customerId, + productId, + entityId, + expected, +}: { + ctx: AutumnContext; + customerId: string; + productId: string; + entityId?: string; + expected: Partial>; +}): Promise => { + const fullCustomer = await CusService.getFull({ + ctx, + idOrInternalId: customerId, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Scheduled, + CusProductStatus.Expired, + ], + withEntities: true, + }); + + const customerProducts = fullCustomer.customer_products.filter( + (customerProduct) => + customerProduct.product_id === productId && + (entityId ? customerProduct.entity_id === entityId : true), + ); + + const byStatus = customerProducts.reduce< + Partial> + >((acc, customerProduct) => { + acc[customerProduct.status] = [ + ...(acc[customerProduct.status] ?? []), + customerProduct, + ]; + return acc; + }, {}); + + for (const [status, count] of Object.entries(expected)) { + const matchingCustomerProducts = + byStatus[status as CusProductStatusType] ?? []; + + expect( + matchingCustomerProducts.length, + `Expected ${count} ${status} rows for ${productId}; got ${JSON.stringify( + customerProducts.map((customerProduct) => ({ + id: customerProduct.id, + status: customerProduct.status, + version: customerProduct.product.version, + })), + )}`, + ).toBe(count); + } + + return { customerProducts, byStatus }; +}; From e8befdb3834ea7772f01900b03574350bc5edda0 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:03:19 +0100 Subject: [PATCH 12/12] =?UTF-8?q?fix:=20=F0=9F=90=9B=20hot=20fix=20cli=20b?= =?UTF-8?q?ugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/utils/scopeDefinitions.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/shared/utils/scopeDefinitions.ts b/shared/utils/scopeDefinitions.ts index 6aac7915f..1f9b50d32 100644 --- a/shared/utils/scopeDefinitions.ts +++ b/shared/utils/scopeDefinitions.ts @@ -675,6 +675,17 @@ function requirementMentions( return needles.some((n) => hay.includes(n as ScopeString)); } +/** + * Rewrite a legacy CRUDL requirement scope to its modern R/W equivalent so it + * can be matched against an expanded grant (which only ever holds modern + meta + * scopes). Modern and meta scopes pass through unchanged. Deliberately applies + * only LEGACY_SCOPE_ALIASES, not expandScopes, so a required `admin`/`owner` + * is never blown up into "every modern scope". + */ +function normaliseRequiredScope(scope: ScopeString): ScopeString { + return (LEGACY_SCOPE_ALIASES[scope] ?? scope) as ScopeString; +} + /** * Check whether a set of granted scopes satisfies a route's requirement. * @@ -705,7 +716,9 @@ export function checkScopes( // Shorthand: a plain array means ALL required. if (Array.isArray(required)) { - const missing = required.filter((s) => !expanded.has(s)); + const missing = required + .map(normaliseRequiredScope) + .filter((s) => !expanded.has(s)); return { allowed: missing.length === 0, missing }; } @@ -714,8 +727,8 @@ export function checkScopes( ANY?: readonly ScopeString[]; }; - const allList = req.ALL ?? []; - const anyList = req.ANY ?? []; + const allList = (req.ALL ?? []).map(normaliseRequiredScope); + const anyList = (req.ANY ?? []).map(normaliseRequiredScope); const missingAll = allList.filter((s) => !expanded.has(s)); const anySatisfied =