From 0d70815e81a7da84d104088a90c0c023a12cb0a9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 18:37:02 -0800 Subject: [PATCH 1/7] feat(billing): AWS Marketplace SaaS contract registration, claim linking, and entitlement reconcile - POST /api/marketplace/aws/register: the listing's fulfillment URL. Resolves the registration token with ResolveCustomer, rejects a foreign product, calls GetEntitlements for the license on every registration, upserts the license by ARN, and 303s to /marketplace/aws with a one-time httpOnly claim cookie when active. - /marketplace/aws onboarding page: sign in or sign up, review the license, and link it to an organization the viewer owns or administers. - GET /api/marketplace/aws/claim and POST /api/marketplace/aws/claim/link: contracted internal routes over application use cases. Linking requires the open claim and org admin, re-verifies the entitlement, refuses a license linked elsewhere, consumes the claim, audits the link, and emails the enterprise inbox. No subscription changes. - GET /api/cron/aws-marketplace-reconcile: drains agreement events from SQS in bounded batches and re-verifies licenses not checked within a day, under a Redis lock. - aws_marketplace_license table (migration 0454); AWS_MARKETPLACE_PRODUCT_CODE and AWS_MARKETPLACE_EVENTS_QUEUE_URL env vars. With no product code every surface is off. --- .../cron/aws-marketplace-reconcile/route.ts | 67 + .../api/marketplace/aws/claim/link/route.ts | 33 + .../app/api/marketplace/aws/claim/route.ts | 22 + .../app/api/marketplace/aws/register/route.ts | 83 + .../aws/aws-marketplace-onboarding.tsx | 268 + apps/sim/app/marketplace/aws/page.tsx | 30 + apps/sim/hooks/queries/aws-marketplace.ts | 37 + apps/sim/lib/api/contracts/aws-marketplace.ts | 60 + .../aws-marketplace/application/operations.ts | 22 + .../aws-marketplace/application/use-cases.ts | 258 + apps/sim/lib/billing/aws-marketplace/claim.ts | 27 + .../sim/lib/billing/aws-marketplace/client.ts | 39 + .../sim/lib/billing/aws-marketplace/config.ts | 10 + .../aws-marketplace/entitlements.test.ts | 162 + .../billing/aws-marketplace/entitlements.ts | 131 + .../aws-marketplace/lifecycle.integration.ts | 476 + .../lib/billing/aws-marketplace/onboarding.ts | 10 + .../lib/billing/aws-marketplace/reconcile.ts | 204 + .../billing/aws-marketplace/registration.ts | 91 + .../lib/billing/aws-marketplace/repository.ts | 203 + apps/sim/lib/core/config/env.ts | 2 + apps/sim/package.json | 2 + bun.lock | 10 + packages/audit/src/types.ts | 3 + .../0454_aws_marketplace_license.sql | 27 + .../db/migrations/meta/0454_snapshot.json | 32651 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 42 + packages/testing/src/mocks/audit.mock.ts | 2 + .../src/mocks/schema-tables.generated.ts | 20 + scripts/check-api-validation-contracts.ts | 1 + 31 files changed, 35000 insertions(+) create mode 100644 apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts create mode 100644 apps/sim/app/api/marketplace/aws/claim/link/route.ts create mode 100644 apps/sim/app/api/marketplace/aws/claim/route.ts create mode 100644 apps/sim/app/api/marketplace/aws/register/route.ts create mode 100644 apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx create mode 100644 apps/sim/app/marketplace/aws/page.tsx create mode 100644 apps/sim/hooks/queries/aws-marketplace.ts create mode 100644 apps/sim/lib/api/contracts/aws-marketplace.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/application/operations.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/application/use-cases.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/claim.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/client.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/config.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/entitlements.test.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/entitlements.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/onboarding.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/reconcile.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/registration.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/repository.ts create mode 100644 packages/db/migrations/0454_aws_marketplace_license.sql create mode 100644 packages/db/migrations/meta/0454_snapshot.json diff --git a/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts b/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts new file mode 100644 index 00000000000..bce125c48a5 --- /dev/null +++ b/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts @@ -0,0 +1,67 @@ +import { createLogger } from '@sim/logger' +import { generateShortId } from '@sim/utils/id' +import { type NextRequest, NextResponse } from 'next/server' +import { verifyCronAuth } from '@/lib/auth/internal' +import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' +import { reconcileAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/reconcile' +import { acquireLock, releaseLock } from '@/lib/core/config/redis' +import { runDetached } from '@/lib/core/utils/background' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +const logger = createLogger('AwsMarketplaceReconcileCron') + +const LOCK_KEY = 'aws-marketplace-reconcile-lock' +/** Covers a full drain plus the re-verification batch; the schedule runs every 15 minutes. */ +const LOCK_TTL_SECONDS = 10 * 60 + +export const dynamic = 'force-dynamic' + +/** + * Applies AWS Marketplace agreement events from the SQS queue and re-verifies licenses no event + * touched within a day. Acknowledges the cron call at once and works in the background; a Redis + * lock keeps runs from overlapping, and every write is idempotent, so a replayed run is harmless. + * A no-op on deployments without `AWS_MARKETPLACE_PRODUCT_CODE`. + */ +export const GET = withRouteHandler(async (request: NextRequest) => { + const authError = verifyCronAuth(request, 'AWS Marketplace reconcile') + if (authError) { + return authError + } + + if (!getAwsMarketplaceProductCode()) { + return NextResponse.json({ + success: true, + message: 'AWS Marketplace is not configured – skipped', + status: 'skip', + }) + } + + const lockValue = generateShortId() + const locked = await acquireLock(LOCK_KEY, lockValue, LOCK_TTL_SECONDS, { + reclaimOnFailure: true, + }) + if (!locked) { + return NextResponse.json( + { + success: true, + message: 'AWS Marketplace reconcile already in progress – skipped', + status: 'skip', + }, + { status: 202 } + ) + } + + runDetached('aws-marketplace-reconcile', async () => { + try { + const summary = await reconcileAwsMarketplaceLicenses() + logger.info('AWS Marketplace reconcile completed', { ...summary }) + } finally { + await releaseLock(LOCK_KEY, lockValue).catch(() => {}) + } + }) + + return NextResponse.json( + { success: true, message: 'AWS Marketplace reconcile started', status: 'started' }, + { status: 202 } + ) +}) diff --git a/apps/sim/app/api/marketplace/aws/claim/link/route.ts b/apps/sim/app/api/marketplace/aws/claim/link/route.ts new file mode 100644 index 00000000000..6b46a96f80a --- /dev/null +++ b/apps/sim/app/api/marketplace/aws/claim/link/route.ts @@ -0,0 +1,33 @@ +import { linkAwsMarketplaceLicenseContract } from '@/lib/api/contracts/aws-marketplace' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { linkAwsMarketplaceLicense } from '@/lib/billing/aws-marketplace/application/use-cases' +import { + AWS_MARKETPLACE_CLAIM_COOKIE, + serializeClaimCookie, +} from '@/lib/billing/aws-marketplace/claim' + +export const POST = defineInternalJsonRoute({ + contract: linkAwsMarketplaceLicenseContract, + operation: linkAwsMarketplaceLicense.operation, + useCase: linkAwsMarketplaceLicense, + auth: internalSessionAuth, + rateLimit: internalRateLimits.user({ bucketName: 'aws-marketplace-link' }), + errorPolicy: internalOrchestrationErrorPolicy, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, + mapInput: ({ body }, { request }) => ({ + organizationId: body.organizationId, + licenseId: body.licenseId, + claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, + }), + present: (_linked, { input }) => ({ + success: true as const, + organizationId: input.organizationId, + }), + /** The claim is spent once the link commits; the cookie that carried it goes with it. */ + finalizeResponse: () => ({ headers: { 'set-cookie': serializeClaimCookie(null) } }), +}) diff --git a/apps/sim/app/api/marketplace/aws/claim/route.ts b/apps/sim/app/api/marketplace/aws/claim/route.ts new file mode 100644 index 00000000000..673559651c5 --- /dev/null +++ b/apps/sim/app/api/marketplace/aws/claim/route.ts @@ -0,0 +1,22 @@ +import { getAwsMarketplaceClaimContract } from '@/lib/api/contracts/aws-marketplace' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { readAwsMarketplaceClaim } from '@/lib/billing/aws-marketplace/application/use-cases' +import { AWS_MARKETPLACE_CLAIM_COOKIE } from '@/lib/billing/aws-marketplace/claim' + +export const GET = defineInternalJsonRoute({ + contract: getAwsMarketplaceClaimContract, + operation: readAwsMarketplaceClaim.operation, + useCase: readAwsMarketplaceClaim, + auth: internalSessionAuth, + rateLimit: internalRateLimits.user({ bucketName: 'aws-marketplace-claim' }), + errorPolicy: internalOrchestrationErrorPolicy, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, + mapInput: (_parsed, { request }) => ({ + claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, + }), +}) diff --git a/apps/sim/app/api/marketplace/aws/register/route.ts b/apps/sim/app/api/marketplace/aws/register/route.ts new file mode 100644 index 00000000000..31a431aa5fa --- /dev/null +++ b/apps/sim/app/api/marketplace/aws/register/route.ts @@ -0,0 +1,83 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { awsMarketplaceRegistrationFormSchema } from '@/lib/api/contracts/aws-marketplace' +import { serializeClaimCookie } from '@/lib/billing/aws-marketplace/claim' +import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' +import { + AWS_MARKETPLACE_ONBOARDING_PATH, + type AwsMarketplaceOnboardingError, +} from '@/lib/billing/aws-marketplace/onboarding' +import { registerAwsMarketplaceBuyer } from '@/lib/billing/aws-marketplace/registration' +import { enforceIpRateLimit, type TokenBucketConfig } from '@/lib/core/rate-limiter' +import { readFormDataWithLimit } from '@/lib/core/utils/stream-limits' +import { getBaseUrl } from '@/lib/core/utils/urls' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('AwsMarketplaceRegisterAPI') + +/** The form carries one opaque token; anything larger is not from AWS. */ +const MAX_FORM_BYTES = 16 * 1024 + +const REGISTER_RATE_LIMIT: TokenBucketConfig = { + maxTokens: 20, + refillRate: 10, + refillIntervalMs: 60_000, +} + +function redirectToOnboarding(error?: AwsMarketplaceOnboardingError): NextResponse { + const url = new URL(AWS_MARKETPLACE_ONBOARDING_PATH, getBaseUrl()) + if (error) url.searchParams.set('error', error) + return NextResponse.redirect(url, 303) +} + +async function readRegistrationToken(request: NextRequest): Promise { + const mediaType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() + if (mediaType !== 'application/x-www-form-urlencoded') return null + let form: FormData + try { + form = await readFormDataWithLimit(request, { + maxBytes: MAX_FORM_BYTES, + label: 'AWS Marketplace registration', + }) + } catch { + return null + } + const parsed = awsMarketplaceRegistrationFormSchema.safeParse({ + 'x-amzn-marketplace-token': form.get('x-amzn-marketplace-token'), + }) + return parsed.success ? parsed.data['x-amzn-marketplace-token'] : null +} + +/** + * The listing's fulfillment URL. AWS sends the buyer's browser here with a cross-site form POST, + * so the request carries no Sim session and is authenticated only by the registration token, + * which ResolveCustomer verifies. The response is always a 303 to the onboarding page, carrying + * the claim cookie when the license is active. The token is never logged. + */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const productCode = getAwsMarketplaceProductCode() + if (!productCode) return NextResponse.json({ error: 'Not found' }, { status: 404 }) + + const rateLimited = await enforceIpRateLimit( + 'aws-marketplace-register', + request, + REGISTER_RATE_LIMIT + ) + if (rateLimited) return rateLimited + + const token = await readRegistrationToken(request) + if (!token) { + logger.warn('AWS Marketplace registration arrived without a usable token') + return redirectToOnboarding('invalid') + } + + const outcome = await registerAwsMarketplaceBuyer(productCode, token) + if (outcome.kind !== 'claimed') return redirectToOnboarding(outcome.kind) + + const response = redirectToOnboarding() + response.headers.append('set-cookie', serializeClaimCookie(outcome.claimToken)) + response.headers.set('Cache-Control', 'no-store') + return response +}) diff --git a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx new file mode 100644 index 00000000000..56552d274cc --- /dev/null +++ b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx @@ -0,0 +1,268 @@ +'use client' + +import { useState } from 'react' +import { AuthField, AuthFormMessage, AuthSelect, AuthTag } from '@sim/auth-ui' +import { useRouter } from 'next/navigation' +import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' +import { client, useSession } from '@/lib/auth/client' +import { + AWS_MARKETPLACE_ONBOARDING_PATH, + type AwsMarketplaceOnboardingError, +} from '@/lib/billing/aws-marketplace/onboarding' +import { APP_ENTRY_PATH } from '@/lib/navigation/paths' +import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect' +import { InviteLayout, InviteStatusCard } from '@/app/invite/components' +import { + useAwsMarketplaceClaim, + useLinkAwsMarketplaceLicense, +} from '@/hooks/queries/aws-marketplace' + +const RESTART_FROM_AWS = + 'Open your Sim subscription in AWS Marketplace and choose Set up your account.' + +const REGISTRATION_ERROR_COPY: Record< + AwsMarketplaceOnboardingError, + { title: string; description: string } +> = { + 'no-subscription': { + title: 'No active subscription found', + description: + 'AWS Marketplace has no active Sim subscription for this AWS account. Subscribe to Sim in AWS Marketplace, then choose Set up your account to return here.', + }, + invalid: { + title: 'This setup link did not work', + description: RESTART_FROM_AWS, + }, + unavailable: { + title: 'AWS Marketplace is not responding', + description: `We could not confirm your subscription with AWS. Try again in a few minutes: ${RESTART_FROM_AWS}`, + }, +} + +function authLink(path: '/login' | '/signup'): string { + return buildAuthCrossLink(path, { + callbackUrl: AWS_MARKETPLACE_ONBOARDING_PATH, + isInviteFlow: true, + }) +} + +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString(undefined, { dateStyle: 'medium' }) +} + +interface AwsMarketplaceOnboardingProps { + registrationError: AwsMarketplaceOnboardingError | null + registrationDisabled: boolean +} + +export function AwsMarketplaceOnboarding({ + registrationError, + registrationDisabled, +}: AwsMarketplaceOnboardingProps) { + const router = useRouter() + const { data: session, isPending: sessionPending } = useSession() + const viewerId = session?.user?.id ?? null + const claimQuery = useAwsMarketplaceClaim(registrationError ? null : viewerId) + const linkLicense = useLinkAwsMarketplaceLicense() + const returnHome = { label: 'Return to Home', onClick: () => router.push('/') } + + if (registrationError) { + const copy = REGISTRATION_ERROR_COPY[registrationError] + return ( + + + + ) + } + + if (sessionPending) { + return ( + + + + ) + } + + if (!session?.user) { + return ( + + router.push(authLink('/signup')) }]), + { + label: 'I already have an account', + onClick: () => router.push(authLink('/login')), + }, + ]} + /> + + ) + } + + if (linkLicense.isSuccess) { + return ( + + router.push(APP_ENTRY_PATH) }]} + /> + + ) + } + + if (claimQuery.isPending) { + return ( + + + + ) + } + + if (claimQuery.isError) { + return ( + + claimQuery.refetch() }, returnHome]} + /> + + ) + } + + const claim = claimQuery.data + if (claim.status !== 'ready') { + return ( + + + + ) + } + + if (claim.organizations.length === 0) { + return ( + + { + await client.signOut() + router.push(authLink('/login')) + }, + }, + returnHome, + ]} + /> + + ) + } + + return ( + + linkLicense.mutate({ licenseId: claim.license.id, organizationId }) + } + onCancel={returnHome.onClick} + /> + ) +} + +interface LinkLicenseCardProps { + claim: Extract + linking: boolean + error: string | null + onLink: (organizationId: string) => void + onCancel: () => void +} + +function LinkLicenseCard({ claim, linking, error, onLink, onCancel }: LinkLicenseCardProps) { + const { license, organizations } = claim + const [selectedOrganizationId, setSelectedOrganizationId] = useState() + const linkedOrganization = organizations.find((org) => org.id === license.organizationId) + const organizationId = + selectedOrganizationId ?? + linkedOrganization?.id ?? + (organizations.length === 1 ? organizations[0]?.id : undefined) + + const rows = [ + { label: 'Plan', value: license.dimension ?? 'Not specified' }, + { label: 'Quantity', value: license.quantity?.toLocaleString() ?? 'Not specified' }, + { + label: 'Expires', + value: license.expiresAt ? formatDate(license.expiresAt) : 'No expiry', + }, + ] + + return ( + + +
+ {rows.map((row) => ( +
+
{row.label}
+
{row.value}
+
+ ))} +
+
Status
+
+ {license.status === 'active' ? 'Active' : 'Inactive'} +
+
+
+ + ({ label: org.name, value: org.id }))} + value={organizationId} + onChange={setSelectedOrganizationId} + placeholder='Select an organization' + fullWidth + /> + + {error && {error}} + + } + actions={[ + { + label: 'Link subscription', + onClick: () => organizationId && onLink(organizationId), + disabled: !organizationId || linking, + loading: linking, + }, + { label: 'Not now', onClick: onCancel }, + ]} + /> +
+ ) +} diff --git a/apps/sim/app/marketplace/aws/page.tsx b/apps/sim/app/marketplace/aws/page.tsx new file mode 100644 index 00000000000..2ad232d3ba5 --- /dev/null +++ b/apps/sim/app/marketplace/aws/page.tsx @@ -0,0 +1,30 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' +import { AWS_MARKETPLACE_ONBOARDING_ERRORS } from '@/lib/billing/aws-marketplace/onboarding' +import { isRegistrationDisabled } from '@/lib/core/config/env-flags' +import { AwsMarketplaceOnboarding } from '@/app/marketplace/aws/aws-marketplace-onboarding' + +export const metadata: Metadata = { + title: 'AWS Marketplace', + robots: { index: false }, +} + +export const dynamic = 'force-dynamic' + +interface AwsMarketplacePageProps { + searchParams: Promise<{ error?: string | string[] }> +} + +/** Where AWS Marketplace buyers land after registration, to link their subscription. */ +export default async function AwsMarketplacePage({ searchParams }: AwsMarketplacePageProps) { + if (!getAwsMarketplaceProductCode()) notFound() + const { error } = await searchParams + const registrationError = AWS_MARKETPLACE_ONBOARDING_ERRORS.find((code) => code === error) ?? null + return ( + + ) +} diff --git a/apps/sim/hooks/queries/aws-marketplace.ts b/apps/sim/hooks/queries/aws-marketplace.ts new file mode 100644 index 00000000000..554b9e4480c --- /dev/null +++ b/apps/sim/hooks/queries/aws-marketplace.ts @@ -0,0 +1,37 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { requestJson } from '@/lib/api/client/request' +import type { ContractBodyInput } from '@/lib/api/contracts' +import { + getAwsMarketplaceClaimContract, + linkAwsMarketplaceLicenseContract, +} from '@/lib/api/contracts/aws-marketplace' + +const AWS_MARKETPLACE_CLAIM_STALE_TIME = 30 * 1000 + +const awsMarketplaceKeys = { + all: ['aws-marketplace'] as const, + claims: () => [...awsMarketplaceKeys.all, 'claim'] as const, + /** The claim rides in an httpOnly cookie; the viewer scopes which organizations it lists. */ + claim: (viewerId: string) => [...awsMarketplaceKeys.claims(), viewerId] as const, +} + +export function useAwsMarketplaceClaim(viewerId: string | null) { + return useQuery({ + queryKey: awsMarketplaceKeys.claim(viewerId ?? ''), + queryFn: ({ signal }) => requestJson(getAwsMarketplaceClaimContract, { signal }), + enabled: Boolean(viewerId), + staleTime: AWS_MARKETPLACE_CLAIM_STALE_TIME, + retry: false, + }) +} + +export function useLinkAwsMarketplaceLicense() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (body: ContractBodyInput) => + requestJson(linkAwsMarketplaceLicenseContract, { body }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: awsMarketplaceKeys.claims() }) + }, + }) +} diff --git a/apps/sim/lib/api/contracts/aws-marketplace.ts b/apps/sim/lib/api/contracts/aws-marketplace.ts new file mode 100644 index 00000000000..09b6dde01d1 --- /dev/null +++ b/apps/sim/lib/api/contracts/aws-marketplace.ts @@ -0,0 +1,60 @@ +import { z } from 'zod' +import { defineRouteContract } from '@/lib/api/contracts' +import { organizationIdSchema } from '@/lib/api/contracts/primitives' + +/** + * The form AWS Marketplace posts to the fulfillment URL. It is not a route contract: the buyer's + * browser submits it as `application/x-www-form-urlencoded`, and the route answers with a redirect. + */ +export const awsMarketplaceRegistrationFormSchema = z.object({ + 'x-amzn-marketplace-token': z.string().min(1, 'Registration token is required').max(4096), +}) + +const awsMarketplaceLicenseSummarySchema = z.object({ + id: z.string(), + status: z.enum(['active', 'inactive']), + dimension: z.string().nullable(), + quantity: z.number().int().nullable(), + expiresAt: z.iso.datetime().nullable(), + /** The organization this license is already linked to, if any. */ + organizationId: z.string().nullable(), +}) + +const awsMarketplaceClaimOrganizationSchema = z.object({ + id: z.string(), + name: z.string(), + role: z.enum(['owner', 'admin']), +}) + +const awsMarketplaceClaimSchema = z.discriminatedUnion('status', [ + /** This browser holds no claim, or one already used. */ + z.object({ status: z.literal('missing') }), + z.object({ status: z.literal('expired') }), + z.object({ + status: z.literal('ready'), + license: awsMarketplaceLicenseSummarySchema, + /** Organizations the viewer owns or administers, in name order. */ + organizations: z.array(awsMarketplaceClaimOrganizationSchema), + }), +]) +export type AwsMarketplaceClaim = z.output + +/** The claim this browser holds, read from its httpOnly claim cookie. */ +export const getAwsMarketplaceClaimContract = defineRouteContract({ + method: 'GET', + path: '/api/marketplace/aws/claim', + response: { mode: 'json', schema: awsMarketplaceClaimSchema }, +}) + +export const linkAwsMarketplaceLicenseContract = defineRouteContract({ + method: 'POST', + path: '/api/marketplace/aws/claim/link', + body: z.object({ + licenseId: z.string().min(1, 'License ID is required').max(255), + organizationId: organizationIdSchema, + }), + response: { + mode: 'json', + schema: z.object({ success: z.literal(true), organizationId: z.string() }), + }, +}) diff --git a/apps/sim/lib/billing/aws-marketplace/application/operations.ts b/apps/sim/lib/billing/aws-marketplace/application/operations.ts new file mode 100644 index 00000000000..854a93d4021 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/application/operations.ts @@ -0,0 +1,22 @@ +import { defineOperation } from '@/lib/core/application/operation' +import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' + +/** + * Only a signed-in person in the browser that holds the claim cookie takes part: AWS hands the + * buyer a claim, never an API client or a delegated assistant. + */ +export const awsMarketplaceOperations = { + // permission-group-exempt: reads the caller's own pending Marketplace claim and the organizations they administer. + readClaim: defineOperation({ + id: 'aws_marketplace.claim.read', + principalKinds: ['session'], + capability: 'none', + }), + // permission-group-exempt: linking a purchased license is organization administration, not a governed capability. + linkLicense: defineOrganizationOperation({ + id: 'organization.aws_marketplace_license.link', + minimumRole: 'admin', + principalKinds: ['session'], + capability: 'none', + }), +} as const diff --git a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts new file mode 100644 index 00000000000..f7a9d70fdef --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts @@ -0,0 +1,258 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { db } from '@sim/db' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' +import { awsMarketplaceOperations } from '@/lib/billing/aws-marketplace/application/operations' +import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' +import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' +import { verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' +import { + type AwsMarketplaceLicenseRecord, + consumeLicenseClaim, + findLicenseByClaimHash, + getLinkParties, + listAdministeredOrganizations, + lockLicense, + recordLicenseVerification, +} from '@/lib/billing/aws-marketplace/repository' +import { + defineAuthorizedOrganizationUseCase, + type OrganizationUseCaseContext, +} from '@/lib/core/application/authorized-organization-use-case' +import { assertOperationPrincipal, type OperationUseCase } from '@/lib/core/application/operation' +import { requireOrganizationSubjectMembership } from '@/lib/core/application/organization-authorization' +import { env } from '@/lib/core/config/env' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { getEmailDomain } from '@/lib/core/utils/urls' +import { sendEmail } from '@/lib/messaging/email/mailer' +import { getFromEmailAddress } from '@/lib/messaging/email/utils' + +const logger = createLogger('AwsMarketplaceLicenses') + +/** The same answer as an unknown route, so a deployment that does not sell on AWS shows nothing. */ +function requireProductCode(): string { + const productCode = getAwsMarketplaceProductCode() + if (!productCode) throw new OrchestrationError('not_found', 'Not found') + return productCode +} + +function isClaimOpen(license: AwsMarketplaceLicenseRecord, now: Date): boolean { + return Boolean(license.claimExpiresAt && license.claimExpiresAt.getTime() > now.getTime()) +} + +const CLAIM_NOT_FOUND = + 'This AWS Marketplace setup link is no longer valid. Open your subscription in AWS Marketplace and choose Set up your account again.' + +/** Refuses unless this browser's claim still names this license and has not expired. */ +function requireOpenClaim( + license: AwsMarketplaceLicenseRecord | null, + licenseId: string, + claimHash: string | null +): AwsMarketplaceLicenseRecord { + if (!license || !claimHash || license.id !== licenseId || license.claimTokenHash !== claimHash) { + throw new OrchestrationError('forbidden', CLAIM_NOT_FOUND) + } + if (!isClaimOpen(license, new Date())) { + throw new OrchestrationError( + 'forbidden', + 'This AWS Marketplace setup link has expired. Open your subscription in AWS Marketplace and choose Set up your account again.' + ) + } + return license +} + +function requireLinkableTo(license: AwsMarketplaceLicenseRecord, organizationId: string): void { + if (license.organizationId && license.organizationId !== organizationId) { + throw new OrchestrationError( + 'conflict', + 'This AWS Marketplace subscription is already linked to another organization.' + ) + } +} + +/** The pending claim this browser holds, and where the signed-in person could link it. */ +export const readAwsMarketplaceClaim: OperationUseCase< + typeof awsMarketplaceOperations.readClaim, + { claimToken: string | null }, + AwsMarketplaceClaim +> = { + operation: awsMarketplaceOperations.readClaim, + async execute({ principal, input }) { + assertOperationPrincipal(principal, awsMarketplaceOperations.readClaim) + requireProductCode() + if (!input.claimToken) return { status: 'missing' } + const license = await findLicenseByClaimHash(hashClaimToken(input.claimToken)) + if (!license) return { status: 'missing' } + if (!isClaimOpen(license, new Date())) return { status: 'expired' } + return { + status: 'ready', + license: { + id: license.id, + status: license.status, + dimension: license.dimension, + quantity: license.quantity, + expiresAt: license.expiresAt?.toISOString() ?? null, + organizationId: license.organizationId, + }, + organizations: await listAdministeredOrganizations(principal.userId), + } + }, +} + +interface LinkLicenseInput { + organizationId: string + licenseId: string + /** The raw claim token from this browser's httpOnly cookie, or null when it holds none. */ + claimToken: string | null +} + +interface LinkedLicense { + license: AwsMarketplaceLicenseRecord + /** False when the license was already linked to this organization; nothing new to announce. */ + newlyLinked: boolean +} + +/** + * Tells the team a license now belongs to an organization, so they provision it the way an + * Enterprise deal is provisioned. No subscription is created or changed here. A failed send never + * undoes the committed link; the warning is the fallback trail. + */ +async function notifyTeamOfLinkedLicense( + license: AwsMarketplaceLicenseRecord, + organizationId: string, + userId: string +): Promise { + const parties = await getLinkParties(organizationId, userId) + const notice = { + licenseId: license.id, + organizationId, + dimension: license.dimension, + quantity: license.quantity, + expiresAt: license.expiresAt?.toISOString() ?? null, + } + const text = `AWS Marketplace license linked +Linked: ${new Date().toISOString()} +Organization: ${parties.organizationName ?? 'Unknown'} (${organizationId}) +Linked by: ${parties.userName ?? 'Unknown'} <${parties.userEmail ?? 'unknown'}> +License: ${license.licenseArn} +AWS account: ${license.customerAwsAccountId ?? 'Not provided'} +Customer identifier: ${license.customerIdentifier} +Dimension: ${license.dimension ?? 'Not provided'} +Quantity: ${license.quantity ?? 'Not provided'} +Expires: ${notice.expiresAt ?? 'No expiry'} + +Provision this organization the way Enterprise deals are provisioned. No subscription was changed. +` + try { + const result = await sendEmail({ + to: [`enterprise@${env.EMAIL_DOMAIN || getEmailDomain()}`], + subject: `[AWS MARKETPLACE] ${parties.organizationName ?? organizationId}`, + text, + from: getFromEmailAddress(), + ...(parties.userEmail && { replyTo: parties.userEmail }), + emailType: 'transactional', + }) + if (result.success) { + logger.info('Notified the team of a linked AWS Marketplace license', notice) + return + } + logger.warn('Could not notify the team of a linked AWS Marketplace license', { + ...notice, + message: result.message, + }) + } catch (error) { + logger.warn('Could not notify the team of a linked AWS Marketplace license', { + ...notice, + message: getErrorMessage(error, 'Unknown error'), + }) + } +} + +/** + * Links the license this browser's claim names to an organization the caller administers. The + * claim is checked before and again under the row lock, so two tabs racing one claim link it at + * most once, and the Entitlement Service is asked again: a license links only while AWS says it + * is current. A license belongs to at most one organization. + */ +export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ + operation: awsMarketplaceOperations.linkLicense, + authorizeResource: () => { + requireProductCode() + }, + async execute({ + input, + context, + }: OrganizationUseCaseContext): Promise { + const productCode = requireProductCode() + const claimHash = input.claimToken ? hashClaimToken(input.claimToken) : null + const claimed = requireOpenClaim( + claimHash ? await findLicenseByClaimHash(claimHash) : null, + input.licenseId, + claimHash + ) + requireLinkableTo(claimed, input.organizationId) + + const verifiedAt = new Date() + const summary = await verifyLicense(productCode, claimed.licenseArn) + if (!summary.active) { + await recordLicenseVerification({ + licenseArn: claimed.licenseArn, + productCode, + summary, + verifiedAt, + }) + throw new OrchestrationError( + 'conflict', + 'AWS Marketplace reports no active subscription for this license.' + ) + } + + return db.transaction(async (tx) => { + const locked = requireOpenClaim( + await lockLicense(tx, input.licenseId), + input.licenseId, + claimHash + ) + requireLinkableTo(locked, input.organizationId) + /** Holds the admin's membership until commit, so a concurrent demotion waits for the link. */ + await requireOrganizationSubjectMembership( + context.userId, + input.organizationId, + 'admin', + 'none', + undefined, + { executor: tx, forShare: true } + ) + const newlyLinked = locked.organizationId !== input.organizationId + const license = await consumeLicenseClaim(tx, { + licenseId: input.licenseId, + link: newlyLinked ? { organizationId: input.organizationId, userId: context.userId } : null, + summary, + verifiedAt, + }) + return { license, newlyLinked } + }) + }, + projectAudit: ({ result }) => + result.newlyLinked + ? { + workspaceId: null, + action: AuditAction.AWS_MARKETPLACE_LICENSE_LINKED, + resourceType: AuditResourceType.AWS_MARKETPLACE_LICENSE, + resourceId: result.license.id, + resourceName: result.license.dimension ?? 'AWS Marketplace license', + description: 'Linked an AWS Marketplace subscription to the organization', + metadata: { + dimension: result.license.dimension, + quantity: result.license.quantity, + expiresAt: result.license.expiresAt?.toISOString() ?? null, + }, + } + : [], + afterSuccess: async ({ result, input, context }) => { + if (result.newlyLinked) { + await notifyTeamOfLinkedLicense(result.license, input.organizationId, context.userId) + } + }, +}) diff --git a/apps/sim/lib/billing/aws-marketplace/claim.ts b/apps/sim/lib/billing/aws-marketplace/claim.ts new file mode 100644 index 00000000000..7b9760ceb31 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/claim.ts @@ -0,0 +1,27 @@ +import { sha256Hex } from '@sim/security/hash' +import { generateShortId } from '@sim/utils/id' + +/** Holds the buyer's one-time claim between the AWS redirect and linking an organization. */ +export const AWS_MARKETPLACE_CLAIM_COOKIE = 'sim_aws_marketplace_claim' + +export const AWS_MARKETPLACE_CLAIM_TTL_MS = 24 * 60 * 60 * 1000 + +/** ~190 random bits; only its hash is stored. */ +export function issueClaimToken(): { token: string; hash: string } { + const token = generateShortId(32) + return { token, hash: hashClaimToken(token) } +} + +export function hashClaimToken(token: string): string { + return sha256Hex(token) +} + +/** + * The `Set-Cookie` value carrying a claim token, or clearing it when `token` is null. Lax is the + * strictest policy that still reaches the onboarding page: AWS opens it with a cross-site form + * POST whose 303 lands as a top-level GET. + */ +export function serializeClaimCookie(token: string | null): string { + const maxAge = token === null ? 0 : Math.floor(AWS_MARKETPLACE_CLAIM_TTL_MS / 1000) + return `${AWS_MARKETPLACE_CLAIM_COOKIE}=${token ?? ''}; Path=/; Max-Age=${maxAge}; HttpOnly; Secure; SameSite=Lax` +} diff --git a/apps/sim/lib/billing/aws-marketplace/client.ts b/apps/sim/lib/billing/aws-marketplace/client.ts new file mode 100644 index 00000000000..277790b3ba7 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/client.ts @@ -0,0 +1,39 @@ +import { MarketplaceEntitlementServiceClient } from '@aws-sdk/client-marketplace-entitlement-service' +import { MarketplaceMeteringClient } from '@aws-sdk/client-marketplace-metering' +import { SQSClient } from '@aws-sdk/client-sqs' +import { getAwsCredentials } from '@/lib/core/config/aws' + +/** The Marketplace metering and entitlement endpoints for SaaS products live only here. */ +const MARKETPLACE_REGION = 'us-east-1' + +let meteringClient: MarketplaceMeteringClient | null = null +let entitlementClient: MarketplaceEntitlementServiceClient | null = null +let queueClient: SQSClient | null = null + +/** + * Credentials come from `getAwsCredentials()`: the SDK's default chain, which on ECS resolves the + * task role. Clients are built on first use, so a deployment with the feature off never reaches + * for credentials. + */ +export function getMeteringClient(): MarketplaceMeteringClient { + meteringClient ??= new MarketplaceMeteringClient({ + region: MARKETPLACE_REGION, + credentials: getAwsCredentials(), + }) + return meteringClient +} + +/** Single attempt per call: `fetchLicenseEntitlements` owns the retry loop and its pacing. */ +export function getEntitlementClient(): MarketplaceEntitlementServiceClient { + entitlementClient ??= new MarketplaceEntitlementServiceClient({ + region: MARKETPLACE_REGION, + credentials: getAwsCredentials(), + maxAttempts: 1, + }) + return entitlementClient +} + +export function getEventsQueueClient(): SQSClient { + queueClient ??= new SQSClient({ region: MARKETPLACE_REGION, credentials: getAwsCredentials() }) + return queueClient +} diff --git a/apps/sim/lib/billing/aws-marketplace/config.ts b/apps/sim/lib/billing/aws-marketplace/config.ts new file mode 100644 index 00000000000..778a029e5b0 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/config.ts @@ -0,0 +1,10 @@ +import { env } from '@/lib/core/config/env' + +/** + * The SaaS product code, or null when this deployment does not sell through AWS Marketplace: + * self-hosted installs and every environment but production. Null turns the registration, + * claim, and reconcile surfaces off. + */ +export function getAwsMarketplaceProductCode(): string | null { + return env.AWS_MARKETPLACE_PRODUCT_CODE ?? null +} diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts new file mode 100644 index 00000000000..d251061371f --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts @@ -0,0 +1,162 @@ +import type { Entitlement } from '@aws-sdk/client-marketplace-entitlement-service' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockSend } = vi.hoisted(() => ({ mockSend: vi.fn() })) + +vi.mock('@aws-sdk/client-marketplace-entitlement-service', () => ({ + MarketplaceEntitlementServiceClient: class { + send = mockSend + }, + GetEntitlementsCommand: class { + constructor(public input: { NextToken?: string }) {} + }, +})) + +import { + fetchLicenseEntitlements, + summarizeEntitlements, +} from '@/lib/billing/aws-marketplace/entitlements' + +const PRODUCT = 'product-placeholder' +const LICENSE = 'license-placeholder' +const NOW = new Date('2026-06-01T00:00:00.000Z') + +function entitlement(overrides: Partial = {}): Entitlement { + return { + ProductCode: PRODUCT, + Dimension: 'enterprise', + CustomerIdentifier: 'customer-placeholder', + Value: { IntegerValue: 25 }, + ...overrides, + } +} + +function awsError(name: string, status: number): Error { + return Object.assign(new Error(name), { name, $metadata: { httpStatusCode: status } }) +} + +describe('summarizeEntitlements', () => { + it('reads an entitlement that expired before now as inactive', () => { + const summary = summarizeEntitlements( + [entitlement({ ExpirationDate: new Date('2026-05-31T23:59:59.000Z') })], + PRODUCT, + NOW + ) + expect(summary).toMatchObject({ active: false, entitlementCount: 1, quantity: 25 }) + }) + + it('reads an entitlement with a future expiry, or none at all, as active', () => { + expect( + summarizeEntitlements( + [entitlement({ ExpirationDate: new Date('2027-06-01T00:00:00.000Z') })], + PRODUCT, + NOW + ).active + ).toBe(true) + expect(summarizeEntitlements([entitlement()], PRODUCT, NOW).active).toBe(true) + }) + + it('treats an entitlement expiring exactly now as lapsed', () => { + expect(summarizeEntitlements([entitlement({ ExpirationDate: NOW })], PRODUCT, NOW).active).toBe( + false + ) + }) + + it('ignores an entitlement for another product', () => { + const summary = summarizeEntitlements( + [entitlement({ ProductCode: 'other-product' })], + PRODUCT, + NOW + ) + expect(summary).toMatchObject({ active: false, entitlementCount: 0, dimension: null }) + }) + + it('describes the current entitlement when another one has lapsed', () => { + const summary = summarizeEntitlements( + [ + entitlement({ + Dimension: 'a-lapsed', + ExpirationDate: new Date('2026-01-01T00:00:00.000Z'), + Value: { IntegerValue: 5 }, + }), + entitlement({ + Dimension: 'b-current', + ExpirationDate: new Date('2027-01-01T00:00:00.000Z'), + Value: { IntegerValue: 50 }, + }), + ], + PRODUCT, + NOW + ) + expect(summary).toMatchObject({ active: true, dimension: 'b-current', quantity: 50 }) + }) + + it('reads quantity only from an integer value, never from another value kind', () => { + expect( + summarizeEntitlements([entitlement({ Value: { DoubleValue: 1200.5 } })], PRODUCT, NOW) + .quantity + ).toBeNull() + expect( + summarizeEntitlements([entitlement({ Value: { IntegerValue: 2.5 } })], PRODUCT, NOW).quantity + ).toBeNull() + }) +}) + +describe('fetchLicenseEntitlements', () => { + beforeEach(() => { + mockSend.mockReset() + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('follows NextToken through an empty page to the last one', async () => { + mockSend + .mockResolvedValueOnce({ Entitlements: [entitlement({ Dimension: 'one' })], NextToken: 't1' }) + .mockResolvedValueOnce({ Entitlements: [], NextToken: 't2' }) + .mockResolvedValueOnce({ Entitlements: [entitlement({ Dimension: 'two' })] }) + + const entitlements = await fetchLicenseEntitlements(PRODUCT, LICENSE) + + expect(entitlements.map((row) => row.Dimension)).toEqual(['one', 'two']) + expect(mockSend.mock.calls.map(([command]) => command.input.NextToken)).toEqual([ + undefined, + 't1', + 't2', + ]) + }) + + it('retries a throttled page and returns once it succeeds', async () => { + mockSend + .mockRejectedValueOnce(awsError('ThrottlingException', 400)) + .mockRejectedValueOnce(awsError('InternalServiceErrorException', 500)) + .mockResolvedValueOnce({ Entitlements: [entitlement()] }) + + const pending = fetchLicenseEntitlements(PRODUCT, LICENSE) + await vi.runAllTimersAsync() + + expect(await pending).toHaveLength(1) + expect(mockSend).toHaveBeenCalledTimes(3) + }) + + it('gives up after three throttled attempts', async () => { + mockSend.mockRejectedValue(awsError('ThrottlingException', 400)) + + const pending = fetchLicenseEntitlements(PRODUCT, LICENSE).catch((error: unknown) => error) + await vi.runAllTimersAsync() + + expect(await pending).toMatchObject({ name: 'ThrottlingException' }) + expect(mockSend).toHaveBeenCalledTimes(3) + }) + + it('does not retry a request the service rejected as invalid', async () => { + mockSend.mockRejectedValue(awsError('InvalidParameterException', 400)) + + await expect(fetchLicenseEntitlements(PRODUCT, LICENSE)).rejects.toMatchObject({ + name: 'InvalidParameterException', + }) + expect(mockSend).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.ts new file mode 100644 index 00000000000..73c2e5a2fa2 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.ts @@ -0,0 +1,131 @@ +import { + type Entitlement, + GetEntitlementsCommand, + type GetEntitlementsCommandOutput, +} from '@aws-sdk/client-marketplace-entitlement-service' +import { sleep } from '@sim/utils/helpers' +import { backoffWithJitter } from '@sim/utils/retry' +import { compareStrings } from '@sim/utils/string' +import { getEntitlementClient } from '@/lib/billing/aws-marketplace/client' + +const MAX_ATTEMPTS = 3 +const RETRY_BACKOFF = { baseMs: 200, maxMs: 2_000 } +/** A license carries one entitlement per purchased dimension; this bounds a runaway `NextToken`. */ +const MAX_PAGES = 20 + +/** What the last Entitlement Service read says about one license. */ +export interface EntitlementSummary { + /** At least one entitlement for our product has no expiry or expires in the future. */ + active: boolean + /** Entitlements AWS returned for our product, current or lapsed; zero once deprovisioned. */ + entitlementCount: number + dimension: string | null + quantity: number | null + expiresAt: Date | null + customerIdentifier: string | null + customerAwsAccountId: string | null +} + +function isCurrent(entitlement: Entitlement, now: Date): boolean { + return !entitlement.ExpirationDate || entitlement.ExpirationDate.getTime() > now.getTime() +} + +/** + * Reduces a license's entitlements to the one the row records. A current entitlement wins, the + * lowest dimension name first so repeated reads agree; with none current, the latest-expiring one + * still describes what lapsed. Quantity is read only from an integer value: a contract's dollar + * amount is never inferred from it. + */ +export function summarizeEntitlements( + entitlements: readonly Entitlement[], + productCode: string, + now: Date +): EntitlementSummary { + const ours = entitlements.filter((entitlement) => entitlement.ProductCode === productCode) + const current = ours + .filter((entitlement) => isCurrent(entitlement, now)) + .sort((left, right) => compareStrings(left.Dimension ?? '', right.Dimension ?? '')) + const lapsed = [...ours].sort( + (left, right) => (right.ExpirationDate?.getTime() ?? 0) - (left.ExpirationDate?.getTime() ?? 0) + ) + const chosen = current[0] ?? lapsed[0] + const integerValue = chosen?.Value?.IntegerValue + return { + active: current.length > 0, + entitlementCount: ours.length, + dimension: chosen?.Dimension ?? null, + quantity: + typeof integerValue === 'number' && Number.isInteger(integerValue) ? integerValue : null, + expiresAt: chosen?.ExpirationDate ?? null, + customerIdentifier: chosen?.CustomerIdentifier ?? null, + customerAwsAccountId: chosen?.CustomerAWSAccountId ?? null, + } +} + +interface AwsServiceErrorShape { + name?: string + $metadata?: { httpStatusCode?: number } +} + +/** Throttling, a server fault, or no HTTP response at all (network, timeout). */ +function isRetryable(error: unknown): boolean { + const shape = error as AwsServiceErrorShape + if (shape?.name === 'ThrottlingException' || shape?.name === 'InternalServiceErrorException') { + return true + } + const status = shape?.$metadata?.httpStatusCode + if (typeof status !== 'number') return true + return status === 429 || status >= 500 +} + +/** + * The Entitlement Service rejected the request itself (a malformed license ARN or product code). + * Asking again cannot succeed; every other failure, including a misconfigured IAM grant, may. + */ +export function isInvalidEntitlementRequest(error: unknown): boolean { + return (error as AwsServiceErrorShape)?.name === 'InvalidParameterException' +} + +async function getEntitlementsPage( + command: GetEntitlementsCommand +): Promise { + for (let attempt = 1; ; attempt++) { + try { + return await getEntitlementClient().send(command) + } catch (error) { + if (attempt >= MAX_ATTEMPTS || !isRetryable(error)) throw error + await sleep(backoffWithJitter(attempt, null, RETRY_BACKOFF)) + } + } +} + +/** Every entitlement AWS holds for one license of our product, across all pages. */ +export async function fetchLicenseEntitlements( + productCode: string, + licenseArn: string +): Promise { + const entitlements: Entitlement[] = [] + let nextToken: string | undefined + for (let page = 0; page < MAX_PAGES; page++) { + const result = await getEntitlementsPage( + new GetEntitlementsCommand({ + ProductCode: productCode, + Filter: { LICENSE_ARN: [licenseArn] }, + NextToken: nextToken, + }) + ) + entitlements.push(...(result.Entitlements ?? [])) + nextToken = result.NextToken + if (!nextToken) return entitlements + } + throw new Error(`GetEntitlements returned more than ${MAX_PAGES} pages for one license`) +} + +/** Reads and summarizes one license's entitlements as of now. */ +export async function verifyLicense( + productCode: string, + licenseArn: string +): Promise { + const entitlements = await fetchLicenseEntitlements(productCode, licenseArn) + return summarizeEntitlements(entitlements, productCode, new Date()) +} diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts new file mode 100644 index 00000000000..83fb41783d5 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -0,0 +1,476 @@ +import type { Entitlement } from '@aws-sdk/client-marketplace-entitlement-service' +import { db } from '@sim/db' +import { auditLog, awsMarketplaceLicense, member, organization, user } from '@sim/db/schema' +import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' +import { emailMailerMock, emailMailerMockFns } from '@sim/testing/mocks/email-mailer.mock' +import { requestUtilsMock, requestUtilsMockFns } from '@sim/testing/mocks/request.mock' +import { generateId } from '@sim/utils/id' +import { and, eq, inArray } from 'drizzle-orm' +import { NextRequest } from 'next/server' +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const PRODUCT = 'product-placeholder' + +const { meteringSend, entitlementSend, queueSend } = vi.hoisted(() => { + process.env.AWS_MARKETPLACE_PRODUCT_CODE = 'product-placeholder' + process.env.AWS_MARKETPLACE_EVENTS_QUEUE_URL = + 'https://queue.example.test/aws-marketplace-placeholder' + return { meteringSend: vi.fn(), entitlementSend: vi.fn(), queueSend: vi.fn() } +}) + +vi.mock('@aws-sdk/client-marketplace-metering', () => ({ + MarketplaceMeteringClient: class { + send = meteringSend + }, + ResolveCustomerCommand: class { + constructor(public input: { RegistrationToken: string }) {} + }, +})) +vi.mock('@aws-sdk/client-marketplace-entitlement-service', () => ({ + MarketplaceEntitlementServiceClient: class { + send = entitlementSend + }, + GetEntitlementsCommand: class { + constructor(public input: { Filter: { LICENSE_ARN: string[] } }) {} + }, +})) +vi.mock('@aws-sdk/client-sqs', () => ({ + SQSClient: class { + send = queueSend + }, + ReceiveMessageCommand: class { + readonly kind = 'receive' + constructor(public input: unknown) {} + }, + DeleteMessageCommand: class { + readonly kind = 'delete' + constructor(public input: { ReceiptHandle: string }) {} + }, +})) +vi.mock('@/lib/messaging/email/mailer', () => emailMailerMock) +vi.mock('@/lib/core/utils/request', () => requestUtilsMock) + +import { linkAwsMarketplaceLicense } from '@/lib/billing/aws-marketplace/application/use-cases' +import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' +import { reconcileAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/reconcile' +import { POST as register } from '@/app/api/marketplace/aws/register/route' + +/** What GetEntitlements answers for each license ARN; an Error is thrown instead. */ +const entitlementsByLicense = new Map() +const licenseArns: string[] = [] +const userIds: string[] = [] +const organizationIds: string[] = [] + +function uniqueArn(): string { + const arn = `license-${generateId()}` + licenseArns.push(arn) + return arn +} + +function currentEntitlement(overrides: Partial = {}): Entitlement { + return { + ProductCode: PRODUCT, + Dimension: 'enterprise', + CustomerIdentifier: 'customer-placeholder', + CustomerAWSAccountId: '000000000000', + Value: { IntegerValue: 25 }, + ExpirationDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), + ...overrides, + } +} + +function lapsedEntitlement(): Entitlement { + return currentEntitlement({ ExpirationDate: new Date(Date.now() - 60_000) }) +} + +function throttled(): Error { + return Object.assign(new Error('Rate exceeded'), { + name: 'ThrottlingException', + $metadata: { httpStatusCode: 400 }, + }) +} + +function registration(body: string, contentType = 'application/x-www-form-urlencoded') { + requestUtilsMockFns.mockGetClientIp.mockReturnValue(`198.51.100.${licenseArns.length + 1}`) + return register( + new NextRequest('http://localhost:3000/api/marketplace/aws/register', { + method: 'POST', + headers: { 'content-type': contentType }, + body, + }), + {} + ) +} + +function resolvesTo(licenseArn: string, productCode = PRODUCT) { + meteringSend.mockResolvedValueOnce({ + CustomerIdentifier: 'customer-placeholder', + CustomerAWSAccountId: '000000000000', + ProductCode: productCode, + LicenseArn: licenseArn, + }) +} + +async function readLicense(licenseArn: string) { + const [row] = await db + .select() + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.licenseArn, licenseArn)) + return row +} + +async function createUser(): Promise { + const id = generateId() + userIds.push(id) + await db.insert(user).values({ + id, + name: 'Marketplace fixture', + email: `${id}@example.test`, + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + }) + return id +} + +async function createOrganization(members: Array<{ userId: string; role: string }>) { + const id = generateId() + organizationIds.push(id) + await db.insert(organization).values({ + id, + name: 'Marketplace organization', + slug: id, + createdAt: new Date(), + }) + await db.insert(member).values( + members.map((entry) => ({ + id: generateId(), + organizationId: id, + ...entry, + createdAt: new Date(), + })) + ) + return id +} + +/** A registered, active license whose claim `token` this browser holds. */ +async function createClaimedLicense( + overrides: Partial = {} +) { + const licenseArn = uniqueArn() + const token = generateId() + const id = generateId() + await db.insert(awsMarketplaceLicense).values({ + id, + licenseArn, + productCode: PRODUCT, + customerIdentifier: 'customer-placeholder', + status: 'active', + dimension: 'enterprise', + quantity: 25, + lastVerifiedAt: new Date(), + claimTokenHash: hashClaimToken(token), + claimExpiresAt: new Date(Date.now() + 60 * 60 * 1000), + ...overrides, + }) + return { id, licenseArn, token } +} + +function rejection(promise: Promise): Promise { + return promise.then( + () => { + throw new Error('Expected the operation to be refused') + }, + (error: unknown) => error + ) +} + +beforeAll(() => { + entitlementSend.mockImplementation( + async (command: { input: { Filter: { LICENSE_ARN: string[] } } }) => { + const answer = entitlementsByLicense.get(command.input.Filter.LICENSE_ARN[0]) + if (answer instanceof Error) throw answer + return { Entitlements: answer ?? [] } + } + ) +}) + +beforeEach(() => { + meteringSend.mockReset() + queueSend.mockReset() + emailMailerMockFns.mockSendEmail.mockReset() + emailMailerMockFns.mockSendEmail.mockResolvedValue({ success: true, message: 'sent' }) +}) + +afterAll(async () => { + await db + .delete(awsMarketplaceLicense) + .where(inArray(awsMarketplaceLicense.licenseArn, licenseArns)) + if (organizationIds.length) { + await db.delete(organization).where(inArray(organization.id, organizationIds)) + } + if (userIds.length) { + await db.delete(auditLog).where(inArray(auditLog.actorId, userIds)) + await db.delete(user).where(inArray(user.id, userIds)) + } +}) + +describe('POST /api/marketplace/aws/register', () => { + it('rejects a form without a registration token before calling AWS', async () => { + const response = await registration('unrelated=1') + + expect(response.status).toBe(303) + expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( + 'invalid' + ) + expect(response.headers.get('set-cookie')).toBeNull() + expect(meteringSend).not.toHaveBeenCalled() + }) + + it('rejects a token that resolves to another product without recording it', async () => { + const licenseArn = uniqueArn() + resolvesTo(licenseArn, 'other-product') + + const response = await registration('x-amzn-marketplace-token=opaque%2Btoken') + + expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( + 'invalid' + ) + expect(response.headers.get('set-cookie')).toBeNull() + expect(await readLicense(licenseArn)).toBeUndefined() + expect(meteringSend.mock.calls[0][0].input.RegistrationToken).toBe('opaque+token') + }) + + it('records an unentitled license as inactive and issues no claim', async () => { + const licenseArn = uniqueArn() + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, [lapsedEntitlement()]) + + const response = await registration('x-amzn-marketplace-token=lapsed-token') + + expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( + 'no-subscription' + ) + expect(response.headers.get('set-cookie')).toBeNull() + expect(await readLicense(licenseArn)).toMatchObject({ + status: 'inactive', + claimTokenHash: null, + claimExpiresAt: null, + }) + }) + + it('issues an httpOnly claim for an active license and stores only its hash', async () => { + const licenseArn = uniqueArn() + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + + const response = await registration('x-amzn-marketplace-token=active-token') + + const location = new URL(response.headers.get('location') ?? '') + expect(response.status).toBe(303) + expect(location.pathname).toBe('/marketplace/aws') + expect(location.searchParams.get('error')).toBeNull() + const cookie = response.headers.get('set-cookie') ?? '' + expect(cookie).toMatch(/HttpOnly/) + expect(cookie).toMatch(/Secure/) + expect(cookie).toMatch(/SameSite=Lax/) + const token = /sim_aws_marketplace_claim=([^;]+)/.exec(cookie)?.[1] ?? '' + expect(token.length).toBeGreaterThan(20) + const row = await readLicense(licenseArn) + expect(row).toMatchObject({ + status: 'active', + quantity: 25, + claimTokenHash: hashClaimToken(token), + }) + expect(row?.claimTokenHash).not.toBe(token) + expect(row?.claimExpiresAt?.getTime()).toBeGreaterThan(Date.now() + 23 * 60 * 60 * 1000) + }) +}) + +describe('linking a claimed license', () => { + let ownerId: string + let memberId: string + let organizationId: string + + beforeAll(async () => { + ownerId = await createUser() + memberId = await createUser() + organizationId = await createOrganization([ + { userId: ownerId, role: 'owner' }, + { userId: memberId, role: 'member' }, + ]) + }) + + function link(userId: string, input: { licenseId: string; claimToken: string | null }) { + return linkAwsMarketplaceLicense.execute({ + principal: createSessionPrincipal({ userId }), + input: { organizationId, ...input }, + }) + } + + it('refuses a browser that holds no claim', async () => { + const license = await createClaimedLicense() + + const error = await rejection(link(ownerId, { licenseId: license.id, claimToken: null })) + + expect(error).toMatchObject({ code: 'forbidden' }) + expect((await readLicense(license.licenseArn))?.organizationId).toBeNull() + }) + + it('refuses an expired claim', async () => { + const license = await createClaimedLicense({ claimExpiresAt: new Date(Date.now() - 1000) }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + link(ownerId, { licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'forbidden', message: expect.stringMatching(/expired/) }) + expect((await readLicense(license.licenseArn))?.organizationId).toBeNull() + }) + + it('refuses an organization member who is not an owner or admin', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + link(memberId, { licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'forbidden', detailCode: 'ORGANIZATION_ADMIN_REQUIRED' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId: null, + claimTokenHash: hashClaimToken(license.token), + }) + }) + + it('refuses to move a license already linked to another organization', async () => { + const otherOwnerId = await createUser() + const otherOrganizationId = await createOrganization([{ userId: otherOwnerId, role: 'owner' }]) + const license = await createClaimedLicense({ organizationId: otherOrganizationId }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + link(ownerId, { licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'conflict' }) + expect((await readLicense(license.licenseArn))?.organizationId).toBe(otherOrganizationId) + }) + + it('refuses when AWS no longer reports a current entitlement', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [lapsedEntitlement()]) + + const error = await rejection( + link(ownerId, { licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'conflict' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId: null, + status: 'inactive', + }) + expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + }) + + it('links once, consumes the claim, audits the link, and notifies the team', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [ + currentEntitlement({ Value: { IntegerValue: 40 } }), + ]) + + await link(ownerId, { licenseId: license.id, claimToken: license.token }) + + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId, + linkedByUserId: ownerId, + quantity: 40, + claimTokenHash: null, + claimExpiresAt: null, + }) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) + expect(emailMailerMockFns.mockSendEmail.mock.calls[0][0].to).toEqual([ + expect.stringMatching(/^enterprise@/), + ]) + await vi.waitFor(async () => { + const entries = await db + .select({ action: auditLog.action }) + .from(auditLog) + .where(and(eq(auditLog.resourceId, license.id), eq(auditLog.actorId, ownerId))) + expect(entries).toEqual([{ action: 'aws_marketplace_license.linked' }]) + }) + + const replay = await rejection( + link(ownerId, { licenseId: license.id, claimToken: license.token }) + ) + expect(replay).toMatchObject({ code: 'forbidden' }) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) + }) +}) + +describe('reconcileAwsMarketplaceLicenses', () => { + it('discards poison events, keeps a throttled one for redelivery, and marks lapsed licenses inactive', async () => { + const eventArn = uniqueArn() + const throttledArn = uniqueArn() + const staleArn = uniqueArn() + await db.insert(awsMarketplaceLicense).values([ + { + id: generateId(), + licenseArn: eventArn, + productCode: PRODUCT, + customerIdentifier: 'customer-placeholder', + status: 'active', + lastVerifiedAt: new Date(Date.now() - 60_000), + }, + { + id: generateId(), + licenseArn: staleArn, + productCode: PRODUCT, + customerIdentifier: 'customer-placeholder', + status: 'active', + dimension: 'enterprise', + lastVerifiedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), + }, + ]) + entitlementsByLicense.set(eventArn, [lapsedEntitlement()]) + entitlementsByLicense.set(throttledArn, throttled()) + entitlementsByLicense.set(staleArn, []) + + const licenseEvent = (arn: string, productCode = PRODUCT) => + JSON.stringify({ + source: 'aws.agreement-marketplace', + 'detail-type': 'License Deprovisioned - Manufacturer', + detail: { license: { arn }, product: { code: productCode } }, + }) + const messages = [ + { MessageId: 'malformed', ReceiptHandle: 'r-malformed', Body: '{not json' }, + { MessageId: 'foreign', ReceiptHandle: 'r-foreign', Body: licenseEvent(eventArn, 'other') }, + { MessageId: 'throttled', ReceiptHandle: 'r-throttled', Body: licenseEvent(throttledArn) }, + { MessageId: 'lapsed', ReceiptHandle: 'r-lapsed', Body: licenseEvent(eventArn) }, + ] + let delivered = false + queueSend.mockImplementation(async (command: { kind: string }) => { + if (command.kind === 'receive') { + if (delivered) return { Messages: [] } + delivered = true + return { Messages: messages } + } + return {} + }) + + const summary = await reconcileAwsMarketplaceLicenses() + + const deleted = queueSend.mock.calls + .map(([command]) => command) + .filter((command) => command.kind === 'delete') + .map((command) => command.input.ReceiptHandle) + expect(deleted.sort()).toEqual(['r-foreign', 'r-lapsed', 'r-malformed']) + expect(summary).toMatchObject({ received: 4, applied: 1, discarded: 2, retained: 1 }) + expect((await readLicense(eventArn))?.status).toBe('inactive') + expect(await readLicense(throttledArn)).toBeUndefined() + const stale = await readLicense(staleArn) + expect(stale).toMatchObject({ status: 'inactive', dimension: 'enterprise' }) + expect(stale?.lastVerifiedAt.getTime()).toBeGreaterThan(Date.now() - 60_000) + }) +}) diff --git a/apps/sim/lib/billing/aws-marketplace/onboarding.ts b/apps/sim/lib/billing/aws-marketplace/onboarding.ts new file mode 100644 index 00000000000..964a3db7cf0 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/onboarding.ts @@ -0,0 +1,10 @@ +/** The onboarding page the registration redirect lands on. Client-safe. */ +export const AWS_MARKETPLACE_ONBOARDING_PATH = '/marketplace/aws' + +/** Why a registration ended without a claim, carried to the onboarding page as `?error=`. */ +export const AWS_MARKETPLACE_ONBOARDING_ERRORS = [ + 'invalid', + 'no-subscription', + 'unavailable', +] as const +export type AwsMarketplaceOnboardingError = (typeof AWS_MARKETPLACE_ONBOARDING_ERRORS)[number] diff --git a/apps/sim/lib/billing/aws-marketplace/reconcile.ts b/apps/sim/lib/billing/aws-marketplace/reconcile.ts new file mode 100644 index 00000000000..2ea1c7224a0 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/reconcile.ts @@ -0,0 +1,204 @@ +import { DeleteMessageCommand, type Message, ReceiveMessageCommand } from '@aws-sdk/client-sqs' +import { createLogger } from '@sim/logger' +import { toStringOrNull } from '@sim/utils/coerce' +import { getErrorMessage } from '@sim/utils/errors' +import { toRecord } from '@sim/utils/object' +import { getEventsQueueClient } from '@/lib/billing/aws-marketplace/client' +import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' +import { + isInvalidEntitlementRequest, + verifyLicense, +} from '@/lib/billing/aws-marketplace/entitlements' +import { + listLicensesVerifiedBefore, + recordLicenseVerification, +} from '@/lib/billing/aws-marketplace/repository' +import { env } from '@/lib/core/config/env' + +const logger = createLogger('AwsMarketplaceReconcile') + +const EVENT_SOURCE = 'aws.agreement-marketplace' +const LICENSE_EVENT_TYPES = new Set([ + 'License Updated - Manufacturer', + 'License Deprovisioned - Manufacturer', +]) + +/** SQS caps one receive at 10 messages; a run drains at most this many batches. */ +const RECEIVE_BATCH_SIZE = 10 +const MAX_RECEIVE_BATCHES = 10 +/** Long enough for a batch of entitlement reads with retries; unhandled messages reappear after it. */ +const VISIBILITY_TIMEOUT_SECONDS = 120 +const RECEIVE_WAIT_SECONDS = 2 + +const REVERIFY_AFTER_MS = 24 * 60 * 60 * 1000 +const REVERIFY_BATCH_SIZE = 50 + +interface AwsMarketplaceReconcileSummary { + received: number + applied: number + discarded: number + retained: number + reverified: number + reverifyFailed: number +} + +type LicenseEvent = { licenseArn: string; productCode: string } | null + +/** The license an agreement event names, or null for a body that is not one. */ +function parseLicenseEvent(body: string | undefined): LicenseEvent { + if (!body) return null + let event: Record + try { + event = toRecord(JSON.parse(body)) + } catch { + return null + } + const detailType = toStringOrNull(event['detail-type']) + if (event.source !== EVENT_SOURCE || !detailType || !LICENSE_EVENT_TYPES.has(detailType)) { + return null + } + const detail = toRecord(event.detail) + const licenseArn = toStringOrNull(toRecord(detail.license).arn) + const productCode = toStringOrNull(toRecord(detail.product).code) + return licenseArn && productCode ? { licenseArn, productCode } : null +} + +type MessageOutcome = 'applied' | 'discarded' | 'retained' + +/** + * Re-reads the license an event names rather than trusting the event's own fields. A message that + * can never apply is discarded; one that failed for a reason that may pass is retained, so SQS + * redelivers it and the dead-letter queue catches one that never succeeds. + */ +async function handleMessage(productCode: string, message: Message): Promise { + const event = parseLicenseEvent(message.Body) + if (!event) { + logger.warn('Discarding an AWS Marketplace event that is not a license event', { + messageId: message.MessageId, + }) + return 'discarded' + } + if (event.productCode !== productCode) { + logger.warn('Discarding an AWS Marketplace event for another product', { + messageId: message.MessageId, + }) + return 'discarded' + } + const verifiedAt = new Date() + try { + const summary = await verifyLicense(productCode, event.licenseArn) + await recordLicenseVerification({ + licenseArn: event.licenseArn, + productCode, + summary, + verifiedAt, + }) + return 'applied' + } catch (error) { + if (isInvalidEntitlementRequest(error)) { + logger.warn('Discarding an AWS Marketplace event the Entitlement Service rejected', { + messageId: message.MessageId, + message: getErrorMessage(error, 'Unknown error'), + }) + return 'discarded' + } + logger.warn('Leaving an AWS Marketplace event for redelivery', { + messageId: message.MessageId, + message: getErrorMessage(error, 'Unknown error'), + }) + return 'retained' + } +} + +async function drainEvents( + productCode: string, + queueUrl: string, + summary: AwsMarketplaceReconcileSummary +): Promise { + const client = getEventsQueueClient() + for (let batch = 0; batch < MAX_RECEIVE_BATCHES; batch++) { + let messages: Message[] + try { + const result = await client.send( + new ReceiveMessageCommand({ + QueueUrl: queueUrl, + MaxNumberOfMessages: RECEIVE_BATCH_SIZE, + VisibilityTimeout: VISIBILITY_TIMEOUT_SECONDS, + WaitTimeSeconds: RECEIVE_WAIT_SECONDS, + }) + ) + messages = result.Messages ?? [] + } catch (error) { + logger.error('Could not receive AWS Marketplace events', { + message: getErrorMessage(error, 'Unknown error'), + }) + return + } + if (messages.length === 0) return + + for (const message of messages) { + summary.received++ + const outcome = await handleMessage(productCode, message) + summary[outcome]++ + if (outcome === 'retained' || !message.ReceiptHandle) continue + try { + await client.send( + new DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: message.ReceiptHandle }) + ) + } catch (error) { + logger.warn('Could not delete a handled AWS Marketplace event; it will be redelivered', { + messageId: message.MessageId, + message: getErrorMessage(error, 'Unknown error'), + }) + } + } + } +} + +async function reverifyStaleLicenses( + productCode: string, + summary: AwsMarketplaceReconcileSummary +): Promise { + const cutoff = new Date(Date.now() - REVERIFY_AFTER_MS) + const licenseArns = await listLicensesVerifiedBefore(cutoff, REVERIFY_BATCH_SIZE) + for (const licenseArn of licenseArns) { + const verifiedAt = new Date() + try { + const entitlements = await verifyLicense(productCode, licenseArn) + await recordLicenseVerification({ + licenseArn, + productCode, + summary: entitlements, + verifiedAt, + }) + summary.reverified++ + } catch (error) { + summary.reverifyFailed++ + logger.warn('Could not re-verify an AWS Marketplace license', { + message: getErrorMessage(error, 'Unknown error'), + }) + } + } +} + +/** + * Applies queued agreement events, then re-reads licenses no event touched within a day. Both + * passes are bounded and idempotent: every write is the Entitlement Service's current answer, and + * an older read never overwrites a newer one. Returns null when the feature is off. + */ +export async function reconcileAwsMarketplaceLicenses(): Promise { + const productCode = getAwsMarketplaceProductCode() + if (!productCode) return null + const summary: AwsMarketplaceReconcileSummary = { + received: 0, + applied: 0, + discarded: 0, + retained: 0, + reverified: 0, + reverifyFailed: 0, + } + const queueUrl = env.AWS_MARKETPLACE_EVENTS_QUEUE_URL + if (queueUrl) await drainEvents(productCode, queueUrl, summary) + await reverifyStaleLicenses(productCode, summary) + return summary +} diff --git a/apps/sim/lib/billing/aws-marketplace/registration.ts b/apps/sim/lib/billing/aws-marketplace/registration.ts new file mode 100644 index 00000000000..a6752f1da33 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/registration.ts @@ -0,0 +1,91 @@ +import { + ResolveCustomerCommand, + type ResolveCustomerCommandOutput, +} from '@aws-sdk/client-marketplace-metering' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { AWS_MARKETPLACE_CLAIM_TTL_MS, issueClaimToken } from '@/lib/billing/aws-marketplace/claim' +import { getMeteringClient } from '@/lib/billing/aws-marketplace/client' +import { type EntitlementSummary, verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' +import type { AwsMarketplaceOnboardingError } from '@/lib/billing/aws-marketplace/onboarding' +import { saveRegisteredLicense } from '@/lib/billing/aws-marketplace/repository' + +const logger = createLogger('AwsMarketplaceRegistration') + +/** Token failures the buyer can fix only by starting again from AWS Marketplace. */ +const REJECTED_TOKEN_ERRORS = new Set(['InvalidTokenException', 'ExpiredTokenException']) + +type AwsMarketplaceRegistration = + | { kind: 'claimed'; claimToken: string } + | { kind: AwsMarketplaceOnboardingError } + +function errorName(error: unknown): string | undefined { + return error instanceof Error ? error.name : undefined +} + +/** + * Turns the registration token AWS posts to the fulfillment URL into a license row. Every + * registration reads the license's entitlements, active or not, and only an active license + * gets a claim the buyer can link to an organization. The token itself is never logged. + */ +export async function registerAwsMarketplaceBuyer( + productCode: string, + registrationToken: string +): Promise { + let customer: ResolveCustomerCommandOutput + try { + customer = await getMeteringClient().send( + new ResolveCustomerCommand({ RegistrationToken: registrationToken }) + ) + } catch (error) { + const name = errorName(error) + if (name && REJECTED_TOKEN_ERRORS.has(name)) { + logger.warn('AWS Marketplace rejected a registration token', { reason: name }) + return { kind: 'invalid' } + } + logger.error('Could not resolve an AWS Marketplace customer', { + reason: name, + message: getErrorMessage(error, 'Unknown error'), + }) + return { kind: 'unavailable' } + } + + if (customer.ProductCode !== productCode) { + logger.warn('AWS Marketplace registration is for another product') + return { kind: 'invalid' } + } + if (!customer.CustomerIdentifier || !customer.LicenseArn) { + logger.warn('AWS Marketplace registration resolved without a customer or license', { + hasCustomer: Boolean(customer.CustomerIdentifier), + hasLicense: Boolean(customer.LicenseArn), + }) + return { kind: 'invalid' } + } + + const verifiedAt = new Date() + let summary: EntitlementSummary + try { + summary = await verifyLicense(productCode, customer.LicenseArn) + } catch (error) { + logger.error('Could not read entitlements for an AWS Marketplace registration', { + reason: errorName(error), + message: getErrorMessage(error, 'Unknown error'), + }) + return { kind: 'unavailable' } + } + + const claim = summary.active ? issueClaimToken() : null + const licenseId = await saveRegisteredLicense({ + licenseArn: customer.LicenseArn, + productCode, + customerIdentifier: customer.CustomerIdentifier, + customerAwsAccountId: customer.CustomerAWSAccountId ?? null, + summary, + verifiedAt, + claim: claim + ? { hash: claim.hash, expiresAt: new Date(Date.now() + AWS_MARKETPLACE_CLAIM_TTL_MS) } + : null, + }) + logger.info('Registered an AWS Marketplace license', { licenseId, active: summary.active }) + return claim ? { kind: 'claimed', claimToken: claim.token } : { kind: 'no-subscription' } +} diff --git a/apps/sim/lib/billing/aws-marketplace/repository.ts b/apps/sim/lib/billing/aws-marketplace/repository.ts new file mode 100644 index 00000000000..dfdc5a7d2f6 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/repository.ts @@ -0,0 +1,203 @@ +import { db } from '@sim/db' +import { awsMarketplaceLicense, member, organization, user } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, asc, eq, inArray, lte } from 'drizzle-orm' +import type { EntitlementSummary } from '@/lib/billing/aws-marketplace/entitlements' +import type { DbTransaction } from '@/lib/db/types' + +export type AwsMarketplaceLicenseRecord = typeof awsMarketplaceLicense.$inferSelect + +interface AdministeredOrganization { + id: string + name: string + role: 'owner' | 'admin' +} + +/** + * The verification columns a read writes. A deprovisioned license returns no entitlements, so its + * last-known dimension, quantity, and expiry stay as they were and only the status changes. + */ +function verificationFields(summary: EntitlementSummary, verifiedAt: Date) { + return { + status: summary.active ? ('active' as const) : ('inactive' as const), + lastVerifiedAt: verifiedAt, + updatedAt: new Date(), + ...(summary.entitlementCount > 0 && { + dimension: summary.dimension, + quantity: summary.quantity, + expiresAt: summary.expiresAt, + }), + } +} + +/** + * Records a registration: the license row keyed by its ARN, as ResolveCustomer and the + * Entitlement Service just described it, with a fresh claim when it is active. An inactive + * registration clears any claim outstanding from an earlier one. + */ +export async function saveRegisteredLicense(input: { + licenseArn: string + productCode: string + customerIdentifier: string + customerAwsAccountId: string | null + summary: EntitlementSummary + verifiedAt: Date + claim: { hash: string; expiresAt: Date } | null +}): Promise { + const fields = { + productCode: input.productCode, + customerIdentifier: input.customerIdentifier, + customerAwsAccountId: input.customerAwsAccountId, + ...verificationFields(input.summary, input.verifiedAt), + claimTokenHash: input.claim?.hash ?? null, + claimExpiresAt: input.claim?.expiresAt ?? null, + } + const [row] = await db + .insert(awsMarketplaceLicense) + .values({ id: generateId(), licenseArn: input.licenseArn, ...fields }) + .onConflictDoUpdate({ target: awsMarketplaceLicense.licenseArn, set: fields }) + .returning({ id: awsMarketplaceLicense.id }) + return row.id +} + +/** + * Applies a verification read that started at `verifiedAt`. A read older than the row's last one + * changes nothing, so overlapping reconcile runs and registrations cannot roll a license back. A + * license AWS describes but nobody registered is recorded so it is visible to the team; one AWS + * no longer describes is only updated if it exists. + */ +export async function recordLicenseVerification(input: { + licenseArn: string + productCode: string + summary: EntitlementSummary + verifiedAt: Date +}): Promise { + const fields = verificationFields(input.summary, input.verifiedAt) + const notNewer = lte(awsMarketplaceLicense.lastVerifiedAt, input.verifiedAt) + if (input.summary.customerIdentifier) { + await db + .insert(awsMarketplaceLicense) + .values({ + id: generateId(), + licenseArn: input.licenseArn, + productCode: input.productCode, + customerIdentifier: input.summary.customerIdentifier, + customerAwsAccountId: input.summary.customerAwsAccountId, + ...fields, + }) + .onConflictDoUpdate({ + target: awsMarketplaceLicense.licenseArn, + set: fields, + setWhere: notNewer, + }) + return + } + await db + .update(awsMarketplaceLicense) + .set(fields) + .where(and(eq(awsMarketplaceLicense.licenseArn, input.licenseArn), notNewer)) +} + +/** The licenses whose last verification is oldest, up to `limit`, none newer than `cutoff`. */ +export async function listLicensesVerifiedBefore(cutoff: Date, limit: number): Promise { + const rows = await db + .select({ licenseArn: awsMarketplaceLicense.licenseArn }) + .from(awsMarketplaceLicense) + .where(lte(awsMarketplaceLicense.lastVerifiedAt, cutoff)) + .orderBy(asc(awsMarketplaceLicense.lastVerifiedAt)) + .limit(limit) + return rows.map((row) => row.licenseArn) +} + +export async function findLicenseByClaimHash( + claimTokenHash: string +): Promise { + const [row] = await db + .select() + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.claimTokenHash, claimTokenHash)) + .limit(1) + return row ?? null +} + +export async function lockLicense( + tx: DbTransaction, + licenseId: string +): Promise { + const [row] = await tx + .select() + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.id, licenseId)) + .for('update') + .limit(1) + return row ?? null +} + +/** + * Consumes the claim and records the verification it relied on, linking the license when `link` + * is given. A license already linked to the claimed organization keeps its original link. + */ +export async function consumeLicenseClaim( + tx: DbTransaction, + input: { + licenseId: string + link: { organizationId: string; userId: string } | null + summary: EntitlementSummary + verifiedAt: Date + } +): Promise { + const [row] = await tx + .update(awsMarketplaceLicense) + .set({ + ...verificationFields(input.summary, input.verifiedAt), + ...(input.link && { + organizationId: input.link.organizationId, + linkedByUserId: input.link.userId, + linkedAt: new Date(), + }), + claimTokenHash: null, + claimExpiresAt: null, + }) + .where(eq(awsMarketplaceLicense.id, input.licenseId)) + .returning() + return row +} + +/** Organizations the user owns or administers, which are the ones a license may be linked to. */ +export async function listAdministeredOrganizations( + userId: string +): Promise { + const rows = await db + .select({ id: organization.id, name: organization.name, role: member.role }) + .from(member) + .innerJoin(organization, eq(member.organizationId, organization.id)) + .where(and(eq(member.userId, userId), inArray(member.role, ['owner', 'admin']))) + .orderBy(asc(organization.name)) + return rows.flatMap((row) => + row.role === 'owner' || row.role === 'admin' ? [{ ...row, role: row.role }] : [] + ) +} + +/** Who linked a license to which organization, for the team's provisioning notice. */ +export async function getLinkParties( + organizationId: string, + userId: string +): Promise<{ organizationName: string | null; userName: string | null; userEmail: string | null }> { + const [[org], [person]] = await Promise.all([ + db + .select({ name: organization.name }) + .from(organization) + .where(eq(organization.id, organizationId)) + .limit(1), + db + .select({ name: user.name, email: user.email }) + .from(user) + .where(eq(user.id, userId)) + .limit(1), + ]) + return { + organizationName: org?.name ?? null, + userName: person?.name ?? null, + userEmail: person?.email ?? null, + } +} diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index dbc48cf9418..b118b14c9b2 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -220,6 +220,8 @@ export const env = createEnv({ STRIPE_PRICE_TEAM_100_MO: z.string().min(1).optional(), // Team Max: $100/seat/mo STRIPE_PRICE_TEAM_100_YR: z.string().min(1).optional(), // Team Max: $1,020/seat/yr OVERAGE_THRESHOLD_DOLLARS: z.number().optional().default(100), // Dollar threshold for incremental overage billing (default: $100) + AWS_MARKETPLACE_PRODUCT_CODE: z.string().min(1).optional(), // AWS Marketplace SaaS product code; unset turns the Marketplace registration, claim, and reconcile surfaces off + AWS_MARKETPLACE_EVENTS_QUEUE_URL: z.string().url().optional(), // SQS queue receiving the Marketplace agreement EventBridge events the reconcile cron drains // Email & Communication EMAIL_VERIFICATION_ENABLED: z.boolean().optional(), // Enable email verification for user registration and login (defaults to false) diff --git a/apps/sim/package.json b/apps/sim/package.json index cfc38c07c81..5e9a32f22ae 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -80,6 +80,8 @@ "@aws-sdk/client-iam": "3.1150.0", "@aws-sdk/client-identitystore": "3.1150.0", "@aws-sdk/client-lambda": "3.1150.0", + "@aws-sdk/client-marketplace-entitlement-service": "3.1150.0", + "@aws-sdk/client-marketplace-metering": "3.1150.0", "@aws-sdk/client-organizations": "3.1150.0", "@aws-sdk/client-rds-data": "3.1150.0", "@aws-sdk/client-s3": "3.1150.0", diff --git a/bun.lock b/bun.lock index ddbfea5e08b..0ab5d27bdcd 100644 --- a/bun.lock +++ b/bun.lock @@ -234,6 +234,8 @@ "@aws-sdk/client-iam": "3.1150.0", "@aws-sdk/client-identitystore": "3.1150.0", "@aws-sdk/client-lambda": "3.1150.0", + "@aws-sdk/client-marketplace-entitlement-service": "3.1150.0", + "@aws-sdk/client-marketplace-metering": "3.1150.0", "@aws-sdk/client-organizations": "3.1150.0", "@aws-sdk/client-rds-data": "3.1150.0", "@aws-sdk/client-s3": "3.1150.0", @@ -1067,6 +1069,10 @@ "@aws-sdk/client-lambda": ["@aws-sdk/client-lambda@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-bld3mYojcpYmUgXJhmwJkMPnTQcqPHOiB7JJV620g5S0Iwc1gbm4WtmR2yrEqwnMM+OngdubbLmP0wtFtSFNBQ=="], + "@aws-sdk/client-marketplace-entitlement-service": ["@aws-sdk/client-marketplace-entitlement-service@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-/CyW4gYtG5m6tdMzQLPeJoiQNu7QFZ896aGxNJkdcBjq0+U4IdKwFJ67wHsW26WhV9LEe66UD2SjCS5agrEQsw=="], + + "@aws-sdk/client-marketplace-metering": ["@aws-sdk/client-marketplace-metering@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-YwkfFFyk+ysIjeO4Wwjc6cM5RKG2VTJAhK3EnTArqTFz67i0+FY6rBcv86mCFvx14iZPGY/VYJtTprELlbIj+w=="], + "@aws-sdk/client-organizations": ["@aws-sdk/client-organizations@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-lhCEv/+maZT29yALAwLTsRfqwHNkXBA862eyy1KZCwS/r/+xeQN89xRaraggLVUUwxDMLWCmgk9faXJy3RwF/g=="], "@aws-sdk/client-rds-data": ["@aws-sdk/client-rds-data@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-HOPcSBZVXFGZEwl0mVr2ISXU8xF8ideKLpEE95MRe17WQltmm4s6ba+/OIAc0hGS5Wqqd8Pjx8aRgIAXYnAi1g=="], @@ -4773,6 +4779,10 @@ "@aws-sdk/client-lambda/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-sdk/client-marketplace-entitlement-service/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@aws-sdk/client-marketplace-metering/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-sdk/client-organizations/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@aws-sdk/client-rds-data/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index ac782009598..e73b191efa9 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -11,6 +11,8 @@ export const AuditAction = { AUTH_SESSIONS_REVOKED: 'auth.sessions_revoked', AUDIT_LOGS_EXPORTED: 'audit_logs.exported', + AWS_MARKETPLACE_LICENSE_LINKED: 'aws_marketplace_license.linked', + API_KEY_CREATED: 'api_key.created', API_KEY_UPDATED: 'api_key.updated', API_KEY_REVOKED: 'api_key.revoked', @@ -285,6 +287,7 @@ export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction] export const AuditResourceType = { ACCOUNT: 'account', API_KEY: 'api_key', + AWS_MARKETPLACE_LICENSE: 'aws_marketplace_license', BILLING: 'billing', BYOK_KEY: 'byok_key', CHANGELOG_RELEASE: 'changelog_release', diff --git a/packages/db/migrations/0454_aws_marketplace_license.sql b/packages/db/migrations/0454_aws_marketplace_license.sql new file mode 100644 index 00000000000..51aa10208cc --- /dev/null +++ b/packages/db/migrations/0454_aws_marketplace_license.sql @@ -0,0 +1,27 @@ +CREATE TYPE "public"."aws_marketplace_license_status" AS ENUM('active', 'inactive');--> statement-breakpoint +CREATE TABLE "aws_marketplace_license" ( + "id" text PRIMARY KEY NOT NULL, + "license_arn" text NOT NULL, + "product_code" text NOT NULL, + "customer_identifier" text NOT NULL, + "customer_aws_account_id" text, + "organization_id" text, + "status" "aws_marketplace_license_status" DEFAULT 'inactive' NOT NULL, + "dimension" text, + "quantity" integer, + "expires_at" timestamp, + "last_verified_at" timestamp NOT NULL, + "claim_token_hash" text, + "claim_expires_at" timestamp, + "linked_by_user_id" text, + "linked_at" timestamp, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "aws_marketplace_license" ADD CONSTRAINT "aws_marketplace_license_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "aws_marketplace_license" ADD CONSTRAINT "aws_marketplace_license_linked_by_user_id_user_id_fk" FOREIGN KEY ("linked_by_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "aws_marketplace_license_arn_unique" ON "aws_marketplace_license" USING btree ("license_arn");--> statement-breakpoint +CREATE UNIQUE INDEX "aws_marketplace_license_claim_token_hash_unique" ON "aws_marketplace_license" USING btree ("claim_token_hash");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_last_verified_at_idx" ON "aws_marketplace_license" USING btree ("last_verified_at");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_organization_id_idx" ON "aws_marketplace_license" USING btree ("organization_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0454_snapshot.json b/packages/db/migrations/meta/0454_snapshot.json new file mode 100644 index 00000000000..a3f561e1812 --- /dev/null +++ b/packages/db/migrations/meta/0454_snapshot.json @@ -0,0 +1,32651 @@ +{ + "id": "dcb900b6-83fb-4bcc-8843-c22bff6659b3", + "prevId": "ae1c3abd-b4a0-4658-8dd6-4858c603d078", + "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 + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_at": { + "name": "refresh_revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_code": { + "name": "refresh_revoked_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_revoked_token_hash": { + "name": "refresh_revoked_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_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": false + }, + "public.agent_memory_turn": { + "name": "agent_memory_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_order": { + "name": "execution_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "encrypted_state": { + "name": "encrypted_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_memory_turn_invocation_unique": { + "name": "agent_memory_turn_invocation_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_memory_turn_workflow_idx": { + "name": "agent_memory_turn_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_memory_turn_memory_id_memory_id_fk": { + "name": "agent_memory_turn_memory_id_memory_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_memory_turn_workflow_id_workflow_id_fk": { + "name": "agent_memory_turn_workflow_id_workflow_id_fk", + "tableFrom": "agent_memory_turn", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.assistant_tool_permission": { + "name": "assistant_tool_permission", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "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 + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "read_policy": { + "name": "read_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'allow'" + }, + "write_policy": { + "name": "write_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ask'" + }, + "overrides": { + "name": "overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assistant_tool_permission_credential_idx": { + "name": "assistant_tool_permission_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assistant_tool_permission_user_idx": { + "name": "assistant_tool_permission_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assistant_tool_permission_organization_id_organization_id_fk": { + "name": "assistant_tool_permission_organization_id_organization_id_fk", + "tableFrom": "assistant_tool_permission", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_tool_permission_user_id_user_id_fk": { + "name": "assistant_tool_permission_user_id_user_id_fk", + "tableFrom": "assistant_tool_permission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assistant_tool_permission_credential_id_credential_id_fk": { + "name": "assistant_tool_permission_credential_id_credential_id_fk", + "tableFrom": "assistant_tool_permission", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "assistant_tool_permission_organization_id_user_id_credential_id_pk": { + "name": "assistant_tool_permission_organization_id_user_id_credential_id_pk", + "columns": ["organization_id", "user_id", "credential_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "assistant_tool_permission_policy_check": { + "name": "assistant_tool_permission_policy_check", + "value": "\"assistant_tool_permission\".\"read_policy\" IN ('allow', 'ask', 'block') AND \"assistant_tool_permission\".\"write_policy\" IN ('allow', 'ask', 'block')" + }, + "assistant_tool_permission_revision_check": { + "name": "assistant_tool_permission_revision_check", + "value": "\"assistant_tool_permission\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_unreconciled_terminal_idx": { + "name": "async_jobs_schedule_unreconciled_terminal_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "surface": { + "name": "surface", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_org_level_created_at_id_idx": { + "name": "audit_log_org_level_created_at_id_idx", + "columns": [ + { + "expression": "(\"metadata\"->>'organizationId')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"audit_log\".\"workspace_id\" IS NULL AND (\"audit_log\".\"metadata\"->>'organizationId') IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aws_marketplace_license": { + "name": "aws_marketplace_license", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "license_arn": { + "name": "license_arn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_code": { + "name": "product_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_identifier": { + "name": "customer_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "customer_aws_account_id": { + "name": "customer_aws_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "aws_marketplace_license_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'inactive'" + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "claim_token_hash": { + "name": "claim_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_expires_at": { + "name": "claim_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_at": { + "name": "linked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "aws_marketplace_license_arn_unique": { + "name": "aws_marketplace_license_arn_unique", + "columns": [ + { + "expression": "license_arn", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "aws_marketplace_license_claim_token_hash_unique": { + "name": "aws_marketplace_license_claim_token_hash_unique", + "columns": [ + { + "expression": "claim_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "aws_marketplace_license_last_verified_at_idx": { + "name": "aws_marketplace_license_last_verified_at_idx", + "columns": [ + { + "expression": "last_verified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "aws_marketplace_license_organization_id_idx": { + "name": "aws_marketplace_license_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "aws_marketplace_license_organization_id_organization_id_fk": { + "name": "aws_marketplace_license_organization_id_organization_id_fk", + "tableFrom": "aws_marketplace_license", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "aws_marketplace_license_linked_by_user_id_user_id_fk": { + "name": "aws_marketplace_license_linked_by_user_id_user_id_fk", + "tableFrom": "aws_marketplace_license", + "tableTo": "user", + "columnsFrom": ["linked_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_change": { + "name": "changelog_change", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "changelog_change_release_position_unique": { + "name": "changelog_change_release_position_unique", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_change_release_id_changelog_release_id_fk": { + "name": "changelog_change_release_id_changelog_release_id_fk", + "tableFrom": "changelog_change", + "tableTo": "changelog_release", + "columnsFrom": ["release_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_change_workflow_id_workflow_id_fk": { + "name": "changelog_change_workflow_id_workflow_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "changelog_change_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "changelog_change", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_change_chat_id_copilot_chats_id_fk": { + "name": "changelog_change_chat_id_copilot_chats_id_fk", + "tableFrom": "changelog_change", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changelog_release": { + "name": "changelog_release", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_major": { + "name": "version_major", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_minor": { + "name": "version_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "version_patch": { + "name": "version_patch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "bump_reason": { + "name": "bump_reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "published_at": { + "name": "published_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "changelog_release_workspace_version_unique": { + "name": "changelog_release_workspace_version_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_major", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_minor", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version_patch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_body_file_unique": { + "name": "changelog_release_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changelog_release_workspace_published_idx": { + "name": "changelog_release_workspace_published_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changelog_release_workspace_id_workspace_id_fk": { + "name": "changelog_release_workspace_id_workspace_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changelog_release_body_file_id_workspace_files_id_fk": { + "name": "changelog_release_body_file_id_workspace_files_id_fk", + "tableFrom": "changelog_release", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "changelog_release_created_by_user_id_fk": { + "name": "changelog_release_created_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "changelog_release_updated_by_user_id_fk": { + "name": "changelog_release_updated_by_user_id_fk", + "tableFrom": "changelog_release", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "changelog_release_version_check": { + "name": "changelog_release_version_check", + "value": "\"changelog_release\".\"version_major\" >= 0 AND \"changelog_release\".\"version_minor\" >= 0 AND \"changelog_release\".\"version_patch\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_tool_calls": { + "name": "include_tool_calls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_user_id_idx": { + "name": "chat_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.client_credential_token": { + "name": "client_credential_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_digest": { + "name": "access_token_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "client_credential_token_expires_at_idx": { + "name": "client_credential_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "client_credential_token_access_token_digest_idx": { + "name": "client_credential_token_access_token_digest_idx", + "columns": [ + { + "expression": "access_token_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"client_credential_token\".\"access_token_digest\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "executable_name": { + "name": "executable_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_args": { + "name": "approved_args", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "assistant_action": { + "name": "assistant_action", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_retryable": { + "name": "execution_retryable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_decision": { + "name": "permission_decision", + "type": "copilot_tool_permission_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "permission_decided_at": { + "name": "permission_decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_requested_at": { + "name": "permission_requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pickup_deadline_at": { + "name": "pickup_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "persist_seq": { + "name": "persist_seq", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": "nextval('copilot_async_tool_calls_persist_seq')" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser_download_started_at": { + "name": "browser_download_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_started_at": { + "name": "execution_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_settled_at": { + "name": "execution_settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "execution_owner_token": { + "name": "execution_owner_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_lease_expires_at": { + "name": "execution_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_revoked_at": { + "name": "execution_revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_attempts": { + "name": "execution_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "client_workflow_execution_id": { + "name": "client_workflow_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sandbox_processes": { + "name": "sandbox_processes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "memory_space_id": { + "name": "memory_space_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_key": { + "name": "external_conversation_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_conversation_metadata": { + "name": "external_conversation_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "auto_allowed_tools": { + "name": "auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_organization_id_idx": { + "name": "copilot_chats_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_external_conversation_unique": { + "name": "copilot_chats_external_conversation_unique", + "columns": [ + { + "expression": "external_conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"copilot_chats\".\"external_conversation_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_org_created_idx": { + "name": "copilot_chats_user_org_created_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_deleted_partial_idx": { + "name": "copilot_chats_user_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_organization_id_organization_id_fk": { + "name": "copilot_chats_organization_id_organization_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "copilot_chats_owner_check": { + "name": "copilot_chats_owner_check", + "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1" + }, + "copilot_chats_organization_workflow_check": { + "name": "copilot_chats_organization_workflow_check", + "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "display_content": { + "name": "display_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_organization_request_stops": { + "name": "copilot_organization_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_organization_request_stops_user_id_user_id_fk": { + "name": "copilot_organization_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_organization_request_stops_organization_id_organization_id_fk": { + "name": "copilot_organization_request_stops_organization_id_organization_id_fk", + "tableFrom": "copilot_organization_request_stops", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_organization_request_stops_user_id_organization_id_stream_id_pk": { + "name": "copilot_organization_request_stops_user_id_organization_id_stream_id_pk", + "columns": ["user_id", "organization_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_request_stops": { + "name": "copilot_request_stops", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "copilot_request_stops_user_id_user_id_fk": { + "name": "copilot_request_stops_user_id_user_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_request_stops_workspace_id_workspace_id_fk": { + "name": "copilot_request_stops_workspace_id_workspace_id_fk", + "tableFrom": "copilot_request_stops", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "copilot_request_stops_user_id_workspace_id_stream_id_pk": { + "name": "copilot_request_stops_user_id_workspace_id_stream_id_pk", + "columns": ["user_id", "workspace_id", "stream_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": ["run_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_execution_version": { + "name": "tool_execution_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tool_admission_closed_at": { + "name": "tool_admission_closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "desktop_device_id": { + "name": "desktop_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_started_at_idx": { + "name": "copilot_runs_chat_started_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_desktop_device_started_at_idx": { + "name": "copilot_runs_desktop_device_started_at_idx", + "columns": [ + { + "expression": "desktop_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_runs\".\"desktop_device_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_organization_id_organization_id_fk": { + "name": "copilot_runs_organization_id_organization_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_desktop_device_id_desktop_devices_id_fk": { + "name": "copilot_runs_desktop_device_id_desktop_devices_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "desktop_devices", + "columnsFrom": ["desktop_device_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_service_usage": { + "name": "copilot_service_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service": { + "name": "service", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cost_usd": { + "name": "cost_usd", + "type": "numeric(12, 8)", + "primaryKey": false, + "notNull": false + }, + "worker_origin": { + "name": "worker_origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_service_usage_pending_idx": { + "name": "copilot_service_usage_pending_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "delivered_at IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unredacted": { + "name": "unredacted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_personal_token": { + "name": "encrypted_personal_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_app_id": { + "name": "authorization_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_enrollment_id": { + "name": "credential_group_enrollment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_oauth_config_version": { + "name": "mcp_oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_scope_version": { + "name": "managed_oauth_scope_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "provider_subject_id": { + "name": "provider_subject_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_tenant_id": { + "name": "provider_tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_oauth_status": { + "name": "managed_oauth_status", + "type": "managed_oauth_credential_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "granted_scopes": { + "name": "granted_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "provider_metadata": { + "name": "provider_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "encrypted_oauth_token_set": { + "name": "encrypted_oauth_token_set", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mcp_tools": { + "name": "mcp_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "mcp_tools_refreshed_at": { + "name": "mcp_tools_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "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 + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_organization_id_idx": { + "name": "credential_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_organization_account_unique": { + "name": "credential_organization_account_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"account_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_org_personal_token_unique": { + "name": "credential_org_personal_token_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_idx": { + "name": "credential_group_enrollment_idx", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_mcp_server_idx": { + "name": "credential_mcp_server_idx", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_option_identity_unique": { + "name": "credential_group_option_identity_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_group_option_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"provider_tenant_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_oauth'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_managed_mcp_enrollment_server_identity_unique": { + "name": "credential_managed_mcp_enrollment_server_identity_unique", + "columns": [ + { + "expression": "credential_group_enrollment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"provider_subject_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"provider_tenant_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential\".\"type\" = 'managed_mcp'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_personal_token_identity_unique": { + "name": "credential_personal_token_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'personal_token'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_organization_id_organization_id_fk": { + "name": "credential_organization_id_organization_id_fk", + "tableFrom": "credential", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_slack_app_id_slack_app_id_fk": { + "name": "credential_slack_app_id_slack_app_id_fk", + "tableFrom": "credential", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk": { + "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk", + "tableFrom": "credential", + "tableTo": "credential_group_enrollment", + "columnsFrom": ["credential_group_enrollment_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_mcp_server_id_mcp_servers_id_fk": { + "name": "credential_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "credential", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_owner_check": { + "name": "credential_owner_check", + "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1" + }, + "credential_organization_type_check": { + "name": "credential_organization_type_check", + "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')" + }, + "credential_personal_token_source_check": { + "name": "credential_personal_token_source_check", + "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_managed_oauth_source_check": { + "name": "credential_managed_oauth_source_check", + "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )" + }, + "credential_managed_oauth_group_binding_check": { + "name": "credential_managed_oauth_group_binding_check", + "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )" + }, + "credential_managed_mcp_source_check": { + "name": "credential_managed_mcp_source_check", + "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )" + }, + "credential_creator_source_check": { + "name": "credential_creator_source_check", + "value": "(type::text = 'managed_mcp') OR created_by IS NOT NULL" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_group": { + "name": "credential_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "options": { + "name": "options", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encrypted_provider_configuration": { + "name": "encrypted_provider_configuration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_organization_unique": { + "name": "credential_group_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_public_id_unique": { + "name": "credential_group_public_id_unique", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_workspace_unique": { + "name": "credential_group_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_workspace_id_workspace_id_fk": { + "name": "credential_group_workspace_id_workspace_id_fk", + "tableFrom": "credential_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_organization_id_organization_id_fk": { + "name": "credential_group_organization_id_organization_id_fk", + "tableFrom": "credential_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_created_by_user_id_fk": { + "name": "credential_group_created_by_user_id_fk", + "tableFrom": "credential_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_owner_check": { + "name": "credential_group_owner_check", + "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.credential_group_enrollment": { + "name": "credential_group_enrollment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "credential_group_enrollment_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'invited'" + }, + "invitation_token_hash": { + "name": "invitation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_expires_at": { + "name": "invitation_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "invited_at": { + "name": "invited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_delivery_error": { + "name": "last_delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_group_enrollment_group_user_unique": { + "name": "credential_group_enrollment_group_user_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_user_id_idx": { + "name": "credential_group_enrollment_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_email_unique": { + "name": "credential_group_enrollment_group_email_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_invitation_token_hash_unique": { + "name": "credential_group_enrollment_invitation_token_hash_unique", + "columns": [ + { + "expression": "invitation_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_status_idx": { + "name": "credential_group_enrollment_group_status_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_group_enrollment_group_invited_at_id_idx": { + "name": "credential_group_enrollment_group_invited_at_id_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invited_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_group_enrollment_credential_group_id_credential_group_id_fk": { + "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_user_id_user_id_fk": { + "name": "credential_group_enrollment_user_id_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_group_enrollment_created_by_user_id_fk": { + "name": "credential_group_enrollment_created_by_user_id_fk", + "tableFrom": "credential_group_enrollment", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_group_enrollment_normalized_email_check": { + "name": "credential_group_enrollment_normalized_email_check", + "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320" + }, + "credential_group_enrollment_invitation_token_hash_length_check": { + "name": "credential_group_enrollment_invitation_token_hash_length_check", + "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dashboard": { + "name": "dashboard", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dashboard_workspace_id_unique": { + "name": "dashboard_workspace_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dashboard_workspace_id_workspace_id_fk": { + "name": "dashboard_workspace_id_workspace_id_fk", + "tableFrom": "dashboard", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "dashboard_created_by_user_id_fk": { + "name": "dashboard_created_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "dashboard_updated_by_user_id_fk": { + "name": "dashboard_updated_by_user_id_fk", + "tableFrom": "dashboard", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deleted_account_identity": { + "name": "deleted_account_identity", + "schema": "", + "columns": { + "identity_hash": { + "name": "identity_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "deleted_account_identity_expires_at_idx": { + "name": "deleted_account_identity_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.desktop_devices": { + "name": "desktop_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_version": { + "name": "app_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "desktop_devices_user_id_idx": { + "name": "desktop_devices_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "desktop_devices_session_id_idx": { + "name": "desktop_devices_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "desktop_devices_user_id_user_id_fk": { + "name": "desktop_devices_user_id_user_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "desktop_devices_session_id_session_id_fk": { + "name": "desktop_devices_session_id_session_id_fk", + "tableFrom": "desktop_devices", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_queue_token": { + "name": "processing_queue_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_deferred_until": { + "name": "processing_deferred_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_recovery_after": { + "name": "processing_recovery_after", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{ws}'::text[]" + }, + "acl_requirements": { + "name": "acl_requirements", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "acl_verified_at": { + "name": "acl_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_modified_at": { + "name": "source_modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_seen_at": { + "name": "source_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_uploaded_by_idx": { + "name": "doc_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"uploaded_by\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_kb_source_modified_idx": { + "name": "doc_kb_source_modified_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_modified_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_acl_gin_idx": { + "name": "doc_acl_gin_idx", + "columns": [ + { + "expression": "acl", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "array_ops" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "gin", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_processing_recovery_idx": { + "name": "doc_connector_processing_recovery_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"content_hash\" IS NOT NULL AND \"document\".\"storage_key\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_connector_processing_status_idx": { + "name": "doc_connector_processing_status_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"processing_status\" IN ('pending', 'processing', 'failed') AND \"document\".\"connector_id\" IS NOT NULL AND \"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_source_lookup_idx": { + "name": "doc_connector_source_lookup_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_reconciliation_v2_idx": { + "name": "doc_connector_reconciliation_v2_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_active_kb_token_count_idx": { + "name": "doc_active_kb_token_count_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "token_count", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_tombstone_idx": { + "name": "doc_connector_tombstone_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NULL AND (\"document\".\"deleted_at\" IS NOT NULL OR \"document\".\"content_hash\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_live_idx": { + "name": "doc_connector_live_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag1_lower_idx": { + "name": "doc_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag2_lower_idx": { + "name": "doc_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag3_lower_idx": { + "name": "doc_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag4_lower_idx": { + "name": "doc_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag5_lower_idx": { + "name": "doc_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag6_lower_idx": { + "name": "doc_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_tag7_lower_idx": { + "name": "doc_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "doc_acl_token_shape_check": { + "name": "doc_acl_token_shape_check", + "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))" + } + }, + "isRLSEnabled": false + }, + "public.document_secret_provenance": { + "name": "document_secret_provenance", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_secret_provenance_document_id_document_id_fk": { + "name": "document_secret_provenance_document_id_document_id_fk", + "tableFrom": "document_secret_provenance", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "document_secret_provenance_status_check": { + "name": "document_secret_provenance_status_check", + "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_384": { + "name": "embedding_384", + "type": "vector(384)", + "primaryKey": false, + "notNull": false + }, + "embedding_768": { + "name": "embedding_768", + "type": "vector(768)", + "primaryKey": false, + "notNull": false + }, + "embedding_1024": { + "name": "embedding_1024", + "type": "vector(1024)", + "primaryKey": false, + "notNull": false + }, + "embedding_3072": { + "name": "embedding_3072", + "type": "vector(3072)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag1_lower_idx": { + "name": "emb_kb_tag1_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag1\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag2_lower_idx": { + "name": "emb_kb_tag2_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag2\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag3_lower_idx": { + "name": "emb_kb_tag3_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag3\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag4_lower_idx": { + "name": "emb_kb_tag4_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag4\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag5_lower_idx": { + "name": "emb_kb_tag5_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag5\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag6_lower_idx": { + "name": "emb_kb_tag6_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag6\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_tag7_lower_idx": { + "name": "emb_kb_tag7_lower_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"tag7\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_width_check": { + "name": "embedding_width_check", + "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_search": { + "name": "embedding_search", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acl": { + "name": "acl", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "binary": { + "name": "binary", + "type": "bit(1536)", + "primaryKey": false, + "notNull": false + }, + "binary_384": { + "name": "binary_384", + "type": "bit(384)", + "primaryKey": false, + "notNull": false + }, + "binary_768": { + "name": "binary_768", + "type": "bit(768)", + "primaryKey": false, + "notNull": false + }, + "binary_1024": { + "name": "binary_1024", + "type": "bit(1024)", + "primaryKey": false, + "notNull": false + }, + "binary_3072": { + "name": "binary_3072", + "type": "bit(3072)", + "primaryKey": false, + "notNull": false + }, + "vector": { + "name": "vector", + "type": "halfvec(1536)", + "primaryKey": false, + "notNull": false + }, + "vector_384": { + "name": "vector_384", + "type": "halfvec(384)", + "primaryKey": false, + "notNull": false + }, + "vector_512": { + "name": "vector_512", + "type": "halfvec(512)", + "primaryKey": false, + "notNull": false + }, + "vector_768": { + "name": "vector_768", + "type": "halfvec(768)", + "primaryKey": false, + "notNull": false + }, + "vector_1024": { + "name": "vector_1024", + "type": "halfvec(1024)", + "primaryKey": false, + "notNull": false + }, + "vector_3072": { + "name": "vector_3072", + "type": "halfvec(3072)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "embedding_search_kb_idx": { + "name": "embedding_search_kb_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_search_document_lookup_idx": { + "name": "embedding_search_document_lookup_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"embedding_search\".\"enabled\"", + "concurrently": true, + "method": "btree", + "with": {} + }, + "embedding_search_cosine_hnsw_idx": { + "name": "embedding_search_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_512_cosine_hnsw_idx": { + "name": "embedding_search_512_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_512", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_384_cosine_hnsw_idx": { + "name": "embedding_search_384_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_384", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_768_cosine_hnsw_idx": { + "name": "embedding_search_768_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_768", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_1024_cosine_hnsw_idx": { + "name": "embedding_search_1024_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_1024", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "embedding_search_3072_cosine_hnsw_idx": { + "name": "embedding_search_3072_cosine_hnsw_idx", + "columns": [ + { + "expression": "vector_3072", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "halfvec_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + } + }, + "foreignKeys": { + "embedding_search_id_embedding_id_fk": { + "name": "embedding_search_id_embedding_id_fk", + "tableFrom": "embedding_search", + "tableTo": "embedding", + "columnsFrom": ["id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_search_width_check": { + "name": "embedding_search_width_check", + "value": "num_nonnulls(\"binary\", \"binary_384\", \"binary_768\", \"binary_1024\", \"binary_3072\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.embedding_secret_provenance": { + "name": "embedding_secret_provenance", + "schema": "", + "columns": { + "embedding_id": { + "name": "embedding_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "embedding_secret_provenance_embedding_id_embedding_id_fk": { + "name": "embedding_secret_provenance_embedding_id_embedding_id_fk", + "tableFrom": "embedding_secret_provenance", + "tableTo": "embedding", + "columnsFrom": ["embedding_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_secret_provenance_status_check": { + "name": "embedding_secret_provenance_status_check", + "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": ["parent_key", "child_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_references_workflow_id_idx": { + "name": "execution_large_value_references_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": ["key", "execution_id", "source"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workflow_id_idx": { + "name": "execution_large_values_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_workspace_id_idx": { + "name": "execution_large_values_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_search_dependency": { + "name": "file_search_dependency", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "file_search_dependency_file_idx": { + "name": "file_search_dependency_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "file_search_dependency_owner_idx": { + "name": "file_search_dependency_owner_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "file_search_dependency_build_id_workspace_file_search_build_id_fk": { + "name": "file_search_dependency_build_id_workspace_file_search_build_id_fk", + "tableFrom": "file_search_dependency", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "file_search_dependency_build_id_file_id_pk": { + "name": "file_search_dependency_build_id_file_id_pk", + "columns": ["build_id", "file_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.file_search_dispatch_queue": { + "name": "file_search_dispatch_queue", + "schema": "", + "columns": { + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_search_dispatch_schedule_idx": { + "name": "file_search_dispatch_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "file_search_dispatch_queue_entity_type_entity_id_pk": { + "name": "file_search_dispatch_queue_entity_type_entity_id_pk", + "columns": ["entity_type", "entity_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_dispatch_owner_check": { + "name": "file_search_dispatch_owner_check", + "value": "\"file_search_dispatch_queue\".\"entity_type\" IN ('workspace', 'project') AND length(\"file_search_dispatch_queue\".\"entity_id\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.folder": { + "name": "folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "folder_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "folder_project_id_idx": { + "name": "folder_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "folder_project_resource_parent_name_active_unique": { + "name": "folder_project_resource_parent_name_active_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL AND \"folder\".\"project_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "folder_user_idx": { + "name": "folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_idx": { + "name": "folder_workspace_resource_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_parent_sort_idx": { + "name": "folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_deleted_at_idx": { + "name": "folder_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_deleted_partial_idx": { + "name": "folder_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"folder\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "folder_workspace_resource_parent_name_active_unique": { + "name": "folder_workspace_resource_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"folder\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "folder_project_id_project_id_fk": { + "name": "folder_project_id_project_id_fk", + "tableFrom": "folder", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "folder_user_id_user_id_fk": { + "name": "folder_user_id_user_id_fk", + "tableFrom": "folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_workspace_id_workspace_id_fk": { + "name": "folder_workspace_id_workspace_id_fk", + "tableFrom": "folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "folder_parent_id_folder_id_fk": { + "name": "folder_parent_id_folder_id_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "folder_workspace_parent_owner_fk": { + "name": "folder_workspace_parent_owner_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id", "workspace_id", "resource_type"], + "columnsTo": ["id", "workspace_id", "resource_type"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "folder_project_parent_owner_fk": { + "name": "folder_project_parent_owner_fk", + "tableFrom": "folder", + "tableTo": "folder", + "columnsFrom": ["parent_id", "project_id", "resource_type"], + "columnsTo": ["id", "project_id", "resource_type"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "folder_workspace_owner_type_unique": { + "name": "folder_workspace_owner_type_unique", + "nullsNotDistinct": false, + "columns": ["id", "workspace_id", "resource_type"] + }, + "folder_project_owner_type_unique": { + "name": "folder_project_owner_type_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id", "resource_type"] + } + }, + "policies": {}, + "checkConstraints": { + "folder_owner_check": { + "name": "folder_owner_check", + "value": "num_nonnulls(\"folder\".\"workspace_id\", \"folder\".\"project_id\") = 1 AND (\"folder\".\"project_id\" IS NULL OR \"folder\".\"resource_type\" = 'file')" + } + }, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "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" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurrence_at": { + "name": "occurrence_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_request_id": { + "name": "billing_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_occurrence_unique": { + "name": "job_execution_logs_schedule_occurrence_unique", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurrence_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_billing_request_unique": { + "name": "job_execution_logs_billing_request_unique", + "columns": [ + { + "expression": "billing_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"job_execution_logs\".\"billing_request_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_open_idx": { + "name": "job_execution_logs_open_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"job_execution_logs\".\"status\" IN ('queued', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_search_index": { + "name": "is_search_index", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_organization_id_idx": { + "name": "kb_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_search_index_unique": { + "name": "kb_organization_search_index_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_organization_name_active_unique": { + "name": "kb_organization_name_active_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_folder_id_idx": { + "name": "kb_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_search_index_unique": { + "name": "kb_workspace_search_index_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_organization_id_organization_id_fk": { + "name": "knowledge_base_organization_id_organization_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_folder_id_folder_id_fk": { + "name": "knowledge_base_folder_id_folder_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kb_owner_check": { + "name": "kb_owner_check", + "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") = 1" + }, + "kb_organization_search_index_check": { + "name": "kb_organization_search_index_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"is_search_index\"" + }, + "kb_organization_folder_check": { + "name": "kb_organization_folder_check", + "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_base_import": { + "name": "knowledge_base_import", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "documents_total": { + "name": "documents_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "documents_imported": { + "name": "documents_imported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "documents_failed": { + "name": "documents_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "vectors_reused": { + "name": "vectors_reused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failures": { + "name": "failures", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "knowledge_base_import_watchdog_idx": { + "name": "knowledge_base_import_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "knowledge_base_import_knowledge_base_id_idx": { + "name": "knowledge_base_import_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_import_workspace_id_workspace_id_fk": { + "name": "knowledge_base_import_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base_import", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_import_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_import_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_import", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "access_mode": { + "name": "access_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workspace'" + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_option_id": { + "name": "credential_group_option_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_status": { + "name": "member_sync_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "member_sync_lock_token": { + "name": "member_sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_lock_lease_at": { + "name": "member_sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_member_sync_at": { + "name": "next_member_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_at": { + "name": "last_member_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_member_sync_error": { + "name": "last_member_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_sync_consecutive_failures": { + "name": "member_sync_consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "access_rewrite_pending": { + "name": "access_rewrite_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image_removal_pending": { + "name": "image_removal_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "member_tombstone_cursor": { + "name": "member_tombstone_cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "member_resurrection_cursor": { + "name": "member_resurrection_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "directory_checkpoint": { + "name": "directory_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + }, + "next_directory_sync_at": { + "name": "next_directory_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detached_at": { + "name": "detached_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "detach_reserved_bytes": { + "name": "detach_reserved_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_member_sync_due_idx": { + "name": "kc_member_sync_due_idx", + "columns": [ + { + "expression": "member_sync_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_member_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_directory_sync_due_idx": { + "name": "kc_directory_sync_due_idx", + "columns": [ + { + "expression": "next_directory_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_credential_group_id_credential_group_id_fk": { + "name": "knowledge_connector_credential_group_id_credential_group_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kc_access_mode_check": { + "name": "kc_access_mode_check", + "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')" + }, + "kc_member_sync_status_check": { + "name": "kc_member_sync_status_check", + "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')" + }, + "kc_sync_lock_exclusive_check": { + "name": "kc_sync_lock_exclusive_check", + "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member": { + "name": "knowledge_connector_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_listing_at": { + "name": "last_complete_listing_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_listed_count": { + "name": "last_listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "member_synced_through": { + "name": "member_synced_through", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewed_at": { + "name": "scope_renewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_cursor": { + "name": "scope_renewal_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_renewal_started_at": { + "name": "scope_renewal_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "change_cursor": { + "name": "change_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "listing_checkpoint": { + "name": "listing_checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kcm_organization_id_idx": { + "name": "kcm_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_credential_unique": { + "name": "kcm_connector_credential_unique", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_connector_queue_idx": { + "name": "kcm_connector_queue_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "last_started_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcm_credential_idx": { + "name": "kcm_credential_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_workspace_id_workspace_id_fk": { + "name": "knowledge_connector_member_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_organization_id_organization_id_fk": { + "name": "knowledge_connector_member_organization_id_organization_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_connector_member_credential_id_credential_id_fk": { + "name": "knowledge_connector_member_credential_id_credential_id_fk", + "tableFrom": "knowledge_connector_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcm_owner_check": { + "name": "kcm_owner_check", + "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1" + }, + "kcm_status_check": { + "name": "kcm_status_check", + "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')" + }, + "kcm_subject_token_shape_check": { + "name": "kcm_subject_token_shape_check", + "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_member_sync_log": { + "name": "knowledge_connector_member_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "members_claimed": { + "name": "members_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_completed": { + "name": "members_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_incomplete": { + "name": "members_incomplete", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "members_failed": { + "name": "members_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "processing_dispatch_failed": { + "name": "processing_dispatch_failed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "docs_listed": { + "name": "docs_listed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_hydrated_once": { + "name": "docs_hydrated_once", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_added": { + "name": "observations_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_renewed": { + "name": "observations_renewed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "observations_removed": { + "name": "observations_removed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_tombstoned": { + "name": "docs_tombstoned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_resurrected": { + "name": "docs_resurrected", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_purged": { + "name": "docs_purged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credentials_audited": { + "name": "credentials_audited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcmsl_connector_started_at_idx": { + "name": "kcmsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcmsl_started_at_partial_idx": { + "name": "kcmsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_member_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcmsl_status_check": { + "name": "kcmsl_status_check", + "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')" + }, + "kcmsl_database_failure_class_check": { + "name": "kcmsl_database_failure_class_check", + "value": "\"knowledge_connector_member_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_partition": { + "name": "knowledge_connector_partition", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "partition_key": { + "name": "partition_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_at": { + "name": "retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_served_at": { + "name": "last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure": { + "name": "failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "permission_cursor": { + "name": "permission_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_attempts": { + "name": "permission_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "permission_retry_at": { + "name": "permission_retry_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permission_last_served_at": { + "name": "permission_last_served_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_started_at": { + "name": "permission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "permission_failure": { + "name": "permission_failure", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcp_content_due_idx": { + "name": "kcp_content_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcp_permission_due_idx": { + "name": "kcp_permission_due_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_retry_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_last_served_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_partition_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_partition_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_partition", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcp_pk": { + "name": "kcp_pk", + "columns": ["connector_id", "partition_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcp_partition_key_check": { + "name": "kcp_partition_key_check", + "value": "octet_length(\"knowledge_connector_partition\".\"partition_key\") BETWEEN 1 AND 1024" + }, + "kcp_context_check": { + "name": "kcp_context_check", + "value": "jsonb_typeof(\"knowledge_connector_partition\".\"context\") = 'object' AND octet_length(\"knowledge_connector_partition\".\"context\"::text) <= 16384" + }, + "kcp_status_check": { + "name": "kcp_status_check", + "value": "\"knowledge_connector_partition\".\"status\" IN ('pending', 'complete', 'blocked')" + }, + "kcp_cursor_check": { + "name": "kcp_cursor_check", + "value": "(\"knowledge_connector_partition\".\"cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"cursor\") <= 393216) AND (\"knowledge_connector_partition\".\"permission_cursor\" IS NULL OR octet_length(\"knowledge_connector_partition\".\"permission_cursor\") <= 393216)" + }, + "kcp_attempts_check": { + "name": "kcp_attempts_check", + "value": "\"knowledge_connector_partition\".\"attempts\" >= 0 AND \"knowledge_connector_partition\".\"permission_attempts\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_grant": { + "name": "knowledge_connector_permission_grant", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kcpg_subject_idx": { + "name": "kcpg_subject_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kcpg_snapshot_fk": { + "name": "kcpg_snapshot_fk", + "tableFrom": "knowledge_connector_permission_grant", + "tableTo": "knowledge_connector_permission_snapshot", + "columnsFrom": ["connector_id"], + "columnsTo": ["connector_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "kcpg_pk": { + "name": "kcpg_pk", + "columns": ["connector_id", "group_key", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcpg_group_check": { + "name": "kcpg_group_check", + "value": "length(\"knowledge_connector_permission_grant\".\"group_key\") BETWEEN 1 AND 255" + }, + "kcpg_subject_check": { + "name": "kcpg_subject_check", + "value": "\"knowledge_connector_permission_grant\".\"subject_token\" ~ '^u:[^[:space:]A-Z]+@[^[:space:]A-Z]+$'" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_permission_snapshot": { + "name": "knowledge_connector_permission_snapshot", + "schema": "", + "columns": { + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "kcps_connector_fk": { + "name": "kcps_connector_fk", + "tableFrom": "knowledge_connector_permission_snapshot", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcps_revision_check": { + "name": "kcps_revision_check", + "value": "\"knowledge_connector_permission_snapshot\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_skipped": { + "name": "docs_skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "listed_count": { + "name": "listed_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_failure_class": { + "name": "database_failure_class", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_started_at_idx": { + "name": "kcsl_connector_started_at_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kcsl_terminal_started_at_idx": { + "name": "kcsl_terminal_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" IN ('completed', 'failed')", + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "kcsl_database_failure_class_check": { + "name": "kcsl_database_failure_class_check", + "value": "\"knowledge_connector_sync_log\".\"database_failure_class\" IN ('capacity', 'conflict', 'connection')" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_document_observation": { + "name": "knowledge_document_observation", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "kdo_member_idx": { + "name": "kdo_member_idx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_document_observation_document_id_document_id_fk": { + "name": "knowledge_document_observation_document_id_document_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": { + "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk", + "tableFrom": "knowledge_document_observation", + "tableTo": "knowledge_connector_member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_document_observation_document_id_member_id_pk": { + "name": "knowledge_document_observation_document_id_member_id_pk", + "columns": ["document_id", "member_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_external_directory": { + "name": "knowledge_external_directory", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_complete_sync_at": { + "name": "last_complete_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "ked_workspace_identity_unique": { + "name": "ked_workspace_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ked_organization_identity_unique": { + "name": "ked_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_directory_workspace_id_workspace_id_fk": { + "name": "knowledge_external_directory_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_external_directory_organization_id_organization_id_fk": { + "name": "knowledge_external_directory_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_directory", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "ked_owner_check": { + "name": "ked_owner_check", + "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group": { + "name": "knowledge_external_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_group_id": { + "name": "external_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "keg_organization_identity_unique": { + "name": "keg_organization_identity_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_organization_synced_idx": { + "name": "keg_organization_synced_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_identity_unique": { + "name": "keg_identity_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tenant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "keg_workspace_synced_idx": { + "name": "keg_workspace_synced_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_synced_at", + "isExpression": false, + "asc": true, + "nulls": "first" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_external_group_organization_id_organization_id_fk": { + "name": "knowledge_external_group_organization_id_organization_id_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "keg_workspace_fk": { + "name": "keg_workspace_fk", + "tableFrom": "knowledge_external_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "keg_owner_check": { + "name": "keg_owner_check", + "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.knowledge_external_group_member": { + "name": "knowledge_external_group_member", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_token": { + "name": "subject_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kegm_subject_token_idx": { + "name": "kegm_subject_token_idx", + "columns": [ + { + "expression": "subject_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "kegm_group_fk": { + "name": "kegm_group_fk", + "tableFrom": "knowledge_external_group_member", + "tableTo": "knowledge_external_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "knowledge_external_group_member_group_id_subject_token_pk": { + "name": "knowledge_external_group_member_group_id_subject_token_pk", + "columns": ["group_id", "subject_token"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_projection_dirty": { + "name": "knowledge_projection_dirty", + "schema": "", + "columns": { + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "content": { + "name": "content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marked_at": { + "name": "marked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "knowledge_projection_dirty_marked_at_idx": { + "name": "knowledge_projection_dirty_marked_at_idx", + "columns": [ + { + "expression": "marked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_projection_dirty_document_id_document_id_fk": { + "name": "knowledge_projection_dirty_document_id_document_id_fk", + "tableFrom": "knowledge_projection_dirty", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_organization_id_organization_id_fk": { + "name": "mcp_server_oauth_organization_id_organization_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_server_oauth_owner_check": { + "name": "mcp_server_oauth_owner_check", + "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_group_id": { + "name": "credential_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_connector_id": { + "name": "managed_connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config_version": { + "name": "oauth_config_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_organization_id_idx": { + "name": "mcp_servers_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_idx": { + "name": "mcp_servers_credential_group_idx", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_credential_group_managed_connector_unique": { + "name": "mcp_servers_credential_group_managed_connector_unique", + "columns": [ + { + "expression": "credential_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "managed_connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_organization_id_organization_id_fk": { + "name": "mcp_servers_organization_id_organization_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_credential_group_id_credential_group_id_fk": { + "name": "mcp_servers_credential_group_id_credential_group_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "credential_group", + "columnsFrom": ["credential_group_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mcp_servers_owner_check": { + "name": "mcp_servers_owner_check", + "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1" + }, + "mcp_servers_organization_managed_check": { + "name": "mcp_servers_organization_managed_check", + "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL" + }, + "mcp_servers_credential_group_managed_connector_check": { + "name": "mcp_servers_credential_group_managed_connector_check", + "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL" + }, + "mcp_servers_managed_connector_oauth_check": { + "name": "mcp_servers_managed_connector_oauth_check", + "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'" + } + }, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "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" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member_profile": { + "name": "member_profile", + "schema": "", + "columns": { + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department": { + "name": "department", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "manager_member_id": { + "name": "manager_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_profile_manager_member_id_idx": { + "name": "member_profile_manager_member_id_idx", + "columns": [ + { + "expression": "manager_member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_profile_updated_by_idx": { + "name": "member_profile_updated_by_idx", + "columns": [ + { + "expression": "updated_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_profile_member_id_member_id_fk": { + "name": "member_profile_member_id_member_id_fk", + "tableFrom": "member_profile", + "tableTo": "member", + "columnsFrom": ["member_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_profile_manager_member_id_member_id_fk": { + "name": "member_profile_manager_member_id_member_id_fk", + "tableFrom": "member_profile", + "tableTo": "member", + "columnsFrom": ["manager_member_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "member_profile_updated_by_user_id_fk": { + "name": "member_profile_updated_by_user_id_fk", + "tableFrom": "member_profile", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "member_profile_not_own_manager": { + "name": "member_profile_not_own_manager", + "value": "\"member_profile\".\"manager_member_id\" <> \"member_profile\".\"member_id\"" + } + }, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "storage_version": { + "name": "storage_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "encrypted_context_summary": { + "name": "encrypted_context_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_artifact": { + "name": "memory_artifact", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "memory_artifact_key_idx": { + "name": "memory_artifact_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_artifact_memory_id_memory_id_fk": { + "name": "memory_artifact_memory_id_memory_id_fk", + "tableFrom": "memory_artifact", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memory_artifact_key_execution_large_values_key_fk": { + "name": "memory_artifact_key_execution_large_values_key_fk", + "tableFrom": "memory_artifact", + "tableTo": "execution_large_values", + "columnsFrom": ["key"], + "columnsTo": ["key"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memory_artifact_memory_id_key_pk": { + "name": "memory_artifact_memory_id_key_pk", + "columns": ["memory_id", "key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory_item": { + "name": "memory_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "memory_item_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "append_key": { + "name": "append_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "turn_id": { + "name": "turn_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_status": { + "name": "provenance_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provenance_entries": { + "name": "provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memory_item_append_unique": { + "name": "memory_item_append_unique", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "append_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_item_sequence_idx": { + "name": "memory_item_sequence_idx", + "columns": [ + { + "expression": "memory_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_item_memory_id_memory_id_fk": { + "name": "memory_item_memory_id_memory_id_fk", + "tableFrom": "memory_item", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_item_kind_check": { + "name": "memory_item_kind_check", + "value": "\"memory_item\".\"kind\" IN ('message', 'exchange')" + }, + "memory_item_provenance_status_check": { + "name": "memory_item_provenance_status_check", + "value": "\"memory_item\".\"provenance_status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.memory_secret_provenance": { + "name": "memory_secret_provenance", + "schema": "", + "columns": { + "memory_id": { + "name": "memory_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memory_secret_provenance_memory_id_memory_id_fk": { + "name": "memory_secret_provenance_memory_id_memory_id_fk", + "tableFrom": "memory_secret_provenance", + "tableTo": "memory", + "columnsFrom": ["memory_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "memory_secret_provenance_status_check": { + "name": "memory_secret_provenance_status_check", + "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.mothership_benchmark_runs": { + "name": "mothership_benchmark_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "benchmark_id": { + "name": "benchmark_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "evaluation_key": { + "name": "evaluation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "correct": { + "name": "correct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "automatic_correct": { + "name": "automatic_correct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total": { + "name": "total", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_benchmark_runs_benchmark_created_idx": { + "name": "mothership_benchmark_runs_benchmark_created_idx", + "columns": [ + { + "expression": "benchmark_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_benchmark_runs_benchmark_id_mothership_benchmarks_id_fk": { + "name": "mothership_benchmark_runs_benchmark_id_mothership_benchmarks_id_fk", + "tableFrom": "mothership_benchmark_runs", + "tableTo": "mothership_benchmarks", + "columnsFrom": ["benchmark_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mothership_benchmark_runs_score_check": { + "name": "mothership_benchmark_runs_score_check", + "value": "\"mothership_benchmark_runs\".\"total\" > 0 AND \"mothership_benchmark_runs\".\"correct\" >= 0 AND \"mothership_benchmark_runs\".\"correct\" <= \"mothership_benchmark_runs\".\"total\"" + }, + "mothership_benchmark_runs_version_check": { + "name": "mothership_benchmark_runs_version_check", + "value": "\"mothership_benchmark_runs\".\"version\" > 0" + }, + "mothership_benchmark_runs_automatic_score_check": { + "name": "mothership_benchmark_runs_automatic_score_check", + "value": "\"mothership_benchmark_runs\".\"automatic_correct\" >= 0 AND \"mothership_benchmark_runs\".\"automatic_correct\" <= \"mothership_benchmark_runs\".\"total\"" + } + }, + "isRLSEnabled": false + }, + "public.mothership_benchmarks": { + "name": "mothership_benchmarks", + "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 + }, + "run_as_user_id": { + "name": "run_as_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "running_stage": { + "name": "running_stage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "planner_chat_id": { + "name": "planner_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_benchmarks_owner_created_idx": { + "name": "mothership_benchmarks_owner_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mothership_benchmarks_workspace_idx": { + "name": "mothership_benchmarks_workspace_idx", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_benchmarks_organization_id_organization_id_fk": { + "name": "mothership_benchmarks_organization_id_organization_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_user_id_user_id_fk": { + "name": "mothership_benchmarks_user_id_user_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_run_as_user_id_user_id_fk": { + "name": "mothership_benchmarks_run_as_user_id_user_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "user", + "columnsFrom": ["run_as_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_benchmarks_source_workspace_id_workspace_id_fk": { + "name": "mothership_benchmarks_source_workspace_id_workspace_id_fk", + "tableFrom": "mothership_benchmarks", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mothership_benchmarks_version_check": { + "name": "mothership_benchmarks_version_check", + "value": "\"mothership_benchmarks\".\"version\" > 0" + }, + "mothership_benchmarks_attempt_check": { + "name": "mothership_benchmarks_attempt_check", + "value": "(\"mothership_benchmarks\".\"running_stage\" IS NULL AND \"mothership_benchmarks\".\"attempt_id\" IS NULL AND \"mothership_benchmarks\".\"lease_expires_at\" IS NULL) OR (\"mothership_benchmarks\".\"running_stage\" IS NOT NULL AND \"mothership_benchmarks\".\"running_stage\" IN ('distill', 'redact', 'plan', 'reconstruct', 'grade') AND \"mothership_benchmarks\".\"attempt_id\" IS NOT NULL AND \"mothership_benchmarks\".\"lease_expires_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": ["added_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": ["workspace_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_memory_selections": { + "name": "mothership_memory_selections", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "space_id": { + "name": "space_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_memory_selections_user_id_user_id_fk": { + "name": "mothership_memory_selections_user_id_user_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_selections_organization_id_organization_id_fk": { + "name": "mothership_memory_selections_organization_id_organization_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_selections_space_id_mothership_memory_spaces_id_fk": { + "name": "mothership_memory_selections_space_id_mothership_memory_spaces_id_fk", + "tableFrom": "mothership_memory_selections", + "tableTo": "mothership_memory_spaces", + "columnsFrom": ["space_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_memory_selections_user_id_organization_id_pk": { + "name": "mothership_memory_selections_user_id_organization_id_pk", + "columns": ["user_id", "organization_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_memory_spaces": { + "name": "mothership_memory_spaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_memory_spaces_owner_idx": { + "name": "mothership_memory_spaces_owner_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_memory_spaces_user_id_user_id_fk": { + "name": "mothership_memory_spaces_user_id_user_id_fk", + "tableFrom": "mothership_memory_spaces", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_memory_spaces_organization_id_organization_id_fk": { + "name": "mothership_memory_spaces_organization_id_organization_id_fk", + "tableFrom": "mothership_memory_spaces", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_resource_effects": { + "name": "mothership_resource_effects", + "schema": "", + "columns": { + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "effect_id": { + "name": "effect_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_resource_effects_chat_id_copilot_chats_id_fk": { + "name": "mothership_resource_effects_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_resource_effects", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "mothership_resource_effects_chat_id_effect_id_pk": { + "name": "mothership_resource_effects_chat_id_effect_id_pk", + "columns": ["chat_id", "effect_id"] + } + }, + "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": 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": 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", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "oauth_access_token_client_id_idx": { + "name": "oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_session_id_idx": { + "name": "oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_id_idx": { + "name": "oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_user_client_idx": { + "name": "oauth_access_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_expires_at_idx": { + "name": "oauth_access_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": { + "oauth_access_token_search_resource_check": { + "name": "oauth_access_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_access_token\".\"scopes\")) OR \"oauth_access_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "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": true, + "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 + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "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", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "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 + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "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": { + "oauth_client_user_id_idx": { + "name": "oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": false + }, + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_client_id_idx": { + "name": "oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": { + "oauth_consent_user_client_reference_unique": { + "name": "oauth_consent_user_client_reference_unique", + "nullsNotDistinct": true, + "columns": ["user_id", "client_id", "reference_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_id": { + "name": "family_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_refresh_token_client_id_idx": { + "name": "oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_session_id_idx": { + "name": "oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_user_client_idx": { + "name": "oauth_refresh_token_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_token_expires_at_idx": { + "name": "oauth_refresh_token_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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" + }, + "oauth_refresh_token_family_id_oauth_token_family_id_fk": { + "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_token_family", + "columnsFrom": ["family_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + }, + "oauth_refresh_token_family_generation_unique": { + "name": "oauth_refresh_token_family_generation_unique", + "nullsNotDistinct": false, + "columns": ["family_id", "generation"] + } + }, + "policies": {}, + "checkConstraints": { + "oauth_refresh_token_generation_check": { + "name": "oauth_refresh_token_generation_check", + "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000" + }, + "oauth_refresh_token_search_resource_check": { + "name": "oauth_refresh_token_search_resource_check", + "value": "NOT ('search:read' = ANY(\"oauth_refresh_token\".\"scopes\")) OR \"oauth_refresh_token\".\"resource\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.oauth_token_family": { + "name": "oauth_token_family", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "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 + }, + "consent_id": { + "name": "consent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_generation": { + "name": "current_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_token_family_client_id_idx": { + "name": "oauth_token_family_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_session_id_idx": { + "name": "oauth_token_family_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_user_client_idx": { + "name": "oauth_token_family_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_consent_id_idx": { + "name": "oauth_token_family_consent_id_idx", + "columns": [ + { + "expression": "consent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_token_family_expires_at_idx": { + "name": "oauth_token_family_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_token_family_client_id_oauth_client_client_id_fk": { + "name": "oauth_token_family_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_client", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_session_id_session_id_fk": { + "name": "oauth_token_family_session_id_session_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "session", + "columnsFrom": ["session_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_token_family_user_id_user_id_fk": { + "name": "oauth_token_family_user_id_user_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_token_family_consent_id_oauth_consent_id_fk": { + "name": "oauth_token_family_consent_id_oauth_consent_id_fk", + "tableFrom": "oauth_token_family", + "tableTo": "oauth_consent", + "columnsFrom": ["consent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "oauth_token_family_generation_check": { + "name": "oauth_token_family_generation_check", + "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000" + } + }, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "session_policy_settings": { + "name": "session_policy_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "security_policy_version": { + "name": "security_policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_access_request_settings": { + "name": "organization_access_request_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "allow_requests": { + "name": "allow_requests", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_access_request_settings_organization_id_organization_id_fk": { + "name": "organization_access_request_settings_organization_id_organization_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_access_request_settings_updated_by_user_id_fk": { + "name": "organization_access_request_settings_updated_by_user_id_fk", + "tableFrom": "organization_access_request_settings", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_byok_keys": { + "name": "organization_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_byok_organization_provider_idx": { + "name": "organization_byok_organization_provider_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_byok_keys_organization_id_organization_id_fk": { + "name": "organization_byok_keys_organization_id_organization_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_byok_keys_created_by_user_id_fk": { + "name": "organization_byok_keys_created_by_user_id_fk", + "tableFrom": "organization_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "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 + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": ["set_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_history": { + "name": "organization_search_history", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "queries": { + "name": "queries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "organization_search_history_user_idx": { + "name": "organization_search_history_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_search_history_organization_id_organization_id_fk": { + "name": "organization_search_history_organization_id_organization_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_search_history_user_id_user_id_fk": { + "name": "organization_search_history_user_id_user_id_fk", + "tableFrom": "organization_search_history", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_history_organization_id_user_id_pk": { + "name": "organization_search_history_organization_id_user_id_pk", + "columns": ["organization_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_integration": { + "name": "organization_search_integration", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_search_integration_organization_id_organization_id_fk": { + "name": "organization_search_integration_organization_id_organization_id_fk", + "tableFrom": "organization_search_integration", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "organization_search_integration_organization_id_connector_type_pk": { + "name": "organization_search_integration_organization_id_connector_type_pk", + "columns": ["organization_id", "connector_type"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_search_mcp_invocation": { + "name": "organization_search_mcp_invocation", + "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": false + }, + "auth_kind": { + "name": "auth_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_search_mcp_invocation_org_created_idx": { + "name": "organization_search_mcp_invocation_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_search_mcp_invocation_user_idx": { + "name": "organization_search_mcp_invocation_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_search_mcp_invocation_org_fk": { + "name": "org_search_mcp_invocation_org_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_search_mcp_invocation_user_fk": { + "name": "org_search_mcp_invocation_user_fk", + "tableFrom": "organization_search_mcp_invocation", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_search_mcp_invocation_tool_check": { + "name": "organization_search_mcp_invocation_tool_check", + "value": "\"organization_search_mcp_invocation\".\"tool_name\" IN ('search', 'read_document', 'chat')" + }, + "organization_search_mcp_invocation_outcome_check": { + "name": "organization_search_mcp_invocation_outcome_check", + "value": "\"organization_search_mcp_invocation\".\"outcome\" IN ('success', 'error', 'cancelled', 'rate_limited')" + }, + "organization_search_mcp_invocation_duration_check": { + "name": "organization_search_mcp_invocation_duration_check", + "value": "\"organization_search_mcp_invocation\".\"duration_ms\" >= 0" + }, + "organization_search_mcp_invocation_client_name_check": { + "name": "organization_search_mcp_invocation_client_name_check", + "value": "length(\"organization_search_mcp_invocation\".\"client_name\") <= 256" + }, + "organization_search_mcp_invocation_auth_check": { + "name": "organization_search_mcp_invocation_auth_check", + "value": "(\"organization_search_mcp_invocation\".\"auth_kind\" = 'oauth_access_token' AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NOT NULL)\n OR (\"organization_search_mcp_invocation\".\"auth_kind\" IN ('personal_api_key', 'workspace_api_key') AND \"organization_search_mcp_invocation\".\"oauth_client_id\" IS NULL AND \"organization_search_mcp_invocation\".\"client_name\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.organization_secret": { + "name": "organization_secret", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_shared_unique": { + "name": "organization_secret_shared_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_member_unique": { + "name": "organization_secret_member_unique", + "columns": [ + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_secret\".\"owner_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_secret_owner_idx": { + "name": "organization_secret_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_id_organization_secret_source_id_fk": { + "name": "organization_secret_source_id_organization_secret_source_id_fk", + "tableFrom": "organization_secret", + "tableTo": "organization_secret_source", + "columnsFrom": ["source_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_secret_owner_user_id_user_id_fk": { + "name": "organization_secret_owner_user_id_user_id_fk", + "tableFrom": "organization_secret", + "tableTo": "user", + "columnsFrom": ["owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_secret_source": { + "name": "organization_secret_source", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_secret_source_org_unique": { + "name": "organization_secret_source_org_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_secret_source_organization_id_organization_id_fk": { + "name": "organization_secret_source_organization_id_organization_id_fk", + "tableFrom": "organization_secret_source", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_secret_source_mode_check": { + "name": "organization_secret_source_mode_check", + "value": "\"organization_secret_source\".\"mode\" IN ('organization', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_pending_type_available_idx": { + "name": "outbox_event_pending_type_available_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"outbox_event\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_config": { + "name": "oauth_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_organization_id_idx": { + "name": "pending_draft_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_org": { + "name": "pending_draft_user_provider_org", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_organization_id_organization_id_fk": { + "name": "pending_credential_draft_organization_id_organization_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "pending_draft_owner_check": { + "name": "pending_draft_owner_check", + "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.people_directory_entry": { + "name": "people_directory_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department": { + "name": "department", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_external_id": { + "name": "manager_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "people_directory_entry_source_id_idx": { + "name": "people_directory_entry_source_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "people_directory_entry_email_idx": { + "name": "people_directory_entry_email_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "people_directory_entry_manager_idx": { + "name": "people_directory_entry_manager_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "manager_external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "people_directory_entry_organization_id_people_directory_sync_organization_id_fk": { + "name": "people_directory_entry_organization_id_people_directory_sync_organization_id_fk", + "tableFrom": "people_directory_entry", + "tableTo": "people_directory_sync", + "columnsFrom": ["organization_id"], + "columnsTo": ["organization_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.people_directory_page": { + "name": "people_directory_page", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_hash": { + "name": "page_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "people_directory_page_organization_id_people_directory_sync_organization_id_fk": { + "name": "people_directory_page_organization_id_people_directory_sync_organization_id_fk", + "tableFrom": "people_directory_page", + "tableTo": "people_directory_sync", + "columnsFrom": ["organization_id"], + "columnsTo": ["organization_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "people_directory_page_organization_id_run_id_page_hash_pk": { + "name": "people_directory_page_organization_id_run_id_page_hash_pk", + "columns": ["organization_id", "run_id", "page_hash"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.people_directory_scim_change": { + "name": "people_directory_scim_change", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "changed_at": { + "name": "changed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "people_directory_scim_change_due_idx": { + "name": "people_directory_scim_change_due_idx", + "columns": [ + { + "expression": "changed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "people_directory_scim_change_organization_id_organization_id_fk": { + "name": "people_directory_scim_change_organization_id_organization_id_fk", + "tableFrom": "people_directory_scim_change", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.people_directory_stage": { + "name": "people_directory_stage", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proposed_id": { + "name": "proposed_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "department": { + "name": "department", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "manager_external_id": { + "name": "manager_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reports_cursor": { + "name": "reports_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reports_complete": { + "name": "reports_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "people_directory_stage_email_idx": { + "name": "people_directory_stage_email_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "people_directory_stage_pending_idx": { + "name": "people_directory_stage_pending_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "NOT \"people_directory_stage\".\"reports_complete\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "people_directory_stage_organization_id_people_directory_sync_organization_id_fk": { + "name": "people_directory_stage_organization_id_people_directory_sync_organization_id_fk", + "tableFrom": "people_directory_stage", + "tableTo": "people_directory_sync", + "columnsFrom": ["organization_id"], + "columnsTo": ["organization_id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "people_directory_stage_organization_id_run_id_external_id_pk": { + "name": "people_directory_stage_organization_id_run_id_external_id_pk", + "columns": ["organization_id", "run_id", "external_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.people_directory_sync": { + "name": "people_directory_sync", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_target": { + "name": "source_target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_credential": { + "name": "encrypted_credential", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "configured_by": { + "name": "configured_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_started_at": { + "name": "run_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "users_read": { + "name": "users_read", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "people_directory_sync_due_idx": { + "name": "people_directory_sync_due_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "people_directory_sync_organization_id_organization_id_fk": { + "name": "people_directory_sync_organization_id_organization_id_fk", + "tableFrom": "people_directory_sync", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "people_directory_sync_configured_by_user_id_fk": { + "name": "people_directory_sync_configured_by_user_id_fk", + "tableFrom": "people_directory_sync", + "tableTo": "user", + "columnsFrom": ["configured_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_access_request": { + "name": "permission_access_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_key": { + "name": "target_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_id": { + "name": "membership_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by": { + "name": "decided_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "permission_access_request_pending_unique": { + "name": "permission_access_request_pending_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"permission_access_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_org_queue_idx": { + "name": "permission_access_request_org_queue_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_access_request_requester_idx": { + "name": "permission_access_request_requester_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_access_request_organization_id_organization_id_fk": { + "name": "permission_access_request_organization_id_organization_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_requester_id_user_id_fk": { + "name": "permission_access_request_requester_id_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["requester_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_access_request_decided_by_user_id_fk": { + "name": "permission_access_request_decided_by_user_id_fk", + "tableFrom": "permission_access_request", + "tableTo": "user", + "columnsFrom": ["decided_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "permission_access_request_status_check": { + "name": "permission_access_request_status_check", + "value": "\"permission_access_request\".\"status\" in ('pending', 'fulfilled', 'declined', 'cancelled', 'closed')" + } + }, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "membership_mode": { + "name": "membership_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'inherit'" + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pinned_item": { + "name": "pinned_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned_at": { + "name": "pinned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pinned_item_user_workspace_idx": { + "name": "pinned_item_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_resource_idx": { + "name": "pinned_item_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pinned_item_user_resource_unique": { + "name": "pinned_item_user_resource_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pinned_item_user_id_user_id_fk": { + "name": "pinned_item_user_id_user_id_fk", + "tableFrom": "pinned_item", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pinned_item_workspace_id_workspace_id_fk": { + "name": "pinned_item_workspace_id_workspace_id_fk", + "tableFrom": "pinned_item", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_organization_archive_id_idx": { + "name": "project_organization_archive_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_owner_archive_id_idx": { + "name": "project_owner_archive_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "project_owner_id_user_id_fk": { + "name": "project_owner_id_user_id_fk", + "tableFrom": "project", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_id_organization_scope_unique": { + "name": "project_id_organization_scope_unique", + "nullsNotDistinct": false, + "columns": ["id", "organization_scope_key"] + } + }, + "policies": {}, + "checkConstraints": { + "project_name_length": { + "name": "project_name_length", + "value": "char_length(btrim(\"project\".\"name\")) BETWEEN 1 AND 100" + } + }, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_entity_id_idx": { + "name": "public_share_entity_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "public_share_entity_binding_check": { + "name": "public_share_entity_binding_check", + "value": "\n (\"public_share\".\"entity_type\" IS NULL AND \"public_share\".\"entity_id\" IS NULL AND \"public_share\".\"workspace_id\" IS NOT NULL)\n OR (\"public_share\".\"entity_type\" IS NOT NULL AND \"public_share\".\"entity_id\" IS NOT NULL AND char_length(\"public_share\".\"entity_id\") > 0\n AND ((\"public_share\".\"entity_type\" = 'workspace' AND \"public_share\".\"workspace_id\" IS NOT NULL AND \"public_share\".\"entity_id\" = \"public_share\".\"workspace_id\")\n OR (\"public_share\".\"entity_type\" = 'project' AND \"public_share\".\"workspace_id\" IS NULL AND \"public_share\".\"resource_type\" = 'file')))\n " + } + }, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "blocked_until": { + "name": "blocked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capacity_state": { + "name": "capacity_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resource_policy": { + "name": "resource_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "document": { + "name": "document", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resource_policy_organization_id_idx": { + "name": "resource_policy_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_resource_unique": { + "name": "resource_policy_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resource_policy_workspace_id_idx": { + "name": "resource_policy_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resource_policy_workspace_id_workspace_id_fk": { + "name": "resource_policy_workspace_id_workspace_id_fk", + "tableFrom": "resource_policy", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_organization_id_organization_id_fk": { + "name": "resource_policy_organization_id_organization_id_fk", + "tableFrom": "resource_policy", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "resource_policy_created_by_user_id_fk": { + "name": "resource_policy_created_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "resource_policy_updated_by_user_id_fk": { + "name": "resource_policy_updated_by_user_id_fk", + "tableFrom": "resource_policy", + "tableTo": "user", + "columnsFrom": ["updated_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "resource_policy_owner_check": { + "name": "resource_policy_owner_check", + "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1" + } + }, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_image": { + "name": "sandbox_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sandbox_image_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "image_ref": { + "name": "image_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_image_id": { + "name": "provider_image_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialization_generation": { + "name": "materialization_generation", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_detail": { + "name": "error_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_image_provider_spec_unique": { + "name": "sandbox_image_provider_spec_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_status_idx": { + "name": "sandbox_image_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sandbox_image_last_used_idx": { + "name": "sandbox_image_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection": { + "name": "scim_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconcile_lock_token": { + "name": "reconcile_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconcile_lease_at": { + "name": "reconcile_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_connection_organization_unique": { + "name": "scim_connection_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_connection_reconcile_due_idx": { + "name": "scim_connection_reconcile_due_idx", + "columns": [ + { + "expression": "reconciled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_connection_organization_id_organization_id_fk": { + "name": "scim_connection_organization_id_organization_id_fk", + "tableFrom": "scim_connection", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_connection_created_by_user_id_fk": { + "name": "scim_connection_created_by_user_id_fk", + "tableFrom": "scim_connection", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_credential": { + "name": "scim_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_credential_token_hash_unique": { + "name": "scim_credential_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_credential_connection_idx": { + "name": "scim_credential_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_credential_connection_id_scim_connection_id_fk": { + "name": "scim_credential_connection_id_scim_connection_id_fk", + "tableFrom": "scim_credential", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_credential_revoked_by_user_id_fk": { + "name": "scim_credential_revoked_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["revoked_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scim_credential_created_by_user_id_fk": { + "name": "scim_credential_created_by_user_id_fk", + "tableFrom": "scim_credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_connection_display_name_unique": { + "name": "scim_group_connection_display_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_external_id_unique": { + "name": "scim_group_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_connection_order_idx": { + "name": "scim_group_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_connection_id_scim_connection_id_fk": { + "name": "scim_group_connection_id_scim_connection_id_fk", + "tableFrom": "scim_group", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_mapping": { + "name": "scim_group_mapping", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_mapping_permission_group_idx": { + "name": "scim_group_mapping_permission_group_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_workspace_idx": { + "name": "scim_group_mapping_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_mapping_group_target_unique": { + "name": "scim_group_mapping_group_target_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_mapping_group_id_scim_group_id_fk": { + "name": "scim_group_mapping_group_id_scim_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_permission_group_id_permission_group_id_fk": { + "name": "scim_group_mapping_permission_group_id_permission_group_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_workspace_id_workspace_id_fk": { + "name": "scim_group_mapping_workspace_id_workspace_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_mapping_created_by_user_id_fk": { + "name": "scim_group_mapping_created_by_user_id_fk", + "tableFrom": "scim_group_mapping", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scim_group_mapping_target_shape": { + "name": "scim_group_mapping_target_shape", + "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )" + } + }, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_group_member_group_user_unique": { + "name": "scim_group_member_group_user_unique", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_group_member_scim_user_idx": { + "name": "scim_group_member_scim_user_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_kind": { + "name": "target_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "baseline_permission": { + "name": "baseline_permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'directory'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_projection_grant_user_target_unique": { + "name": "scim_projection_grant_user_target_unique", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_projection_grant_connection_idx": { + "name": "scim_projection_grant_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_connection_id_scim_connection_id_fk": { + "name": "scim_projection_grant_connection_id_scim_connection_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": ["scim_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_request_log": { + "name": "scim_request_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scim_type": { + "name": "scim_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_request_log_connection_created_idx": { + "name": "scim_request_log_connection_created_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_request_log_connection_id_scim_connection_id_fk": { + "name": "scim_request_log_connection_id_scim_connection_id_fk", + "tableFrom": "scim_request_log", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_connection_user_unique": { + "name": "scim_user_connection_user_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_user_name_unique": { + "name": "scim_user_connection_user_name_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_external_id_unique": { + "name": "scim_user_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "external_id is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_connection_order_idx": { + "name": "scim_user_connection_order_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_user_idx": { + "name": "scim_user_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_connection_id_scim_connection_id_fk": { + "name": "scim_user_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user_tombstone": { + "name": "scim_user_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scim_user_tombstone_connection_external_id_unique": { + "name": "scim_user_tombstone_connection_external_id_unique", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scim_user_tombstone_user_idx": { + "name": "scim_user_tombstone_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_tombstone_connection_id_scim_connection_id_fk": { + "name": "scim_user_tombstone_connection_id_scim_connection_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "scim_connection", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_user_tombstone_user_id_user_id_fk": { + "name": "scim_user_tombstone_user_id_user_id_fk", + "tableFrom": "scim_user_tombstone", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secret_usage": { + "name": "secret_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_name": { + "name": "secret_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_scope": { + "name": "secret_scope", + "type": "secret_usage_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "secret_owner_user_id": { + "name": "secret_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "source": { + "name": "source", + "type": "secret_usage_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "usage_date": { + "name": "usage_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "last_execution_id": { + "name": "last_execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_trigger": { + "name": "last_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secret_usage_bucket_unique": { + "name": "secret_usage_bucket_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secret_usage_secret_recent_idx": { + "name": "secret_usage_secret_recent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "secret_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secret_usage_workspace_id_workspace_id_fk": { + "name": "secret_usage_workspace_id_workspace_id_fk", + "tableFrom": "secret_usage", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "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", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "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 + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_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" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_focus_on_click": { + "name": "auto_focus_on_click", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_link": { + "name": "sim_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_name": { + "name": "normalized_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_url": { + "name": "default_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unlisted": { + "name": "unlisted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "restrict_edit": { + "name": "restrict_edit", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "visit_count": { + "name": "visit_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_visited_at": { + "name": "last_visited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sim_link_organization_name_unique": { + "name": "sim_link_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sim_link_created_by_idx": { + "name": "sim_link_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sim_link_organization_id_organization_id_fk": { + "name": "sim_link_organization_id_organization_id_fk", + "tableFrom": "sim_link", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sim_link_created_by_user_id_fk": { + "name": "sim_link_created_by_user_id_fk", + "tableFrom": "sim_link", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": ["workflow_id", "block_id", "scope_key"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill_member": { + "name": "skill_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "skill_id": { + "name": "skill_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_member_user_id_idx": { + "name": "skill_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_member_unique": { + "name": "skill_member_unique", + "columns": [ + { + "expression": "skill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_member_skill_id_skill_id_fk": { + "name": "skill_member_skill_id_skill_id_fk", + "tableFrom": "skill_member", + "tableTo": "skill", + "columnsFrom": ["skill_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_user_id_user_id_fk": { + "name": "skill_member_user_id_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_member_invited_by_user_id_fk": { + "name": "skill_member_invited_by_user_id_fk", + "tableFrom": "skill_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_app": { + "name": "slack_app", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_client_secret": { + "name": "encrypted_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_signing_secret": { + "name": "encrypted_signing_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "slack_app_organization_id_organization_id_fk": { + "name": "slack_app_organization_id_organization_id_fk", + "tableFrom": "slack_app", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "slack_app_owner_check": { + "name": "slack_app_owner_check", + "value": "(\"slack_app\".\"kind\" = 'custom' AND \"slack_app\".\"organization_id\" IS NOT NULL) OR (\"slack_app\".\"kind\" = 'shared' AND \"slack_app\".\"organization_id\" IS NULL)" + }, + "slack_app_custom_credentials_check": { + "name": "slack_app_custom_credentials_check", + "value": "\"slack_app\".\"kind\" = 'shared' OR (\"slack_app\".\"client_id\" IS NOT NULL AND \"slack_app\".\"encrypted_client_secret\" IS NOT NULL AND \"slack_app\".\"encrypted_signing_secret\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.slack_search_installation": { + "name": "slack_search_installation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_app_id": { + "name": "slack_app_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_name": { + "name": "team_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bot_user_id": { + "name": "bot_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credential_version": { + "name": "credential_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_outcome": { + "name": "last_outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_installation_organization_idx": { + "name": "slack_search_installation_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_credential_unique": { + "name": "slack_search_installation_credential_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_app_team_unique": { + "name": "slack_search_installation_app_team_unique", + "columns": [ + { + "expression": "app_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_installation_active_team_unique": { + "name": "slack_search_installation_active_team_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_installation\".\"enabled\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_installation_organization_id_organization_id_fk": { + "name": "slack_search_installation_organization_id_organization_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_credential_id_credential_id_fk": { + "name": "slack_search_installation_credential_id_credential_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "slack_search_installation_slack_app_id_slack_app_id_fk": { + "name": "slack_search_installation_slack_app_id_slack_app_id_fk", + "tableFrom": "slack_search_installation", + "tableTo": "slack_app", + "columnsFrom": ["slack_app_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_search_turn": { + "name": "slack_search_turn", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "slack_search_turn_ordinal_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_key": { + "name": "conversation_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "lease_id": { + "name": "lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "slack_search_turn_event_unique": { + "name": "slack_search_turn_event_unique", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_pending_idx": { + "name": "slack_search_turn_pending_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_thread_idx": { + "name": "slack_search_turn_thread_idx", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "slack_search_turn_active_thread_unique": { + "name": "slack_search_turn_active_thread_unique", + "columns": [ + { + "expression": "conversation_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"slack_search_turn\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "slack_search_turn_installation_id_slack_search_installation_id_fk": { + "name": "slack_search_turn_installation_id_slack_search_installation_id_fk", + "tableFrom": "slack_search_turn", + "tableTo": "slack_search_installation", + "columnsFrom": ["installation_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_domain": { + "name": "sso_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "primary_provider_id": { + "name": "primary_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sso_domain_domain_idx": { + "name": "sso_domain_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_org_domain_unique": { + "name": "sso_domain_org_domain_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_domain_verified_unique": { + "name": "sso_domain_verified_unique", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "status = 'verified'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_domain_organization_id_organization_id_fk": { + "name": "sso_domain_organization_id_organization_id_fk", + "tableFrom": "sso_domain", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_domain_created_by_user_id_fk": { + "name": "sso_domain_created_by_user_id_fk", + "tableFrom": "sso_domain", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "jit_provisioning_enabled": { + "name": "jit_provisioning_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "last_closed_period_start": { + "name": "last_closed_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscription_cycle_close_lagging_idx": { + "name": "subscription_cycle_close_lagging_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_capability_governed_user_id_user_id_fk": { + "name": "table_row_executions_capability_governed_user_id_user_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": ["row_id", "group_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capability_governed_user_id": { + "name": "capability_governed_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_governed_active_idx": { + "name": "table_run_dispatches_governed_active_idx", + "columns": [ + { + "expression": "capability_governed_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "table_run_dispatches_capability_governed_user_id_user_id_fk": { + "name": "table_run_dispatches_capability_governed_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": ["capability_governed_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_views": { + "name": "table_views", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_views_table_created_idx": { + "name": "table_views_table_created_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_workspace_created_idx": { + "name": "table_views_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_views_table_default_unique": { + "name": "table_views_table_default_unique", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_views_table_id_user_table_definitions_id_fk": { + "name": "table_views_table_id_user_table_definitions_id_fk", + "tableFrom": "table_views", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_workspace_id_workspace_id_fk": { + "name": "table_views_workspace_id_workspace_id_fk", + "tableFrom": "table_views", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_views_created_by_user_id_fk": { + "name": "table_views_created_by_user_id_fk", + "tableFrom": "table_views", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.upload_session": { + "name": "upload_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "upload_session_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "upload_session_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "storage_context": { + "name": "storage_context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "final_key": { + "name": "final_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_provider": { + "name": "storage_provider", + "type": "upload_session_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_object_version": { + "name": "provider_object_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "part_count": { + "name": "part_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "upload_session_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'uploading'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "processing_lease_id": { + "name": "processing_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processing_lease_expires_at": { + "name": "processing_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_file_id": { + "name": "completed_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "upload_session_token_hash_unique": { + "name": "upload_session_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_final_key_unique": { + "name": "upload_session_final_key_unique", + "columns": [ + { + "expression": "final_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "upload_session_status_expires_at_idx": { + "name": "upload_session_status_expires_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_cost_idx": { + "name": "usage_log_billing_entity_period_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_created_at_cost_idx": { + "name": "usage_log_billing_entity_created_at_cost_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_cost_idx": { + "name": "usage_log_workspace_created_at_cost_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "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 + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "suspension_source": { + "name": "suspension_source", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_email_lower_idx": { + "name": "user_email_lower_idx", + "columns": [ + { + "expression": "lower(btrim(\"email\"))", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_gmail_mailbox_idx": { + "name": "user_gmail_mailbox_idx", + "columns": [ + { + "expression": "(CASE WHEN split_part(lower(btrim(\"email\")), '@', 2) IN ('gmail.com', 'googlemail.com') THEN replace(split_part(split_part(lower(btrim(\"email\")), '@', 1), '+', 1), '.', '') || '@gmail.com' END)", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "(CASE WHEN split_part(lower(btrim(\"user\".\"email\")), '@', 2) IN ('gmail.com', 'googlemail.com') THEN replace(split_part(split_part(lower(btrim(\"user\".\"email\")), '@', 1), '+', 1), '.', '') || '@gmail.com' END) IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": ["normalized_email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "free_tier_eligible": { + "name": "free_tier_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "schema_locked": { + "name": "schema_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "insert_locked": { + "name": "insert_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "update_locked": { + "name": "update_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "delete_locked": { + "name": "delete_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_folder_id_idx": { + "name": "user_table_def_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_created_by_idx": { + "name": "user_table_def_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_folder_id_folder_id_fk": { + "name": "user_table_definitions_folder_id_folder_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_changes": { + "name": "user_table_row_changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_table_row_changes_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_delta": { + "name": "row_delta", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "user_table_row_changes_table_id_idx": { + "name": "user_table_row_changes_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_row_changes_table_id_user_table_definitions_id_fk": { + "name": "user_table_row_changes_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_row_changes", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_row_secret_provenance": { + "name": "user_table_row_secret_provenance", + "schema": "", + "columns": { + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": { + "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk", + "tableFrom": "user_table_row_secret_provenance", + "tableTo": "user_table_rows", + "columnsFrom": ["row_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "user_table_row_secret_provenance_status_check": { + "name": "user_table_row_secret_provenance_status_check", + "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_created_by_idx": { + "name": "user_table_rows_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_rows\".\"created_by\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_created_id_idx": { + "name": "user_table_rows_table_created_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": ["table_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "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": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_status": { + "name": "registration_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "registration_generation": { + "name": "registration_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "config_fingerprint": { + "name": "config_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prepared_at": { + "name": "prepared_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_active_registration_unique": { + "name": "webhook_active_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_candidate_registration_unique": { + "name": "webhook_candidate_registration_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_registration_status_generation_idx": { + "name": "webhook_registration_status_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "registration_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_registration_status_check": { + "name": "webhook_registration_status_check", + "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')" + }, + "webhook_registration_generation_check": { + "name": "webhook_registration_generation_check", + "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.webhook_path_claim": { + "name": "webhook_path_claim", + "schema": "", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_path_claim_workflow_idx": { + "name": "webhook_path_claim_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_path_claim_workflow_id_workflow_id_fk": { + "name": "webhook_path_claim_workflow_id_workflow_id_fk", + "tableFrom": "webhook_path_claim", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_path_claim_generation_check": { + "name": "webhook_path_claim_generation_check", + "value": "\"webhook_path_claim\".\"generation\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "fork_sync_excluded": { + "name": "fork_sync_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_active_workspace_sort_idx": { + "name": "workflow_active_workspace_sort_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_folder_id_fk": { + "name": "workflow_folder_id_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "error_enabled": { + "name": "error_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retry": { + "name": "retry", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_operation": { + "name": "workflow_deployment_operation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_active_version_id": { + "name": "previous_active_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "protocol_version": { + "name": "protocol_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preparing'" + }, + "component_readiness": { + "name": "component_readiness", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_deployment_operation_workflow_generation_unique": { + "name": "workflow_deployment_operation_workflow_generation_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_idempotency_unique": { + "name": "workflow_deployment_operation_workflow_idempotency_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_in_flight_unique": { + "name": "workflow_deployment_operation_workflow_in_flight_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_status_idx": { + "name": "workflow_deployment_operation_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_deployment_version_idx": { + "name": "workflow_deployment_operation_deployment_version_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_operation_workflow_version_generation_idx": { + "name": "workflow_deployment_operation_workflow_version_generation_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "generation", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_operation_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_operation_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_deployment_operation", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["previous_active_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_deployment_operation_action_check": { + "name": "workflow_deployment_operation_action_check", + "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')" + }, + "workflow_deployment_operation_status_check": { + "name": "workflow_deployment_operation_status_check", + "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')" + }, + "workflow_deployment_operation_generation_check": { + "name": "workflow_deployment_operation_generation_check", + "value": "\"workflow_deployment_operation\".\"generation\" > 0" + }, + "workflow_deployment_operation_protocol_version_check": { + "name": "workflow_deployment_operation_protocol_version_check", + "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "execution_deadline_at": { + "name": "execution_deadline_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_activity_level_idx": { + "name": "workflow_execution_logs_workspace_activity_level_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "total_duration_ms", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_deadline_idx": { + "name": "workflow_execution_logs_running_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_started_at_idx": { + "name": "workflow_execution_logs_redacting_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'redacting'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_redacting_deadline_idx": { + "name": "workflow_execution_logs_redacting_deadline_idx", + "columns": [ + { + "expression": "execution_deadline_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_scope": { + "name": "secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "mounted_secrets": { + "name": "mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_budget_usd": { + "name": "run_budget_usd", + "type": "numeric(12, 4)", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": { + "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_operation", + "columnsFrom": ["deployment_operation_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test": { + "name": "workflow_test", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_file_id": { + "name": "body_file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cases": { + "name": "cases", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_test_workspace_name_unique": { + "name": "workflow_test_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_test\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_test_body_file_unique": { + "name": "workflow_test_body_file_unique", + "columns": [ + { + "expression": "body_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_workspace_id_workspace_id_fk": { + "name": "workflow_test_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_body_file_id_workspace_files_id_fk": { + "name": "workflow_test_body_file_id_workspace_files_id_fk", + "tableFrom": "workflow_test", + "tableTo": "workspace_files", + "columnsFrom": ["body_file_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_test_created_by_user_id_user_id_fk": { + "name": "workflow_test_created_by_user_id_user_id_fk", + "tableFrom": "workflow_test", + "tableTo": "user", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_test_run": { + "name": "workflow_test_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "test_id": { + "name": "test_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "passed": { + "name": "passed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skipped": { + "name": "skipped", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ran_against": { + "name": "ran_against", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "triggered_by_actor": { + "name": "triggered_by_actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_test_run_test_version_started_idx": { + "name": "workflow_test_run_test_version_started_idx", + "columns": [ + { + "expression": "test_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_test_run_test_id_workflow_test_id_fk": { + "name": "workflow_test_run_test_id_workflow_test_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workflow_test", + "columnsFrom": ["test_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_workspace_id_workspace_id_fk": { + "name": "workflow_test_run_workspace_id_workspace_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_test_run_triggered_by_user_id_user_id_fk": { + "name": "workflow_test_run_triggered_by_user_id_user_id_fk", + "tableFrom": "workflow_test_run", + "tableTo": "user", + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workflow_test_run_version_check": { + "name": "workflow_test_run_version_check", + "value": "\"workflow_test_run\".\"version\" IN ('draft', 'deployed')" + }, + "workflow_test_run_status_check": { + "name": "workflow_test_run_status_check", + "value": "\"workflow_test_run\".\"status\" IN ('running', 'passed', 'failed', 'error')" + }, + "workflow_test_run_completed_check": { + "name": "workflow_test_run_completed_check", + "value": "(\"workflow_test_run\".\"status\" = 'running') = (\"workflow_test_run\".\"completed_at\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_scope_key": { + "name": "organization_scope_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE WHEN organization_id IS NULL THEN 'personal' ELSE 'organization:' || organization_id END", + "type": "stored" + } + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_secret_scope": { + "name": "inbox_secret_scope", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "inbox_mounted_secrets": { + "name": "inbox_mounted_secrets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fork_sync_new_workflows_excluded": { + "name": "fork_sync_new_workflows_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_billed_account_user_id_idx": { + "name": "workspace_billed_account_user_id_idx", + "columns": [ + { + "expression": "billed_account_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_project_id_id_idx": { + "name": "workspace_project_id_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_inbox_provider_id_idx": { + "name": "workspace_inbox_provider_id_idx", + "columns": [ + { + "expression": "inbox_provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_id_project_id_fk": { + "name": "workspace_project_id_project_id_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_project_organization_fk": { + "name": "workspace_project_organization_fk", + "tableFrom": "workspace", + "tableTo": "project", + "columnsFrom": ["project_id", "organization_scope_key"], + "columnsTo": ["id", "organization_scope_key"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_fork_project_fk": { + "name": "workspace_fork_project_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": ["forked_from_workspace_id", "project_id"], + "columnsTo": ["id", "project_id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_id_project_unique": { + "name": "workspace_id_project_unique", + "nullsNotDistinct": false, + "columns": ["id", "project_id"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_uploaded_by_idx": { + "name": "workspace_file_uploaded_by_idx", + "columns": [ + { + "expression": "uploaded_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_collab_state": { + "name": "workspace_file_collab_state", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "doc_state": { + "name": "doc_state", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_collab_state_file_id_workspace_files_id_fk": { + "name": "workspace_file_collab_state_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_collab_state", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_backfill": { + "name": "workspace_file_search_backfill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "after_workspace_id": { + "name": "after_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_entity_type": { + "name": "after_entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_entity_id": { + "name": "after_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "after_file_id": { + "name": "after_file_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_build": { + "name": "workspace_file_search_build", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "artifact_key": { + "name": "artifact_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_file_search_build_file_idx": { + "name": "workspace_file_search_build_file_idx", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_build_cleanup_idx": { + "name": "workspace_file_search_build_cleanup_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_build\".\"expires_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_build_owner_check": { + "name": "file_search_build_owner_check", + "value": "(\"workspace_file_search_build\".\"entity_type\" IS NULL AND \"workspace_file_search_build\".\"entity_id\" IS NULL AND \"workspace_file_search_build\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_build\".\"entity_type\" = 'workspace' AND \"workspace_file_search_build\".\"entity_id\" = \"workspace_file_search_build\".\"workspace_id\") OR (\"workspace_file_search_build\".\"entity_type\" = 'project' AND length(\"workspace_file_search_build\".\"entity_id\") > 0 AND \"workspace_file_search_build\".\"workspace_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_chunk": { + "name": "workspace_file_search_chunk", + "schema": "", + "columns": { + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_start": { + "name": "line_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fragment": { + "name": "fragment", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "overlap": { + "name": "overlap", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "file_search_chunk_owner_content_idx": { + "name": "file_search_chunk_owner_content_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_chunk\".\"entity_type\" = 'project'", + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "workspace_file_search_chunk_line_idx": { + "name": "workspace_file_search_chunk_line_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "line_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_chunk_content_idx": { + "name": "workspace_file_search_chunk_content_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + } + }, + "foreignKeys": { + "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_chunk_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_chunk", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_chunk_pk": { + "name": "workspace_file_search_chunk_pk", + "columns": ["build_id", "ordinal"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_chunk_owner_check": { + "name": "file_search_chunk_owner_check", + "value": "(\"workspace_file_search_chunk\".\"entity_type\" IS NULL AND \"workspace_file_search_chunk\".\"entity_id\" IS NULL AND \"workspace_file_search_chunk\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_chunk\".\"entity_type\" = 'workspace' AND \"workspace_file_search_chunk\".\"entity_id\" = \"workspace_file_search_chunk\".\"workspace_id\") OR (\"workspace_file_search_chunk\".\"entity_type\" = 'project' AND length(\"workspace_file_search_chunk\".\"entity_id\") > 0 AND \"workspace_file_search_chunk\".\"workspace_id\" IS NULL)" + }, + "workspace_file_search_chunk_content_size": { + "name": "workspace_file_search_chunk_content_size", + "value": "octet_length(\"workspace_file_search_chunk\".\"content\") <= 8192" + }, + "workspace_file_search_chunk_position": { + "name": "workspace_file_search_chunk_position", + "value": "\"workspace_file_search_chunk\".\"ordinal\" >= 0 AND \"workspace_file_search_chunk\".\"line_start\" > 0 AND \"workspace_file_search_chunk\".\"overlap\" BETWEEN 0 AND 2" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_dispatch_queue": { + "name": "workspace_file_search_dispatch_queue", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "enqueued_at": { + "name": "enqueued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_dispatched_at": { + "name": "last_dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_dispatch_queue_schedule_idx": { + "name": "workspace_file_search_dispatch_queue_schedule_idx", + "columns": [ + { + "expression": "last_dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "first" + }, + { + "expression": "enqueued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_queue_workspace_fk": { + "name": "workspace_file_search_queue_workspace_fk", + "tableFrom": "workspace_file_search_dispatch_queue", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_index": { + "name": "workspace_file_search_index", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "partial": { + "name": "partial", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_search_index_workspace_status_idx": { + "name": "workspace_file_search_index_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_pending_dispatch_idx": { + "name": "workspace_file_search_index_pending_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_index_active_dispatch_idx": { + "name": "workspace_file_search_index_active_dispatch_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_index_file_fk": { + "name": "workspace_file_search_index_file_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_index_workspace_fk": { + "name": "workspace_file_search_index_workspace_fk", + "tableFrom": "workspace_file_search_index", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_index_pk": { + "name": "workspace_file_search_index_pk", + "columns": ["file_id", "source_content_updated_at"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_search_revision": { + "name": "workspace_file_search_revision", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "workspace_file_search_index_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "line_count": { + "name": "line_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexed_bytes": { + "name": "indexed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "handoff_expires_at": { + "name": "handoff_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "file_search_revision_owner_pending_idx": { + "name": "file_search_revision_owner_pending_idx", + "columns": [ + { + "expression": "coalesce(\"entity_type\", 'workspace')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"entity_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "file_search_revision_owner_status_idx": { + "name": "file_search_revision_owner_status_idx", + "columns": [ + { + "expression": "coalesce(\"entity_type\", 'workspace')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"entity_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_workspace_status_idx": { + "name": "workspace_file_search_revision_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_build_idx": { + "name": "workspace_file_search_revision_build_idx", + "columns": [ + { + "expression": "build_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_pending_idx": { + "name": "workspace_file_search_revision_pending_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_revision_active_idx": { + "name": "workspace_file_search_revision_active_idx", + "columns": [ + { + "expression": "dispatched_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_search_revision\".\"status\" = 'pending' AND \"workspace_file_search_revision\".\"dispatched_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_revision_file_id_workspace_files_id_fk": { + "name": "workspace_file_search_revision_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk": { + "name": "workspace_file_search_revision_build_id_workspace_file_search_build_id_fk", + "tableFrom": "workspace_file_search_revision", + "tableTo": "workspace_file_search_build", + "columnsFrom": ["build_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "file_search_revision_owner_check": { + "name": "file_search_revision_owner_check", + "value": "(\"workspace_file_search_revision\".\"entity_type\" IS NULL AND \"workspace_file_search_revision\".\"entity_id\" IS NULL AND \"workspace_file_search_revision\".\"workspace_id\" IS NOT NULL) OR (\"workspace_file_search_revision\".\"entity_type\" = 'workspace' AND \"workspace_file_search_revision\".\"entity_id\" = \"workspace_file_search_revision\".\"workspace_id\") OR (\"workspace_file_search_revision\".\"entity_type\" = 'project' AND length(\"workspace_file_search_revision\".\"entity_id\") > 0 AND \"workspace_file_search_revision\".\"workspace_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_search_segment": { + "name": "workspace_file_search_segment", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_content_updated_at": { + "name": "source_content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "line_number": { + "name": "line_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_number": { + "name": "segment_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "segment_start": { + "name": "segment_start", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "line_length": { + "name": "line_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "workspace_file_search_segment_workspace_revision_idx": { + "name": "workspace_file_search_segment_workspace_revision_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_content_updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_search_segment_workspace_content_trgm_idx": { + "name": "workspace_file_search_segment_workspace_content_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "content", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_search_segment_file_fk": { + "name": "workspace_file_search_segment_file_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_search_segment_workspace_fk": { + "name": "workspace_file_search_segment_workspace_fk", + "tableFrom": "workspace_file_search_segment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_file_search_segment_pk": { + "name": "workspace_file_search_segment_pk", + "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_secret_provenance": { + "name": "workspace_file_secret_provenance", + "schema": "", + "columns": { + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_file_secret_provenance_file_id_workspace_files_id_fk": { + "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_secret_provenance", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_secret_provenance_status_check": { + "name": "workspace_file_secret_provenance_status_check", + "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_file_version": { + "name": "workspace_file_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "file_id": { + "name": "file_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "superseded_at": { + "name": "superseded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "workspace_file_version_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_user_ids": { + "name": "author_user_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "restored_from_version": { + "name": "restored_from_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_status": { + "name": "secret_provenance_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_provenance_entries": { + "name": "secret_provenance_entries", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_version_file_version_unique": { + "name": "workspace_file_version_file_version_unique", + "columns": [ + { + "expression": "file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_key_unique": { + "name": "workspace_file_version_key_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_id_idx": { + "name": "workspace_file_version_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_version_workspace_superseded_idx": { + "name": "workspace_file_version_workspace_superseded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "superseded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_version\".\"superseded_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_version_file_id_workspace_files_id_fk": { + "name": "workspace_file_version_file_id_workspace_files_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace_files", + "columnsFrom": ["file_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_version_workspace_id_workspace_id_fk": { + "name": "workspace_file_version_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_version", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_file_version_provenance_status_check": { + "name": "workspace_file_version_provenance_status_check", + "value": "\"workspace_file_version\".\"secret_provenance_status\" IS NULL OR \"workspace_file_version\".\"secret_provenance_status\" IN ('exact', 'unknown', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content_updated_at": { + "name": "content_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "date_trunc('milliseconds', now())" + }, + "secret_provenance_version": { + "name": "secret_provenance_version", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_files_project_id_idx": { + "name": "workspace_files_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_project_folder_name_active_unique": { + "name": "workspace_files_project_folder_name_active_unique", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"project_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_search_owner_keyset_idx": { + "name": "workspace_files_search_owner_keyset_idx", + "columns": [ + { + "expression": "(CASE WHEN \"project_id\" IS NOT NULL THEN 'project' ELSE 'workspace' END)", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"project_id\", \"workspace_id\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND ((\"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL) OR (\"workspace_files\".\"context\" = 'project' AND \"workspace_files\".\"project_id\" IS NOT NULL))", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_active_keyset_idx": { + "name": "workspace_files_workspace_active_keyset_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_active_uploaded_idx": { + "name": "workspace_files_workspace_active_uploaded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_uploaded_idx": { + "name": "workspace_files_workspace_folder_uploaded_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_name_trgm_idx": { + "name": "workspace_files_workspace_name_trgm_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "text_ops" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "gin", + "with": { + "fastupdate": "off" + } + }, + "workspace_files_workspace_display_name_idx": { + "name": "workspace_files_workspace_display_name_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "regexp_replace(regexp_replace(regexp_replace(normalize(\"original_name\", NFC), '^[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+|[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+$', '', 'g'), '[\\x01-\\x1f\\x7f]', '', 'g'), '[ \\t\\n\\v\\f\\r\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+', ' ', 'g')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": true, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_organization_id_idx": { + "name": "workspace_files_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_project_id_project_id_fk": { + "name": "workspace_files_project_id_project_id_fk", + "tableFrom": "workspace_files", + "tableTo": "project", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_organization_id_organization_id_fk": { + "name": "workspace_files_organization_id_organization_id_fk", + "tableFrom": "workspace_files", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_folder_id_fk": { + "name": "workspace_files_folder_id_folder_id_fk", + "tableFrom": "workspace_files", + "tableTo": "folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_files_owner_check": { + "name": "workspace_files_owner_check", + "value": "num_nonnulls(\"workspace_files\".\"workspace_id\", \"workspace_files\".\"project_id\", \"workspace_files\".\"organization_id\") <= 1" + }, + "workspace_files_workspace_binding_check": { + "name": "workspace_files_workspace_binding_check", + "value": "\"workspace_files\".\"context\" NOT IN ('workspace', 'chat', 'mothership', 'execution', 'workspace-logos') OR \"workspace_files\".\"workspace_id\" IS NOT NULL" + }, + "workspace_files_project_binding_check": { + "name": "workspace_files_project_binding_check", + "value": "(\"workspace_files\".\"project_id\" IS NOT NULL) = (\"workspace_files\".\"context\" = 'project') AND (\"workspace_files\".\"project_id\" IS NULL OR \"workspace_files\".\"chat_id\" IS NULL)" + }, + "workspace_files_organization_binding_check": { + "name": "workspace_files_organization_binding_check", + "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_workflow_sync": { + "name": "workspace_fork_workflow_sync", + "schema": "", + "columns": { + "deployment_operation_id": { + "name": "deployment_operation_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workflow_id": { + "name": "source_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_deployment_version_id": { + "name": "source_deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "workspace_fork_workflow_sync_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "promote_run_id": { + "name": "promote_run_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rollback_operation_id": { + "name": "rollback_operation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rolled_back_at": { + "name": "rolled_back_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_fork_workflow_sync_baseline_idx": { + "name": "workspace_fork_workflow_sync_baseline_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"activated_at\" IS NOT NULL AND \"workspace_fork_workflow_sync\".\"rolled_back_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_run_idx": { + "name": "workspace_fork_workflow_sync_run_idx", + "columns": [ + { + "expression": "promote_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_rollback_idx": { + "name": "workspace_fork_workflow_sync_rollback_idx", + "columns": [ + { + "expression": "rollback_operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_fork_workflow_sync\".\"rollback_operation_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_source_idx": { + "name": "workspace_fork_workflow_sync_source_idx", + "columns": [ + { + "expression": "source_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_target_idx": { + "name": "workspace_fork_workflow_sync_target_idx", + "columns": [ + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_workflow_sync_child_workspace_idx": { + "name": "workspace_fork_workflow_sync_child_workspace_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_workflow_sync_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workspace", + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_source_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["source_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk": { + "name": "workspace_fork_workflow_sync_target_workflow_id_workflow_id_fk", + "tableFrom": "workspace_fork_workflow_sync", + "tableTo": "workflow", + "columnsFrom": ["target_workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operation_receipt": { + "name": "workspace_operation_receipt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operation_receipt_request_unique": { + "name": "workspace_operation_receipt_request_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operation_receipt_workspace_created_idx": { + "name": "workspace_operation_receipt_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operation_receipt_workspace_id_workspace_id_fk": { + "name": "workspace_operation_receipt_workspace_id_workspace_id_fk", + "tableFrom": "workspace_operation_receipt", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_sandbox": { + "name": "workspace_sandbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "language": { + "name": "language", + "type": "sandbox_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dependencies": { + "name": "dependencies", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "cli_tools": { + "name": "cli_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_sandbox_workspace_name_unique": { + "name": "workspace_sandbox_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_sandbox_spec_hash_idx": { + "name": "workspace_sandbox_spec_hash_idx", + "columns": [ + { + "expression": "spec_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_sandbox_workspace_id_workspace_id_fk": { + "name": "workspace_sandbox_workspace_id_workspace_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_sandbox_created_by_user_id_fk": { + "name": "workspace_sandbox_created_by_user_id_fk", + "tableFrom": "workspace_sandbox", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_visit": { + "name": "workspace_visit", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visited_at": { + "name": "visited_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_visit_workspace_idx": { + "name": "workspace_visit_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_visit_user_id_user_id_fk": { + "name": "workspace_visit_user_id_user_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_visit_workspace_id_workspace_id_fk": { + "name": "workspace_visit_workspace_id_workspace_id_fk", + "tableFrom": "workspace_visit", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_visit_user_id_workspace_id_pk": { + "name": "workspace_visit_user_id_workspace_id_pk", + "columns": ["user_id", "workspace_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.aws_marketplace_license_status": { + "name": "aws_marketplace_license_status", + "schema": "public", + "values": ["active", "inactive"] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": ["mothership", "copilot"] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] + }, + "public.copilot_tool_permission_decision": { + "name": "copilot_tool_permission_decision", + "schema": "public", + "values": ["allow", "allow_chat", "always_allow", "skip"] + }, + "public.credential_group_enrollment_status": { + "name": "credential_group_enrollment_status", + "schema": "public", + "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"] + }, + "public.credential_group_status": { + "name": "credential_group_status", + "schema": "public", + "values": ["active", "disabled"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "managed_oauth", + "managed_mcp", + "env_workspace", + "env_personal", + "service_account", + "personal_token" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": ["hourly", "daily"] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": [ + "s3", + "gcs", + "azure_blob", + "sentinel", + "datadog", + "bigquery", + "snowflake", + "webhook" + ] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": ["running", "success", "failed"] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": ["cron", "manual"] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": ["execution_log", "paused_snapshot"] + }, + "public.folder_resource_type": { + "name": "folder_resource_type", + "schema": "public", + "values": ["workflow", "file", "knowledge_base", "table"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": ["internal", "external"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.managed_oauth_credential_status": { + "name": "managed_oauth_credential_status", + "schema": "public", + "values": ["active", "needs_reauth", "revoked"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.sandbox_image_status": { + "name": "sandbox_image_status", + "schema": "public", + "values": ["pending", "building", "ready", "failed"] + }, + "public.sandbox_language": { + "name": "sandbox_language", + "schema": "public", + "values": ["javascript", "python"] + }, + "public.secret_usage_scope": { + "name": "secret_usage_scope", + "schema": "public", + "values": ["workspace", "personal"] + }, + "public.secret_usage_source": { + "name": "secret_usage_source", + "schema": "public", + "values": ["workflow", "copilot", "mcp"] + }, + "public.upload_session_method": { + "name": "upload_session_method", + "schema": "public", + "values": ["put", "multipart"] + }, + "public.upload_session_provider": { + "name": "upload_session_provider", + "schema": "public", + "values": ["local", "s3", "blob", "gcs"] + }, + "public.upload_session_purpose": { + "name": "upload_session_purpose", + "schema": "public", + "values": [ + "workspace_file", + "project_file", + "table_import", + "knowledge_import", + "knowledge_document", + "profile_picture", + "workspace_logo", + "organization_logo", + "mothership_attachment", + "execution_attachment" + ] + }, + "public.upload_session_status": { + "name": "upload_session_status", + "schema": "public", + "values": [ + "uploading", + "completing", + "finalizing", + "completed", + "aborting", + "aborted", + "failed", + "expired" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed", "tool", "model_unbilled"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment", + "voice-output", + "api-tool", + "workflow-test" + ] + }, + "public.workspace_file_search_index_status": { + "name": "workspace_file_search_index_status", + "schema": "public", + "values": ["pending", "ready", "skipped", "failed"] + }, + "public.workspace_file_version_source": { + "name": "workspace_file_version_source", + "schema": "public", + "values": ["upload", "user", "api", "copilot", "workflow", "collab", "revert", "unknown"] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": ["push", "pull"] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "file_folder", + "mcp_server", + "workflow_mcp_server", + "custom_block", + "custom_tool", + "skill", + "sandbox" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": ["personal", "organization", "grandfathered_shared"] + } + }, + "schemas": {}, + "sequences": { + "public.copilot_async_tool_calls_persist_seq": { + "name": "copilot_async_tool_calls_persist_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 21557b8f1be..4745537f3b5 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -3172,6 +3172,13 @@ "when": 1793920630365, "tag": "0453_credential_multiple_accounts_per_provider", "breakpoints": true + }, + { + "idx": 454, + "version": "7", + "when": 1793931258443, + "tag": "0454_aws_marketplace_license", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 77e3161f66e..c8052c6b0b4 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1927,6 +1927,48 @@ export const organization = pgTable('organization', { updatedAt: timestamp('updated_at').defaultNow().notNull(), }) +export const awsMarketplaceLicenseStatusEnum = pgEnum('aws_marketplace_license_status', [ + 'active', + 'inactive', +]) + +/** + * An AWS Marketplace SaaS contract license, one row per license ARN: one AWS account can hold + * several concurrent agreements. `status` mirrors the last Entitlement Service read, never a + * contract amount. The buyer's browser holds a one-time claim token whose hash and expiry live + * here until an organization owner or admin links the license to their organization. + */ +export const awsMarketplaceLicense = pgTable( + 'aws_marketplace_license', + { + id: text('id').primaryKey(), + licenseArn: text('license_arn').notNull(), + productCode: text('product_code').notNull(), + customerIdentifier: text('customer_identifier').notNull(), + customerAwsAccountId: text('customer_aws_account_id'), + organizationId: text('organization_id').references(() => organization.id, { + onDelete: 'set null', + }), + status: awsMarketplaceLicenseStatusEnum('status').notNull().default('inactive'), + dimension: text('dimension'), + quantity: integer('quantity'), + expiresAt: timestamp('expires_at'), + lastVerifiedAt: timestamp('last_verified_at').notNull(), + claimTokenHash: text('claim_token_hash'), + claimExpiresAt: timestamp('claim_expires_at'), + linkedByUserId: text('linked_by_user_id').references(() => user.id, { onDelete: 'set null' }), + linkedAt: timestamp('linked_at'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => [ + uniqueIndex('aws_marketplace_license_arn_unique').on(table.licenseArn), + uniqueIndex('aws_marketplace_license_claim_token_hash_unique').on(table.claimTokenHash), + index('aws_marketplace_license_last_verified_at_idx').on(table.lastVerifiedAt), + index('aws_marketplace_license_organization_id_idx').on(table.organizationId), + ] +) + /** Private navigation snapshots, scoped to the person and organization that recorded them. */ export const organizationSearchHistory = pgTable( 'organization_search_history', diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 99a26502fee..b9103dadade 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -14,6 +14,7 @@ const AuditAction = { AUTH_IMPERSONATION_ENDED: 'auth.impersonation_ended', AUTH_SESSIONS_REVOKED: 'auth.sessions_revoked', AUDIT_LOGS_EXPORTED: 'audit_logs.exported', + AWS_MARKETPLACE_LICENSE_LINKED: 'aws_marketplace_license.linked', API_KEY_CREATED: 'api_key.created', API_KEY_UPDATED: 'api_key.updated', API_KEY_REVOKED: 'api_key.revoked', @@ -241,6 +242,7 @@ const AuditAction = { const AuditResourceType = { ACCOUNT: 'account', API_KEY: 'api_key', + AWS_MARKETPLACE_LICENSE: 'aws_marketplace_license', BILLING: 'billing', BYOK_KEY: 'byok_key', CHAT: 'chat', diff --git a/packages/testing/src/mocks/schema-tables.generated.ts b/packages/testing/src/mocks/schema-tables.generated.ts index 5e3dc56dd62..89ad55585b3 100644 --- a/packages/testing/src/mocks/schema-tables.generated.ts +++ b/packages/testing/src/mocks/schema-tables.generated.ts @@ -449,6 +449,25 @@ export const GENERATED_SCHEMA_TABLES = { 'createdAt', 'updatedAt', ], + awsMarketplaceLicense: [ + 'id', + 'licenseArn', + 'productCode', + 'customerIdentifier', + 'customerAwsAccountId', + 'organizationId', + 'status', + 'dimension', + 'quantity', + 'expiresAt', + 'lastVerifiedAt', + 'claimTokenHash', + 'claimExpiresAt', + 'linkedByUserId', + 'linkedAt', + 'createdAt', + 'updatedAt', + ], organizationSearchHistory: ['organizationId', 'userId', 'sources', 'queries'], member: ['id', 'userId', 'organizationId', 'role', 'createdAt'], memberProfile: [ @@ -2250,6 +2269,7 @@ export const GENERATED_SCHEMA_ENUMS = { secretUsageSourceEnum: ['workflow', 'copilot', 'mcp'], billingBlockedReasonEnum: ['payment_failed', 'dispute'], billingEntityTypeEnum: ['user', 'organization'], + awsMarketplaceLicenseStatusEnum: ['active', 'inactive'], invitationKindEnum: ['organization', 'workspace'], invitationMembershipIntentEnum: ['internal', 'external'], invitationStatusEnum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired'], diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 1afbb402217..35dd0d2376d 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -116,6 +116,7 @@ const INDIRECT_ZOD_ROUTES = new Set([ 'apps/sim/app/api/cron/cleanup-oauth-tokens/route.ts', 'apps/sim/app/api/cron/renew-subscriptions/route.ts', 'apps/sim/app/api/cron/billing-cycle-close/route.ts', + 'apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts', 'apps/sim/app/api/cron/reconcile-billing-seats/route.ts', 'apps/sim/app/api/cron/reconcile-inbox-entitlement/route.ts', 'apps/sim/app/api/cron/run-data-drains/route.ts', From 69cd6ac6bf5466f0173b7abfe40c8c104b57e2e2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 18:45:13 -0800 Subject: [PATCH 2/7] feat(billing): register AWS Marketplace licenses to an account and sign Marketplace calls as the task role - POST /api/marketplace/aws/claim/register-account: a session-only application operation for a buyer who administers no organization (or prefers not to pick one). Same guards as linking: an open claim for this license, rechecked under the row lock, and a fresh entitlement read that must be current. Refuses a license already linked to an organization. Records the buyer as linkedBy with no organization, consumes the claim, clears the cookie, audits aws_marketplace_license.registered, and emails the enterprise inbox to create the buyer's organization. Such a license can still be linked later. - Onboarding page: registering is the primary action with no administered organization and a secondary one beside the organization picker; a registered success state. - Marketplace and event-queue clients use the ECS container credential provider when the container credential endpoint is present, since the app env also carries static user keys the default chain would prefer; elsewhere they keep the default chain. --- .../aws/claim/register-account/route.ts | 29 +++ .../aws/aws-marketplace-onboarding.tsx | 130 +++++++----- apps/sim/hooks/queries/aws-marketplace.ts | 12 ++ apps/sim/lib/api/contracts/aws-marketplace.ts | 8 + .../aws-marketplace/application/operations.ts | 6 + .../aws-marketplace/application/use-cases.ts | 189 +++++++++++++----- .../sim/lib/billing/aws-marketplace/client.ts | 33 ++- .../aws-marketplace/lifecycle.integration.ts | 135 ++++++++++++- .../lib/billing/aws-marketplace/repository.ts | 21 +- apps/sim/lib/core/config/env.ts | 2 + apps/sim/package.json | 1 + bun.lock | 1 + packages/audit/src/types.ts | 1 + packages/testing/src/mocks/audit.mock.ts | 1 + 14 files changed, 457 insertions(+), 112 deletions(-) create mode 100644 apps/sim/app/api/marketplace/aws/claim/register-account/route.ts diff --git a/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts b/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts new file mode 100644 index 00000000000..f4cd1f7388b --- /dev/null +++ b/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts @@ -0,0 +1,29 @@ +import { registerAwsMarketplaceLicenseToAccountContract } from '@/lib/api/contracts/aws-marketplace' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { registerAwsMarketplaceLicenseToAccount } from '@/lib/billing/aws-marketplace/application/use-cases' +import { + AWS_MARKETPLACE_CLAIM_COOKIE, + serializeClaimCookie, +} from '@/lib/billing/aws-marketplace/claim' + +export const POST = defineInternalJsonRoute({ + contract: registerAwsMarketplaceLicenseToAccountContract, + operation: registerAwsMarketplaceLicenseToAccount.operation, + useCase: registerAwsMarketplaceLicenseToAccount, + auth: internalSessionAuth, + rateLimit: internalRateLimits.user({ bucketName: 'aws-marketplace-link' }), + errorPolicy: internalOrchestrationErrorPolicy, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, + mapInput: ({ body }, { request }) => ({ + licenseId: body.licenseId, + claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, + }), + present: () => ({ success: true as const }), + /** The claim is spent once the registration commits; the cookie that carried it goes with it. */ + finalizeResponse: () => ({ headers: { 'set-cookie': serializeClaimCookie(null) } }), +}) diff --git a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx index 56552d274cc..c7966ee6f29 100644 --- a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx +++ b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { AuthField, AuthFormMessage, AuthSelect, AuthTag } from '@sim/auth-ui' import { useRouter } from 'next/navigation' import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' -import { client, useSession } from '@/lib/auth/client' +import { useSession } from '@/lib/auth/client' import { AWS_MARKETPLACE_ONBOARDING_PATH, type AwsMarketplaceOnboardingError, @@ -15,6 +15,7 @@ import { InviteLayout, InviteStatusCard } from '@/app/invite/components' import { useAwsMarketplaceClaim, useLinkAwsMarketplaceLicense, + useRegisterAwsMarketplaceLicenseToAccount, } from '@/hooks/queries/aws-marketplace' const RESTART_FROM_AWS = @@ -64,6 +65,7 @@ export function AwsMarketplaceOnboarding({ const viewerId = session?.user?.id ?? null const claimQuery = useAwsMarketplaceClaim(registrationError ? null : viewerId) const linkLicense = useLinkAwsMarketplaceLicense() + const registerToAccount = useRegisterAwsMarketplaceLicenseToAccount() const returnHome = { label: 'Return to Home', onClick: () => router.push('/') } if (registrationError) { @@ -122,6 +124,19 @@ export function AwsMarketplaceOnboarding({ ) } + if (registerToAccount.isSuccess) { + return ( + + router.push(APP_ENTRY_PATH) }]} + /> + + ) + } + if (claimQuery.isPending) { return ( @@ -157,36 +172,22 @@ export function AwsMarketplaceOnboarding({ ) } - if (claim.organizations.length === 0) { - return ( - - { - await client.signOut() - router.push(authLink('/login')) - }, - }, - returnHome, - ]} - /> - - ) - } - + const pending = linkLicense.isPending || registerToAccount.isPending return ( + registering={registerToAccount.isPending} + error={(linkLicense.error ?? registerToAccount.error)?.message ?? null} + onLink={(organizationId) => { + registerToAccount.reset() linkLicense.mutate({ licenseId: claim.license.id, organizationId }) - } + }} + onRegister={() => { + linkLicense.reset() + registerToAccount.mutate({ licenseId: claim.license.id }) + }} onCancel={returnHome.onClick} /> ) @@ -194,13 +195,29 @@ export function AwsMarketplaceOnboarding({ interface LinkLicenseCardProps { claim: Extract + pending: boolean linking: boolean + registering: boolean error: string | null onLink: (organizationId: string) => void + onRegister: () => void onCancel: () => void } -function LinkLicenseCard({ claim, linking, error, onLink, onCancel }: LinkLicenseCardProps) { +/** + * Links the license to an organization the viewer administers, or registers it to their + * account when they administer none or would rather not pick one. + */ +function LinkLicenseCard({ + claim, + pending, + linking, + registering, + error, + onLink, + onRegister, + onCancel, +}: LinkLicenseCardProps) { const { license, organizations } = claim const [selectedOrganizationId, setSelectedOrganizationId] = useState() const linkedOrganization = organizations.find((org) => org.id === license.organizationId) @@ -218,12 +235,28 @@ function LinkLicenseCard({ claim, linking, error, onLink, onCancel }: LinkLicens }, ] + const hasOrganizations = organizations.length > 0 + const registerAction = { + label: hasOrganizations ? 'Register to my account instead' : 'Register to my account', + onClick: onRegister, + disabled: pending, + loading: registering, + } + return (
@@ -240,26 +273,33 @@ function LinkLicenseCard({ claim, linking, error, onLink, onCancel }: LinkLicens
- - ({ label: org.name, value: org.id }))} - value={organizationId} - onChange={setSelectedOrganizationId} - placeholder='Select an organization' - fullWidth - /> - + {hasOrganizations && ( + + ({ label: org.name, value: org.id }))} + value={organizationId} + onChange={setSelectedOrganizationId} + placeholder='Select an organization' + fullWidth + /> + + )} {error && {error}} } actions={[ - { - label: 'Link subscription', - onClick: () => organizationId && onLink(organizationId), - disabled: !organizationId || linking, - loading: linking, - }, + ...(hasOrganizations + ? [ + { + label: 'Link subscription', + onClick: () => organizationId && onLink(organizationId), + disabled: !organizationId || pending, + loading: linking, + }, + ] + : []), + registerAction, { label: 'Not now', onClick: onCancel }, ]} /> diff --git a/apps/sim/hooks/queries/aws-marketplace.ts b/apps/sim/hooks/queries/aws-marketplace.ts index 554b9e4480c..0fe128c7086 100644 --- a/apps/sim/hooks/queries/aws-marketplace.ts +++ b/apps/sim/hooks/queries/aws-marketplace.ts @@ -4,6 +4,7 @@ import type { ContractBodyInput } from '@/lib/api/contracts' import { getAwsMarketplaceClaimContract, linkAwsMarketplaceLicenseContract, + registerAwsMarketplaceLicenseToAccountContract, } from '@/lib/api/contracts/aws-marketplace' const AWS_MARKETPLACE_CLAIM_STALE_TIME = 30 * 1000 @@ -35,3 +36,14 @@ export function useLinkAwsMarketplaceLicense() { }, }) } + +export function useRegisterAwsMarketplaceLicenseToAccount() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (body: ContractBodyInput) => + requestJson(registerAwsMarketplaceLicenseToAccountContract, { body }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: awsMarketplaceKeys.claims() }) + }, + }) +} diff --git a/apps/sim/lib/api/contracts/aws-marketplace.ts b/apps/sim/lib/api/contracts/aws-marketplace.ts index 09b6dde01d1..21c3a57a933 100644 --- a/apps/sim/lib/api/contracts/aws-marketplace.ts +++ b/apps/sim/lib/api/contracts/aws-marketplace.ts @@ -58,3 +58,11 @@ export const linkAwsMarketplaceLicenseContract = defineRouteContract({ schema: z.object({ success: z.literal(true), organizationId: z.string() }), }, }) + +/** Registers the claimed license to the signed-in account when no organization is chosen. */ +export const registerAwsMarketplaceLicenseToAccountContract = defineRouteContract({ + method: 'POST', + path: '/api/marketplace/aws/claim/register-account', + body: z.object({ licenseId: z.string().min(1, 'License ID is required').max(255) }), + response: { mode: 'json', schema: z.object({ success: z.literal(true) }) }, +}) diff --git a/apps/sim/lib/billing/aws-marketplace/application/operations.ts b/apps/sim/lib/billing/aws-marketplace/application/operations.ts index 854a93d4021..ad556c640c6 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/operations.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/operations.ts @@ -12,6 +12,12 @@ export const awsMarketplaceOperations = { principalKinds: ['session'], capability: 'none', }), + // permission-group-exempt: a buyer with no organization takes their own purchase; nothing a group governs is touched. + registerToAccount: defineOperation({ + id: 'aws_marketplace.license.register_account', + principalKinds: ['session'], + capability: 'none', + }), // permission-group-exempt: linking a purchased license is organization administration, not a governed capability. linkLicense: defineOrganizationOperation({ id: 'organization.aws_marketplace_license.link', diff --git a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts index f7a9d70fdef..0230b898941 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts @@ -20,6 +20,7 @@ import { defineAuthorizedOrganizationUseCase, type OrganizationUseCaseContext, } from '@/lib/core/application/authorized-organization-use-case' +import { recordProjectedUseCaseAuditEntries } from '@/lib/core/application/authorized-workspace-use-case' import { assertOperationPrincipal, type OperationUseCase } from '@/lib/core/application/operation' import { requireOrganizationSubjectMembership } from '@/lib/core/application/organization-authorization' import { env } from '@/lib/core/config/env' @@ -62,7 +63,11 @@ function requireOpenClaim( return license } -function requireLinkableTo(license: AwsMarketplaceLicenseRecord, organizationId: string): void { +/** A license linked to one organization is never moved, and never taken without one. */ +function requireLinkableTo( + license: AwsMarketplaceLicenseRecord, + organizationId: string | null +): void { if (license.organizationId && license.organizationId !== organizationId) { throw new OrchestrationError( 'conflict', @@ -100,27 +105,69 @@ export const readAwsMarketplaceClaim: OperationUseCase< }, } -interface LinkLicenseInput { - organizationId: string +interface ClaimInput { licenseId: string /** The raw claim token from this browser's httpOnly cookie, or null when it holds none. */ claimToken: string | null } +interface LinkLicenseInput extends ClaimInput { + organizationId: string +} + interface LinkedLicense { license: AwsMarketplaceLicenseRecord - /** False when the license was already linked to this organization; nothing new to announce. */ + /** False when the license already belonged here; nothing new to audit or announce. */ newlyLinked: boolean } /** - * Tells the team a license now belongs to an organization, so they provision it the way an - * Enterprise deal is provisioned. No subscription is created or changed here. A failed send never - * undoes the committed link; the warning is the fallback trail. + * Checks this browser's claim, then asks the Entitlement Service again: a license is taken only + * while AWS says it is current. A lapsed answer is recorded before the refusal. The caller + * rechecks the claim under the row lock, so two tabs racing one claim take it at most once. */ -async function notifyTeamOfLinkedLicense( +async function verifyClaimedLicense(input: ClaimInput, organizationId: string | null) { + const productCode = requireProductCode() + const claimHash = input.claimToken ? hashClaimToken(input.claimToken) : null + const claimed = requireOpenClaim( + claimHash ? await findLicenseByClaimHash(claimHash) : null, + input.licenseId, + claimHash + ) + requireLinkableTo(claimed, organizationId) + + const verifiedAt = new Date() + const summary = await verifyLicense(productCode, claimed.licenseArn) + if (!summary.active) { + await recordLicenseVerification({ + licenseArn: claimed.licenseArn, + productCode, + summary, + verifiedAt, + }) + throw new OrchestrationError( + 'conflict', + 'AWS Marketplace reports no active subscription for this license.' + ) + } + return { claimHash, summary, verifiedAt } +} + +const TEAM_INSTRUCTIONS = { + organization: + 'Provision this organization the way Enterprise deals are provisioned. No subscription was changed.', + account: + 'Registered to an account, no organization yet. Create the Enterprise organization for this buyer and link it. No subscription was changed.', +} as const + +/** + * Tells the team a license is taken, so they provision it the way an Enterprise deal is + * provisioned. No subscription is created or changed here. A failed send never undoes the + * committed write; the warning is the fallback trail. + */ +async function notifyTeam( license: AwsMarketplaceLicenseRecord, - organizationId: string, + organizationId: string | null, userId: string ): Promise { const parties = await getLinkParties(organizationId, userId) @@ -131,10 +178,10 @@ async function notifyTeamOfLinkedLicense( quantity: license.quantity, expiresAt: license.expiresAt?.toISOString() ?? null, } - const text = `AWS Marketplace license linked -Linked: ${new Date().toISOString()} -Organization: ${parties.organizationName ?? 'Unknown'} (${organizationId}) -Linked by: ${parties.userName ?? 'Unknown'} <${parties.userEmail ?? 'unknown'}> + const text = `AWS Marketplace license ${organizationId ? 'linked' : 'registered to an account'} +Taken: ${new Date().toISOString()} +Organization: ${organizationId ? `${parties.organizationName ?? 'Unknown'} (${organizationId})` : 'None yet'} +Buyer: ${parties.userName ?? 'Unknown'} <${parties.userEmail ?? 'unknown'}> (${userId}) License: ${license.licenseArn} AWS account: ${license.customerAwsAccountId ?? 'Not provided'} Customer identifier: ${license.customerIdentifier} @@ -142,38 +189,48 @@ Dimension: ${license.dimension ?? 'Not provided'} Quantity: ${license.quantity ?? 'Not provided'} Expires: ${notice.expiresAt ?? 'No expiry'} -Provision this organization the way Enterprise deals are provisioned. No subscription was changed. +${TEAM_INSTRUCTIONS[organizationId ? 'organization' : 'account']} ` + const subject = organizationId + ? (parties.organizationName ?? organizationId) + : `${parties.userEmail ?? userId} (no organization)` try { const result = await sendEmail({ to: [`enterprise@${env.EMAIL_DOMAIN || getEmailDomain()}`], - subject: `[AWS MARKETPLACE] ${parties.organizationName ?? organizationId}`, + subject: `[AWS MARKETPLACE] ${subject}`, text, from: getFromEmailAddress(), ...(parties.userEmail && { replyTo: parties.userEmail }), emailType: 'transactional', }) if (result.success) { - logger.info('Notified the team of a linked AWS Marketplace license', notice) + logger.info('Notified the team of an AWS Marketplace license', notice) return } - logger.warn('Could not notify the team of a linked AWS Marketplace license', { + logger.warn('Could not notify the team of an AWS Marketplace license', { ...notice, message: result.message, }) } catch (error) { - logger.warn('Could not notify the team of a linked AWS Marketplace license', { + logger.warn('Could not notify the team of an AWS Marketplace license', { ...notice, message: getErrorMessage(error, 'Unknown error'), }) } } +function licenseAuditMetadata(license: AwsMarketplaceLicenseRecord) { + return { + dimension: license.dimension, + quantity: license.quantity, + expiresAt: license.expiresAt?.toISOString() ?? null, + } +} + /** - * Links the license this browser's claim names to an organization the caller administers. The - * claim is checked before and again under the row lock, so two tabs racing one claim link it at - * most once, and the Entitlement Service is asked again: a license links only while AWS says it - * is current. A license belongs to at most one organization. + * Links the license this browser's claim names to an organization the caller administers. A + * license belongs to at most one organization; one registered to an account and no organization + * yet may still be linked. */ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ operation: awsMarketplaceOperations.linkLicense, @@ -184,30 +241,10 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ input, context, }: OrganizationUseCaseContext): Promise { - const productCode = requireProductCode() - const claimHash = input.claimToken ? hashClaimToken(input.claimToken) : null - const claimed = requireOpenClaim( - claimHash ? await findLicenseByClaimHash(claimHash) : null, - input.licenseId, - claimHash + const { claimHash, summary, verifiedAt } = await verifyClaimedLicense( + input, + input.organizationId ) - requireLinkableTo(claimed, input.organizationId) - - const verifiedAt = new Date() - const summary = await verifyLicense(productCode, claimed.licenseArn) - if (!summary.active) { - await recordLicenseVerification({ - licenseArn: claimed.licenseArn, - productCode, - summary, - verifiedAt, - }) - throw new OrchestrationError( - 'conflict', - 'AWS Marketplace reports no active subscription for this license.' - ) - } - return db.transaction(async (tx) => { const locked = requireOpenClaim( await lockLicense(tx, input.licenseId), @@ -243,16 +280,66 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ resourceId: result.license.id, resourceName: result.license.dimension ?? 'AWS Marketplace license', description: 'Linked an AWS Marketplace subscription to the organization', - metadata: { - dimension: result.license.dimension, - quantity: result.license.quantity, - expiresAt: result.license.expiresAt?.toISOString() ?? null, - }, + metadata: licenseAuditMetadata(result.license), } : [], afterSuccess: async ({ result, input, context }) => { if (result.newlyLinked) { - await notifyTeamOfLinkedLicense(result.license, input.organizationId, context.userId) + await notifyTeam(result.license, input.organizationId, context.userId) } }, }) + +/** + * Registers the license this browser's claim names to the signed-in account, with no + * organization: a new buyer who administers none still completes setup, and the team creates + * the Enterprise organization and links it. A license already linked to an organization is + * refused; registering it here never moves it. + */ +export const registerAwsMarketplaceLicenseToAccount: OperationUseCase< + typeof awsMarketplaceOperations.registerToAccount, + ClaimInput, + { licenseId: string } +> = { + operation: awsMarketplaceOperations.registerToAccount, + async execute({ principal, input, request }) { + assertOperationPrincipal(principal, awsMarketplaceOperations.registerToAccount) + const { claimHash, summary, verifiedAt } = await verifyClaimedLicense(input, null) + const { license, newlyRegistered } = await db.transaction(async (tx) => { + const locked = requireOpenClaim( + await lockLicense(tx, input.licenseId), + input.licenseId, + claimHash + ) + requireLinkableTo(locked, null) + const newlyRegistered = locked.linkedByUserId !== principal.userId + const consumed = await consumeLicenseClaim(tx, { + licenseId: input.licenseId, + link: newlyRegistered ? { organizationId: null, userId: principal.userId } : null, + summary, + verifiedAt, + }) + return { license: consumed, newlyRegistered } + }) + if (newlyRegistered) { + recordProjectedUseCaseAuditEntries( + awsMarketplaceOperations.registerToAccount, + null, + principal, + request, + [ + { + action: AuditAction.AWS_MARKETPLACE_LICENSE_REGISTERED, + resourceType: AuditResourceType.AWS_MARKETPLACE_LICENSE, + resourceId: license.id, + resourceName: license.dimension ?? 'AWS Marketplace license', + description: 'Registered an AWS Marketplace subscription to an account', + metadata: licenseAuditMetadata(license), + }, + ] + ) + await notifyTeam(license, null, principal.userId) + } + return { licenseId: license.id } + }, +} diff --git a/apps/sim/lib/billing/aws-marketplace/client.ts b/apps/sim/lib/billing/aws-marketplace/client.ts index 277790b3ba7..172b29b7a98 100644 --- a/apps/sim/lib/billing/aws-marketplace/client.ts +++ b/apps/sim/lib/billing/aws-marketplace/client.ts @@ -1,7 +1,9 @@ import { MarketplaceEntitlementServiceClient } from '@aws-sdk/client-marketplace-entitlement-service' import { MarketplaceMeteringClient } from '@aws-sdk/client-marketplace-metering' import { SQSClient } from '@aws-sdk/client-sqs' +import { fromHttp } from '@aws-sdk/credential-provider-http' import { getAwsCredentials } from '@/lib/core/config/aws' +import { env } from '@/lib/core/config/env' /** The Marketplace metering and entitlement endpoints for SaaS products live only here. */ const MARKETPLACE_REGION = 'us-east-1' @@ -11,14 +13,30 @@ let entitlementClient: MarketplaceEntitlementServiceClient | null = null let queueClient: SQSClient | null = null /** - * Credentials come from `getAwsCredentials()`: the SDK's default chain, which on ECS resolves the - * task role. Clients are built on first use, so a deployment with the feature off never reaches - * for credentials. + * Credentials for the Marketplace and event-queue clients only. On ECS the app's env also carries + * a static IAM user's keys, which the SDK's default chain prefers and which Sim's general AWS + * clients (`getAwsCredentials()`) sign with. Infra grants ResolveCustomer, GetEntitlements, and the + * queue permissions to the ECS task role alone, and AWS asks for temporary role credentials here, + * so whenever the container credential endpoint is present these clients sign as the task role. + * Elsewhere (local development, tests) they keep the default chain. */ +function marketplaceCredentials() { + const relativeUri = env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI + const fullUri = env.AWS_CONTAINER_CREDENTIALS_FULL_URI + if (relativeUri || fullUri) { + return fromHttp({ + awsContainerCredentialsRelativeUri: relativeUri, + awsContainerCredentialsFullUri: fullUri, + }) + } + return getAwsCredentials() +} + +/** Built on first use, so a deployment with the feature off never reaches for credentials. */ export function getMeteringClient(): MarketplaceMeteringClient { meteringClient ??= new MarketplaceMeteringClient({ region: MARKETPLACE_REGION, - credentials: getAwsCredentials(), + credentials: marketplaceCredentials(), }) return meteringClient } @@ -27,13 +45,16 @@ export function getMeteringClient(): MarketplaceMeteringClient { export function getEntitlementClient(): MarketplaceEntitlementServiceClient { entitlementClient ??= new MarketplaceEntitlementServiceClient({ region: MARKETPLACE_REGION, - credentials: getAwsCredentials(), + credentials: marketplaceCredentials(), maxAttempts: 1, }) return entitlementClient } export function getEventsQueueClient(): SQSClient { - queueClient ??= new SQSClient({ region: MARKETPLACE_REGION, credentials: getAwsCredentials() }) + queueClient ??= new SQSClient({ + region: MARKETPLACE_REGION, + credentials: marketplaceCredentials(), + }) return queueClient } diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts index 83fb41783d5..de7b99d4ff6 100644 --- a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -50,7 +50,10 @@ vi.mock('@aws-sdk/client-sqs', () => ({ vi.mock('@/lib/messaging/email/mailer', () => emailMailerMock) vi.mock('@/lib/core/utils/request', () => requestUtilsMock) -import { linkAwsMarketplaceLicense } from '@/lib/billing/aws-marketplace/application/use-cases' +import { + linkAwsMarketplaceLicense, + registerAwsMarketplaceLicenseToAccount, +} from '@/lib/billing/aws-marketplace/application/use-cases' import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' import { reconcileAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/reconcile' import { POST as register } from '@/app/api/marketplace/aws/register/route' @@ -409,6 +412,136 @@ describe('linking a claimed license', () => { }) }) +describe('registering a claimed license to an account', () => { + let buyerId: string + + beforeAll(async () => { + buyerId = await createUser() + }) + + function registerToAccount(input: { licenseId: string; claimToken: string | null }) { + return registerAwsMarketplaceLicenseToAccount.execute({ + principal: createSessionPrincipal({ userId: buyerId }), + input, + }) + } + + it('registers to a buyer with no organization, consumes the claim, audits, and emails once', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + await registerToAccount({ licenseId: license.id, claimToken: license.token }) + + const row = await readLicense(license.licenseArn) + expect(row).toMatchObject({ + organizationId: null, + linkedByUserId: buyerId, + claimTokenHash: null, + claimExpiresAt: null, + }) + expect(row?.linkedAt).toBeInstanceOf(Date) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) + expect(emailMailerMockFns.mockSendEmail.mock.calls[0][0]).toMatchObject({ + to: [expect.stringMatching(/^enterprise@/)], + text: expect.stringMatching(/no organization yet/), + }) + await vi.waitFor(async () => { + const entries = await db + .select({ action: auditLog.action }) + .from(auditLog) + .where(and(eq(auditLog.resourceId, license.id), eq(auditLog.actorId, buyerId))) + expect(entries).toEqual([{ action: 'aws_marketplace_license.registered' }]) + }) + + const replay = await rejection( + registerToAccount({ licenseId: license.id, claimToken: license.token }) + ) + expect(replay).toMatchObject({ code: 'forbidden' }) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) + }) + + it('refuses a browser that holds no claim', async () => { + const license = await createClaimedLicense() + + const error = await rejection(registerToAccount({ licenseId: license.id, claimToken: null })) + + expect(error).toMatchObject({ code: 'forbidden' }) + expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() + }) + + it('refuses an expired claim', async () => { + const license = await createClaimedLicense({ claimExpiresAt: new Date(Date.now() - 1000) }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + registerToAccount({ licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'forbidden', message: expect.stringMatching(/expired/) }) + expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() + }) + + it('refuses when AWS no longer reports a current entitlement', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [lapsedEntitlement()]) + + const error = await rejection( + registerToAccount({ licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'conflict' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + linkedByUserId: null, + status: 'inactive', + claimTokenHash: hashClaimToken(license.token), + }) + expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + }) + + it('refuses a license already linked to an organization', async () => { + const ownerId = await createUser() + const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) + const license = await createClaimedLicense({ organizationId, linkedByUserId: ownerId }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + registerToAccount({ licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'conflict' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId, + linkedByUserId: ownerId, + }) + }) + + it('lets a license registered to an account be linked to an organization later', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + await registerToAccount({ licenseId: license.id, claimToken: license.token }) + + resolvesTo(license.licenseArn) + const response = await registration('x-amzn-marketplace-token=second-visit') + const token = + /sim_aws_marketplace_claim=([^;]+)/.exec(response.headers.get('set-cookie') ?? '')?.[1] ?? + null + expect(token).not.toBeNull() + const organizationId = await createOrganization([{ userId: buyerId, role: 'owner' }]) + + await linkAwsMarketplaceLicense.execute({ + principal: createSessionPrincipal({ userId: buyerId }), + input: { organizationId, licenseId: license.id, claimToken: token }, + }) + + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId, + linkedByUserId: buyerId, + claimTokenHash: null, + }) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) + }) +}) + describe('reconcileAwsMarketplaceLicenses', () => { it('discards poison events, keeps a throttled one for redelivery, and marks lapsed licenses inactive', async () => { const eventArn = uniqueArn() diff --git a/apps/sim/lib/billing/aws-marketplace/repository.ts b/apps/sim/lib/billing/aws-marketplace/repository.ts index dfdc5a7d2f6..54d88c68842 100644 --- a/apps/sim/lib/billing/aws-marketplace/repository.ts +++ b/apps/sim/lib/billing/aws-marketplace/repository.ts @@ -134,14 +134,15 @@ export async function lockLicense( } /** - * Consumes the claim and records the verification it relied on, linking the license when `link` - * is given. A license already linked to the claimed organization keeps its original link. + * Consumes the claim and records the verification it relied on, taking the license when `link` + * is given: for an organization, or for an account alone when its organization is null. A + * license already taken by the same party keeps its original link. */ export async function consumeLicenseClaim( tx: DbTransaction, input: { licenseId: string - link: { organizationId: string; userId: string } | null + link: { organizationId: string | null; userId: string } | null summary: EntitlementSummary verifiedAt: Date } @@ -180,15 +181,17 @@ export async function listAdministeredOrganizations( /** Who linked a license to which organization, for the team's provisioning notice. */ export async function getLinkParties( - organizationId: string, + organizationId: string | null, userId: string ): Promise<{ organizationName: string | null; userName: string | null; userEmail: string | null }> { const [[org], [person]] = await Promise.all([ - db - .select({ name: organization.name }) - .from(organization) - .where(eq(organization.id, organizationId)) - .limit(1), + organizationId + ? db + .select({ name: organization.name }) + .from(organization) + .where(eq(organization.id, organizationId)) + .limit(1) + : Promise.resolve([]), db .select({ name: user.name, email: user.email }) .from(user) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index b118b14c9b2..da8c2d61915 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -368,6 +368,8 @@ export const env = createEnv({ AWS_ACCESS_KEY_ID: z.string().optional(), // AWS access key ID AWS_SECRET_ACCESS_KEY: z.string().optional(), // AWS secret access key TRIGGER_AWS_ROLE_ARN: z.string().optional(), // Role Trigger.dev runs assume through workload identity, instead of static keys + AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: z.string().optional(), // Set by ECS: path of the task-role credential endpoint + AWS_CONTAINER_CREDENTIALS_FULL_URI: z.string().optional(), // Set by ECS Anywhere/EKS Pod Identity: full task-role credential endpoint URL S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files S3_EXECUTION_FILES_BUCKET_NAME: z.string().optional(), // S3 bucket for workflow execution files diff --git a/apps/sim/package.json b/apps/sim/package.json index 5e9a32f22ae..dd41c896f58 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -92,6 +92,7 @@ "@aws-sdk/client-sso-admin": "3.1150.0", "@aws-sdk/client-sts": "3.1150.0", "@aws-sdk/client-textract": "3.1150.0", + "@aws-sdk/credential-provider-http": "3.972.76", "@aws-sdk/lib-dynamodb": "3.1150.0", "@aws-sdk/s3-request-presigner": "3.1150.0", "@azure/communication-email": "1.1.0", diff --git a/bun.lock b/bun.lock index 0ab5d27bdcd..9c4cc947ee9 100644 --- a/bun.lock +++ b/bun.lock @@ -246,6 +246,7 @@ "@aws-sdk/client-sso-admin": "3.1150.0", "@aws-sdk/client-sts": "3.1150.0", "@aws-sdk/client-textract": "3.1150.0", + "@aws-sdk/credential-provider-http": "3.972.76", "@aws-sdk/lib-dynamodb": "3.1150.0", "@aws-sdk/s3-request-presigner": "3.1150.0", "@azure/communication-email": "1.1.0", diff --git a/packages/audit/src/types.ts b/packages/audit/src/types.ts index e73b191efa9..2c897ceff76 100644 --- a/packages/audit/src/types.ts +++ b/packages/audit/src/types.ts @@ -12,6 +12,7 @@ export const AuditAction = { AUDIT_LOGS_EXPORTED: 'audit_logs.exported', AWS_MARKETPLACE_LICENSE_LINKED: 'aws_marketplace_license.linked', + AWS_MARKETPLACE_LICENSE_REGISTERED: 'aws_marketplace_license.registered', API_KEY_CREATED: 'api_key.created', API_KEY_UPDATED: 'api_key.updated', diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index b9103dadade..50af39f6484 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -15,6 +15,7 @@ const AuditAction = { AUTH_SESSIONS_REVOKED: 'auth.sessions_revoked', AUDIT_LOGS_EXPORTED: 'audit_logs.exported', AWS_MARKETPLACE_LICENSE_LINKED: 'aws_marketplace_license.linked', + AWS_MARKETPLACE_LICENSE_REGISTERED: 'aws_marketplace_license.registered', API_KEY_CREATED: 'api_key.created', API_KEY_UPDATED: 'api_key.updated', API_KEY_REVOKED: 'api_key.revoked', From a7563183ec85774ab882dac490fdd00d9fca80d1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 19:35:42 -0800 Subject: [PATCH 3/7] fix(billing): register new-product AWS Marketplace buyers, meet the listing guidelines, and handle agreement events - ResolveCustomer leaves CustomerIdentifier empty for new SaaS products, so registration now requires the account ID and keys off it and the license ARN. customer_identifier is nullable, customer_aws_account_id is required (not unique), and agreement_id is stored. No license ARN yet means a future-dated agreement: nothing is saved and the buyer is told to return on the start date. - Listing guidelines: the claim step collects a contact email and optional company, the buyer gets a confirmation with turnaround and support contact, organization members see subscription status on the billing page through a contracted read, and the team is emailed when a license's status, quantity, or expiry changes, or when an event names a license nobody registered. - Agreement events: Ended re-verifies the licenses of that agreement; a RENEW tells the team once across both seller roles and links nothing; agreements for other products are dropped quietly. - Every writer of entitlement fields honours the newest read, so a slow registration or claim cannot restore a lapsed license, and a claim is refused if a newer read says inactive. - Post-commit notices, lookups included, never fail the request. Re-verifies order by last attempt, so failing licenses cannot starve the batch. The reconcile has a 6-minute wall-clock budget and a per-message abort that fit the receive visibility timeout and the cron lock. - The fulfillment URL redirects a GET to onboarding and accepts multipart forms under the same cap. --- .../cron/aws-marketplace-reconcile/route.ts | 2 +- .../api/marketplace/aws/claim/link/route.ts | 2 + .../aws/claim/register-account/route.ts | 2 + .../app/api/marketplace/aws/register/route.ts | 15 +- .../[id]/aws-marketplace-licenses/route.ts | 26 + .../aws/aws-marketplace-onboarding.tsx | 102 +++- .../settings/components/billing/billing.tsx | 5 + .../aws-marketplace-subscription-section.tsx | 55 ++ apps/sim/hooks/queries/aws-marketplace.ts | 20 + apps/sim/lib/api/contracts/aws-marketplace.ts | 37 +- .../aws-marketplace/application/operations.ts | 7 + .../aws-marketplace/application/use-cases.ts | 145 ++--- .../billing/aws-marketplace/entitlements.ts | 25 +- .../aws-marketplace/lifecycle.integration.ts | 571 +++++++++++++++++- .../billing/aws-marketplace/notifications.ts | 264 ++++++++ .../lib/billing/aws-marketplace/onboarding.ts | 4 + .../lib/billing/aws-marketplace/reconcile.ts | 217 +++++-- .../billing/aws-marketplace/registration.ts | 32 +- .../lib/billing/aws-marketplace/repository.ts | 320 ++++++++-- .../0454_aws_marketplace_license.sql | 14 +- .../db/migrations/meta/0454_snapshot.json | 93 ++- packages/db/migrations/meta/_journal.json | 2 +- packages/db/schema.ts | 26 +- .../src/mocks/schema-tables.generated.ts | 5 + 24 files changed, 1711 insertions(+), 280 deletions(-) create mode 100644 apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx create mode 100644 apps/sim/lib/billing/aws-marketplace/notifications.ts diff --git a/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts b/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts index bce125c48a5..b25b37572af 100644 --- a/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts +++ b/apps/sim/app/api/cron/aws-marketplace-reconcile/route.ts @@ -11,7 +11,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('AwsMarketplaceReconcileCron') const LOCK_KEY = 'aws-marketplace-reconcile-lock' -/** Covers a full drain plus the re-verification batch; the schedule runs every 15 minutes. */ +/** Outlasts the reconcile's 6-minute run budget; the schedule runs every 15 minutes. */ const LOCK_TTL_SECONDS = 10 * 60 export const dynamic = 'force-dynamic' diff --git a/apps/sim/app/api/marketplace/aws/claim/link/route.ts b/apps/sim/app/api/marketplace/aws/claim/link/route.ts index 6b46a96f80a..b5b4ecdf9ae 100644 --- a/apps/sim/app/api/marketplace/aws/claim/link/route.ts +++ b/apps/sim/app/api/marketplace/aws/claim/link/route.ts @@ -22,6 +22,8 @@ export const POST = defineInternalJsonRoute({ mapInput: ({ body }, { request }) => ({ organizationId: body.organizationId, licenseId: body.licenseId, + contactEmail: body.contactEmail, + companyName: body.companyName || null, claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, }), present: (_linked, { input }) => ({ diff --git a/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts b/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts index f4cd1f7388b..de32a3677b8 100644 --- a/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts +++ b/apps/sim/app/api/marketplace/aws/claim/register-account/route.ts @@ -21,6 +21,8 @@ export const POST = defineInternalJsonRoute({ staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, mapInput: ({ body }, { request }) => ({ licenseId: body.licenseId, + contactEmail: body.contactEmail, + companyName: body.companyName || null, claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, }), present: () => ({ success: true as const }), diff --git a/apps/sim/app/api/marketplace/aws/register/route.ts b/apps/sim/app/api/marketplace/aws/register/route.ts index 31a431aa5fa..16af5e1d589 100644 --- a/apps/sim/app/api/marketplace/aws/register/route.ts +++ b/apps/sim/app/api/marketplace/aws/register/route.ts @@ -17,6 +17,9 @@ export const dynamic = 'force-dynamic' const logger = createLogger('AwsMarketplaceRegisterAPI') +/** AWS posts a URL-encoded form; multipart is accepted too, under the same cap. */ +const FORM_MEDIA_TYPES = new Set(['application/x-www-form-urlencoded', 'multipart/form-data']) + /** The form carries one opaque token; anything larger is not from AWS. */ const MAX_FORM_BYTES = 16 * 1024 @@ -34,7 +37,7 @@ function redirectToOnboarding(error?: AwsMarketplaceOnboardingError): NextRespon async function readRegistrationToken(request: NextRequest): Promise { const mediaType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() - if (mediaType !== 'application/x-www-form-urlencoded') return null + if (!mediaType || !FORM_MEDIA_TYPES.has(mediaType)) return null let form: FormData try { form = await readFormDataWithLimit(request, { @@ -44,8 +47,9 @@ async function readRegistrationToken(request: NextRequest): Promise { response.headers.set('Cache-Control', 'no-store') return response }) + +/** A refresh, back button, or bookmark of the fulfillment URL lands on the onboarding page. */ +export const GET = withRouteHandler(async () => { + if (!getAwsMarketplaceProductCode()) + return NextResponse.json({ error: 'Not found' }, { status: 404 }) + return redirectToOnboarding() +}) diff --git a/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts b/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts new file mode 100644 index 00000000000..a6d4bb352ca --- /dev/null +++ b/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts @@ -0,0 +1,26 @@ +import { listOrganizationAwsMarketplaceLicensesContract } from '@/lib/api/contracts/aws-marketplace' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { listOrganizationAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/application/use-cases' + +export const GET = defineInternalJsonRoute({ + contract: listOrganizationAwsMarketplaceLicensesContract, + operation: listOrganizationAwsMarketplaceLicenses.operation, + useCase: listOrganizationAwsMarketplaceLicenses, + auth: internalSessionAuth, + rateLimit: internalRateLimits.user({ bucketName: 'aws-marketplace-licenses' }), + errorPolicy: internalOrchestrationErrorPolicy, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, + mapInput: ({ params }) => ({ organizationId: params.id }), + present: (licenses) => ({ + licenses: licenses.map((license) => ({ + ...license, + expiresAt: license.expiresAt?.toISOString() ?? null, + lastVerifiedAt: license.lastVerifiedAt.toISOString(), + })), + }), +}) diff --git a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx index c7966ee6f29..5c8ba29bbc2 100644 --- a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx +++ b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx @@ -1,17 +1,26 @@ 'use client' -import { useState } from 'react' -import { AuthField, AuthFormMessage, AuthSelect, AuthTag } from '@sim/auth-ui' +import { type ReactNode, useState } from 'react' +import { + AuthField, + AuthFormMessage, + AuthInput, + AuthSelect, + AuthTag, + AuthTextLink, +} from '@sim/auth-ui' import { useRouter } from 'next/navigation' import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' import { useSession } from '@/lib/auth/client' import { AWS_MARKETPLACE_ONBOARDING_PATH, + AWS_MARKETPLACE_SETUP_TURNAROUND, type AwsMarketplaceOnboardingError, } from '@/lib/billing/aws-marketplace/onboarding' import { APP_ENTRY_PATH } from '@/lib/navigation/paths' import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect' import { InviteLayout, InviteStatusCard } from '@/app/invite/components' +import { useBrandConfig } from '@/ee/whitelabeling' import { useAwsMarketplaceClaim, useLinkAwsMarketplaceLicense, @@ -30,6 +39,11 @@ const REGISTRATION_ERROR_COPY: Record< description: 'AWS Marketplace has no active Sim subscription for this AWS account. Subscribe to Sim in AWS Marketplace, then choose Set up your account to return here.', }, + 'not-started': { + title: 'Your agreement has not started', + description: + 'Come back to AWS Marketplace on your agreement start date and choose Set up your account.', + }, invalid: { title: 'This setup link did not work', description: RESTART_FROM_AWS, @@ -47,6 +61,21 @@ function authLink(path: '/login' | '/signup'): string { }) } +const SETUP_NEXT_STEP = `Our team will set up your Enterprise workspace ${AWS_MARKETPLACE_SETUP_TURNAROUND} and email you when it is ready.` + +interface SupportContactProps { + supportEmail: string +} + +/** AWS requires support contact options on the fulfillment landing page. */ +function SupportContact({ supportEmail }: SupportContactProps) { + return ( +

+ Questions? Email {supportEmail} +

+ ) +} + function formatDate(iso: string): string { return new Date(iso).toLocaleDateString(undefined, { dateStyle: 'medium' }) } @@ -66,6 +95,8 @@ export function AwsMarketplaceOnboarding({ const claimQuery = useAwsMarketplaceClaim(registrationError ? null : viewerId) const linkLicense = useLinkAwsMarketplaceLicense() const registerToAccount = useRegisterAwsMarketplaceLicenseToAccount() + const { supportEmail } = useBrandConfig() + const supportContact = supportEmail ? : undefined const returnHome = { label: 'Return to Home', onClick: () => router.push('/') } if (registrationError) { @@ -117,7 +148,8 @@ export function AwsMarketplaceOnboarding({ router.push(APP_ENTRY_PATH) }]} />
@@ -130,7 +162,8 @@ export function AwsMarketplaceOnboarding({ router.push(APP_ENTRY_PATH) }]} />
@@ -176,31 +209,40 @@ export function AwsMarketplaceOnboarding({ return ( { + onLink={(organizationId, contact) => { registerToAccount.reset() - linkLicense.mutate({ licenseId: claim.license.id, organizationId }) + linkLicense.mutate({ licenseId: claim.license.id, organizationId, ...contact }) }} - onRegister={() => { + onRegister={(contact) => { linkLicense.reset() - registerToAccount.mutate({ licenseId: claim.license.id }) + registerToAccount.mutate({ licenseId: claim.license.id, ...contact }) }} onCancel={returnHome.onClick} /> ) } +interface BuyerContact { + contactEmail: string + companyName?: string +} + interface LinkLicenseCardProps { claim: Extract + defaultContactEmail: string + supportContact: ReactNode pending: boolean linking: boolean registering: boolean error: string | null - onLink: (organizationId: string) => void - onRegister: () => void + onLink: (organizationId: string, contact: BuyerContact) => void + onRegister: (contact: BuyerContact) => void onCancel: () => void } @@ -210,6 +252,8 @@ interface LinkLicenseCardProps { */ function LinkLicenseCard({ claim, + defaultContactEmail, + supportContact, pending, linking, registering, @@ -220,6 +264,13 @@ function LinkLicenseCard({ }: LinkLicenseCardProps) { const { license, organizations } = claim const [selectedOrganizationId, setSelectedOrganizationId] = useState() + const [contactEmail, setContactEmail] = useState(defaultContactEmail) + const [companyName, setCompanyName] = useState('') + const contact: BuyerContact = { + contactEmail: contactEmail.trim(), + ...(companyName.trim() && { companyName: companyName.trim() }), + } + const hasContact = contact.contactEmail.length > 0 const linkedOrganization = organizations.find((org) => org.id === license.organizationId) const organizationId = selectedOrganizationId ?? @@ -238,8 +289,8 @@ function LinkLicenseCard({ const hasOrganizations = organizations.length > 0 const registerAction = { label: hasOrganizations ? 'Register to my account instead' : 'Register to my account', - onClick: onRegister, - disabled: pending, + onClick: () => onRegister(contact), + disabled: pending || !hasContact, loading: registering, } @@ -254,8 +305,8 @@ function LinkLicenseCard({ } description={ hasOrganizations - ? 'Choose the organization this subscription is for, or register it to your account. Our team will finish setting up your plan.' - : 'Register this subscription to your account. Our team will set up your Enterprise workspace and email you.' + ? `Choose the organization this subscription is for, or register it to your account. ${SETUP_NEXT_STEP}` + : `Register this subscription to your account. ${SETUP_NEXT_STEP}` } details={
@@ -285,7 +336,26 @@ function LinkLicenseCard({ /> )} + + setContactEmail(event.target.value)} + required + /> + + + setCompanyName(event.target.value)} + /> + {error && {error}} + {supportContact}
} actions={[ @@ -293,8 +363,8 @@ function LinkLicenseCard({ ? [ { label: 'Link subscription', - onClick: () => organizationId && onLink(organizationId), - disabled: !organizationId || pending, + onClick: () => organizationId && onLink(organizationId, contact), + disabled: !organizationId || !hasContact || pending, loading: linking, }, ] diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx index f9cabc91f93..4b11f63c01e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx @@ -45,6 +45,7 @@ import { } from '@/lib/billing/subscriptions/utils' import { buildUpgradeHref } from '@/lib/billing/upgrade-reasons' import { getBaseUrl } from '@/lib/core/utils/urls' +import { AwsMarketplaceSubscriptionSection } from '@/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section' import { CreditUsageSection } from '@/app/workspace/[workspaceId]/settings/components/billing/components/credit-usage-section/credit-usage-section' import { UsageLimitField } from '@/app/workspace/[workspaceId]/settings/components/billing/components/usage-limit-field/usage-limit-field' import { getSubscriptionPermissions } from '@/app/workspace/[workspaceId]/settings/components/billing/subscription-permissions' @@ -660,6 +661,10 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps )} + {billingOrganizationId && ( + + )} + {!subscription.isFree && invoices.length > 0 && (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx new file mode 100644 index 00000000000..5a0b579f165 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx @@ -0,0 +1,55 @@ +'use client' + +import { Badge } from '@sim/emcn' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { useOrganizationAwsMarketplaceLicenses } from '@/hooks/queries/aws-marketplace' + +function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString() +} + +interface AwsMarketplaceSubscriptionSectionProps { + organizationId: string +} + +/** Read-only: AWS requires buyers to see their subscription status in the product. */ +export function AwsMarketplaceSubscriptionSection({ + organizationId, +}: AwsMarketplaceSubscriptionSectionProps) { + const { data: licenses } = useOrganizationAwsMarketplaceLicenses(organizationId) + if (!licenses?.length) return null + + return ( + +
+ {licenses.map((license) => { + const rows = [ + { label: 'Plan', value: license.dimension ?? 'Not specified' }, + { label: 'Quantity', value: license.quantity?.toLocaleString() ?? 'Not specified' }, + { + label: 'Expires', + value: license.expiresAt ? formatDate(license.expiresAt) : 'No expiry', + }, + { label: 'Last verified', value: formatDate(license.lastVerifiedAt) }, + ] + return ( +
+
+ Status + + {license.status === 'active' ? 'Active' : 'Inactive'} + +
+ {rows.map((row) => ( +
+ {row.label} + {row.value} +
+ ))} +
+ ) + })} +
+
+ ) +} diff --git a/apps/sim/hooks/queries/aws-marketplace.ts b/apps/sim/hooks/queries/aws-marketplace.ts index 0fe128c7086..527ae167b85 100644 --- a/apps/sim/hooks/queries/aws-marketplace.ts +++ b/apps/sim/hooks/queries/aws-marketplace.ts @@ -4,16 +4,21 @@ import type { ContractBodyInput } from '@/lib/api/contracts' import { getAwsMarketplaceClaimContract, linkAwsMarketplaceLicenseContract, + listOrganizationAwsMarketplaceLicensesContract, registerAwsMarketplaceLicenseToAccountContract, } from '@/lib/api/contracts/aws-marketplace' const AWS_MARKETPLACE_CLAIM_STALE_TIME = 30 * 1000 +const AWS_MARKETPLACE_LICENSES_STALE_TIME = 60 * 1000 const awsMarketplaceKeys = { all: ['aws-marketplace'] as const, claims: () => [...awsMarketplaceKeys.all, 'claim'] as const, /** The claim rides in an httpOnly cookie; the viewer scopes which organizations it lists. */ claim: (viewerId: string) => [...awsMarketplaceKeys.claims(), viewerId] as const, + organizationLicenses: () => [...awsMarketplaceKeys.all, 'organization-licenses'] as const, + organizationLicenseList: (organizationId: string) => + [...awsMarketplaceKeys.organizationLicenses(), organizationId] as const, } export function useAwsMarketplaceClaim(viewerId: string | null) { @@ -47,3 +52,18 @@ export function useRegisterAwsMarketplaceLicenseToAccount() { }, }) } + +/** The Marketplace subscriptions linked to an organization, for its billing page. */ +export function useOrganizationAwsMarketplaceLicenses(organizationId: string | null) { + return useQuery({ + queryKey: awsMarketplaceKeys.organizationLicenseList(organizationId ?? ''), + queryFn: ({ signal }) => + requestJson(listOrganizationAwsMarketplaceLicensesContract, { + params: { id: organizationId ?? '' }, + signal, + }), + enabled: Boolean(organizationId), + staleTime: AWS_MARKETPLACE_LICENSES_STALE_TIME, + select: (data) => data.licenses, + }) +} diff --git a/apps/sim/lib/api/contracts/aws-marketplace.ts b/apps/sim/lib/api/contracts/aws-marketplace.ts index 21c3a57a933..9d8f9085e34 100644 --- a/apps/sim/lib/api/contracts/aws-marketplace.ts +++ b/apps/sim/lib/api/contracts/aws-marketplace.ts @@ -10,6 +10,17 @@ export const awsMarketplaceRegistrationFormSchema = z.object({ 'x-amzn-marketplace-token': z.string().min(1, 'Registration token is required').max(4096), }) +/** AWS requires the registration page to collect the buyer's email; the company is optional. */ +const buyerContactSchema = z.object({ + contactEmail: z + .string() + .trim() + .min(1, 'Contact email is required') + .max(254, 'Contact email must be at most 254 characters') + .email('Enter a valid contact email'), + companyName: z.string().trim().max(200, 'Company name must be at most 200 characters').optional(), +}) + const awsMarketplaceLicenseSummarySchema = z.object({ id: z.string(), status: z.enum(['active', 'inactive']), @@ -49,7 +60,7 @@ export const getAwsMarketplaceClaimContract = defineRouteContract({ export const linkAwsMarketplaceLicenseContract = defineRouteContract({ method: 'POST', path: '/api/marketplace/aws/claim/link', - body: z.object({ + body: buyerContactSchema.extend({ licenseId: z.string().min(1, 'License ID is required').max(255), organizationId: organizationIdSchema, }), @@ -63,6 +74,28 @@ export const linkAwsMarketplaceLicenseContract = defineRouteContract({ export const registerAwsMarketplaceLicenseToAccountContract = defineRouteContract({ method: 'POST', path: '/api/marketplace/aws/claim/register-account', - body: z.object({ licenseId: z.string().min(1, 'License ID is required').max(255) }), + body: buyerContactSchema.extend({ + licenseId: z.string().min(1, 'License ID is required').max(255), + }), response: { mode: 'json', schema: z.object({ success: z.literal(true) }) }, }) + +const organizationAwsMarketplaceLicenseSchema = z.object({ + id: z.string(), + status: z.enum(['active', 'inactive']), + dimension: z.string().nullable(), + quantity: z.number().int().nullable(), + expiresAt: z.iso.datetime().nullable(), + lastVerifiedAt: z.iso.datetime(), +}) + +/** The Marketplace licenses linked to an organization, for its billing page. */ +export const listOrganizationAwsMarketplaceLicensesContract = defineRouteContract({ + method: 'GET', + path: '/api/organizations/[id]/aws-marketplace-licenses', + params: z.object({ id: organizationIdSchema }), + response: { + mode: 'json', + schema: z.object({ licenses: z.array(organizationAwsMarketplaceLicenseSchema) }), + }, +}) diff --git a/apps/sim/lib/billing/aws-marketplace/application/operations.ts b/apps/sim/lib/billing/aws-marketplace/application/operations.ts index ad556c640c6..c0610844148 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/operations.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/operations.ts @@ -18,6 +18,13 @@ export const awsMarketplaceOperations = { principalKinds: ['session'], capability: 'none', }), + // permission-group-exempt: members see their organization's subscription status, as the billing page shows its plan. + listOrganizationLicenses: defineOrganizationOperation({ + id: 'organization.aws_marketplace_license.list', + minimumRole: 'member', + principalKinds: ['session'], + capability: 'none', + }), // permission-group-exempt: linking a purchased license is organization administration, not a governed capability. linkLicense: defineOrganizationOperation({ id: 'organization.aws_marketplace_license.link', diff --git a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts index 0230b898941..714eeedfd6c 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts @@ -1,18 +1,21 @@ import { AuditAction, AuditResourceType } from '@sim/audit' import { db } from '@sim/db' -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' import { awsMarketplaceOperations } from '@/lib/billing/aws-marketplace/application/operations' import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' -import { verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' +import { type EntitlementSummary, verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' +import { + notifyBuyerOfTakenLicense, + notifyTeamOfTakenLicense, + notifyTeamOfVerification, +} from '@/lib/billing/aws-marketplace/notifications' import { type AwsMarketplaceLicenseRecord, consumeLicenseClaim, findLicenseByClaimHash, - getLinkParties, listAdministeredOrganizations, + listOrganizationLicenses, lockLicense, recordLicenseVerification, } from '@/lib/billing/aws-marketplace/repository' @@ -23,13 +26,7 @@ import { import { recordProjectedUseCaseAuditEntries } from '@/lib/core/application/authorized-workspace-use-case' import { assertOperationPrincipal, type OperationUseCase } from '@/lib/core/application/operation' import { requireOrganizationSubjectMembership } from '@/lib/core/application/organization-authorization' -import { env } from '@/lib/core/config/env' import { OrchestrationError } from '@/lib/core/orchestration/types' -import { getEmailDomain } from '@/lib/core/utils/urls' -import { sendEmail } from '@/lib/messaging/email/mailer' -import { getFromEmailAddress } from '@/lib/messaging/email/utils' - -const logger = createLogger('AwsMarketplaceLicenses') /** The same answer as an unknown route, so a deployment that does not sell on AWS shows nothing. */ function requireProductCode(): string { @@ -42,6 +39,22 @@ function isClaimOpen(license: AwsMarketplaceLicenseRecord, now: Date): boolean { return Boolean(license.claimExpiresAt && license.claimExpiresAt.getTime() > now.getTime()) } +const NO_ACTIVE_SUBSCRIPTION = 'AWS Marketplace reports no active subscription for this license.' + +/** + * Under the row lock: a newer verification than this request's wins. If it says the license + * lapsed, the take is refused; otherwise it stands and this older read is not written. + */ +function newerVerificationGate( + locked: AwsMarketplaceLicenseRecord, + verifiedAt: Date, + summary: EntitlementSummary +) { + if (locked.lastVerifiedAt.getTime() <= verifiedAt.getTime()) return { summary, verifiedAt } + if (locked.status !== 'active') throw new OrchestrationError('conflict', NO_ACTIVE_SUBSCRIPTION) + return null +} + const CLAIM_NOT_FOUND = 'This AWS Marketplace setup link is no longer valid. Open your subscription in AWS Marketplace and choose Set up your account again.' @@ -109,6 +122,9 @@ interface ClaimInput { licenseId: string /** The raw claim token from this browser's httpOnly cookie, or null when it holds none. */ claimToken: string | null + /** Whom to contact about provisioning; AWS asks the registration page to collect it. */ + contactEmail: string + companyName: string | null } interface LinkLicenseInput extends ClaimInput { @@ -139,86 +155,19 @@ async function verifyClaimedLicense(input: ClaimInput, organizationId: string | const verifiedAt = new Date() const summary = await verifyLicense(productCode, claimed.licenseArn) if (!summary.active) { - await recordLicenseVerification({ - licenseArn: claimed.licenseArn, - productCode, - summary, - verifiedAt, - }) - throw new OrchestrationError( - 'conflict', - 'AWS Marketplace reports no active subscription for this license.' + await notifyTeamOfVerification( + await recordLicenseVerification({ + licenseArn: claimed.licenseArn, + productCode, + summary, + verifiedAt, + }) ) + throw new OrchestrationError('conflict', NO_ACTIVE_SUBSCRIPTION) } return { claimHash, summary, verifiedAt } } -const TEAM_INSTRUCTIONS = { - organization: - 'Provision this organization the way Enterprise deals are provisioned. No subscription was changed.', - account: - 'Registered to an account, no organization yet. Create the Enterprise organization for this buyer and link it. No subscription was changed.', -} as const - -/** - * Tells the team a license is taken, so they provision it the way an Enterprise deal is - * provisioned. No subscription is created or changed here. A failed send never undoes the - * committed write; the warning is the fallback trail. - */ -async function notifyTeam( - license: AwsMarketplaceLicenseRecord, - organizationId: string | null, - userId: string -): Promise { - const parties = await getLinkParties(organizationId, userId) - const notice = { - licenseId: license.id, - organizationId, - dimension: license.dimension, - quantity: license.quantity, - expiresAt: license.expiresAt?.toISOString() ?? null, - } - const text = `AWS Marketplace license ${organizationId ? 'linked' : 'registered to an account'} -Taken: ${new Date().toISOString()} -Organization: ${organizationId ? `${parties.organizationName ?? 'Unknown'} (${organizationId})` : 'None yet'} -Buyer: ${parties.userName ?? 'Unknown'} <${parties.userEmail ?? 'unknown'}> (${userId}) -License: ${license.licenseArn} -AWS account: ${license.customerAwsAccountId ?? 'Not provided'} -Customer identifier: ${license.customerIdentifier} -Dimension: ${license.dimension ?? 'Not provided'} -Quantity: ${license.quantity ?? 'Not provided'} -Expires: ${notice.expiresAt ?? 'No expiry'} - -${TEAM_INSTRUCTIONS[organizationId ? 'organization' : 'account']} -` - const subject = organizationId - ? (parties.organizationName ?? organizationId) - : `${parties.userEmail ?? userId} (no organization)` - try { - const result = await sendEmail({ - to: [`enterprise@${env.EMAIL_DOMAIN || getEmailDomain()}`], - subject: `[AWS MARKETPLACE] ${subject}`, - text, - from: getFromEmailAddress(), - ...(parties.userEmail && { replyTo: parties.userEmail }), - emailType: 'transactional', - }) - if (result.success) { - logger.info('Notified the team of an AWS Marketplace license', notice) - return - } - logger.warn('Could not notify the team of an AWS Marketplace license', { - ...notice, - message: result.message, - }) - } catch (error) { - logger.warn('Could not notify the team of an AWS Marketplace license', { - ...notice, - message: getErrorMessage(error, 'Unknown error'), - }) - } -} - function licenseAuditMetadata(license: AwsMarketplaceLicenseRecord) { return { dimension: license.dimension, @@ -265,8 +214,8 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ const license = await consumeLicenseClaim(tx, { licenseId: input.licenseId, link: newlyLinked ? { organizationId: input.organizationId, userId: context.userId } : null, - summary, - verifiedAt, + contact: { email: input.contactEmail, companyName: input.companyName }, + verification: newerVerificationGate(locked, verifiedAt, summary), }) return { license, newlyLinked } }) @@ -285,7 +234,8 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ : [], afterSuccess: async ({ result, input, context }) => { if (result.newlyLinked) { - await notifyTeam(result.license, input.organizationId, context.userId) + await notifyTeamOfTakenLicense(result.license, input.organizationId, context.userId) + await notifyBuyerOfTakenLicense(result.license) } }, }) @@ -316,8 +266,8 @@ export const registerAwsMarketplaceLicenseToAccount: OperationUseCase< const consumed = await consumeLicenseClaim(tx, { licenseId: input.licenseId, link: newlyRegistered ? { organizationId: null, userId: principal.userId } : null, - summary, - verifiedAt, + contact: { email: input.contactEmail, companyName: input.companyName }, + verification: newerVerificationGate(locked, verifiedAt, summary), }) return { license: consumed, newlyRegistered } }) @@ -338,8 +288,21 @@ export const registerAwsMarketplaceLicenseToAccount: OperationUseCase< }, ] ) - await notifyTeam(license, null, principal.userId) + await notifyTeamOfTakenLicense(license, null, principal.userId) + await notifyBuyerOfTakenLicense(license) } return { licenseId: license.id } }, } + +/** + * The Marketplace licenses linked to an organization, so its members can see their subscription + * in Sim. Empty where Sim does not sell on AWS, so the billing page shows nothing there. + */ +export const listOrganizationAwsMarketplaceLicenses = defineAuthorizedOrganizationUseCase({ + operation: awsMarketplaceOperations.listOrganizationLicenses, + async execute({ context }: OrganizationUseCaseContext<{ organizationId: string }>) { + if (!getAwsMarketplaceProductCode()) return [] + return listOrganizationLicenses(context.organizationId) + }, +}) diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.ts index 73c2e5a2fa2..2234e0a17f7 100644 --- a/apps/sim/lib/billing/aws-marketplace/entitlements.ts +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.ts @@ -3,7 +3,7 @@ import { GetEntitlementsCommand, type GetEntitlementsCommandOutput, } from '@aws-sdk/client-marketplace-entitlement-service' -import { sleep } from '@sim/utils/helpers' +import { interruptibleSleep } from '@sim/utils/helpers' import { backoffWithJitter } from '@sim/utils/retry' import { compareStrings } from '@sim/utils/string' import { getEntitlementClient } from '@/lib/billing/aws-marketplace/client' @@ -87,14 +87,16 @@ export function isInvalidEntitlementRequest(error: unknown): boolean { } async function getEntitlementsPage( - command: GetEntitlementsCommand + command: GetEntitlementsCommand, + signal?: AbortSignal ): Promise { for (let attempt = 1; ; attempt++) { + signal?.throwIfAborted() try { - return await getEntitlementClient().send(command) + return await getEntitlementClient().send(command, { abortSignal: signal }) } catch (error) { - if (attempt >= MAX_ATTEMPTS || !isRetryable(error)) throw error - await sleep(backoffWithJitter(attempt, null, RETRY_BACKOFF)) + if (attempt >= MAX_ATTEMPTS || !isRetryable(error) || signal?.aborted) throw error + await interruptibleSleep(backoffWithJitter(attempt, null, RETRY_BACKOFF), signal) } } } @@ -102,7 +104,8 @@ async function getEntitlementsPage( /** Every entitlement AWS holds for one license of our product, across all pages. */ export async function fetchLicenseEntitlements( productCode: string, - licenseArn: string + licenseArn: string, + signal?: AbortSignal ): Promise { const entitlements: Entitlement[] = [] let nextToken: string | undefined @@ -112,7 +115,8 @@ export async function fetchLicenseEntitlements( ProductCode: productCode, Filter: { LICENSE_ARN: [licenseArn] }, NextToken: nextToken, - }) + }), + signal ) entitlements.push(...(result.Entitlements ?? [])) nextToken = result.NextToken @@ -121,11 +125,12 @@ export async function fetchLicenseEntitlements( throw new Error(`GetEntitlements returned more than ${MAX_PAGES} pages for one license`) } -/** Reads and summarizes one license's entitlements as of now. */ +/** Reads and summarizes one license's entitlements as of now; `signal` bounds the whole read. */ export async function verifyLicense( productCode: string, - licenseArn: string + licenseArn: string, + signal?: AbortSignal ): Promise { - const entitlements = await fetchLicenseEntitlements(productCode, licenseArn) + const entitlements = await fetchLicenseEntitlements(productCode, licenseArn, signal) return summarizeEntitlements(entitlements, productCode, new Date()) } diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts index de7b99d4ff6..a6de5c15725 100644 --- a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -10,12 +10,21 @@ import { NextRequest } from 'next/server' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const PRODUCT = 'product-placeholder' +/** New SaaS products: ResolveCustomer names the buyer by account ID, never `CustomerIdentifier`. */ +const ACCOUNT = '000000000000' +const AGREEMENT = 'agmt-placeholder' +const CONTACT = { contactEmail: 'buyer@example.test', companyName: 'Example Co' } -const { meteringSend, entitlementSend, queueSend } = vi.hoisted(() => { +const { meteringSend, entitlementSend, queueSend, brandLookup } = vi.hoisted(() => { process.env.AWS_MARKETPLACE_PRODUCT_CODE = 'product-placeholder' process.env.AWS_MARKETPLACE_EVENTS_QUEUE_URL = 'https://queue.example.test/aws-marketplace-placeholder' - return { meteringSend: vi.fn(), entitlementSend: vi.fn(), queueSend: vi.fn() } + return { + meteringSend: vi.fn(), + entitlementSend: vi.fn(), + queueSend: vi.fn(), + brandLookup: { fail: false }, + } }) vi.mock('@aws-sdk/client-marketplace-metering', () => ({ @@ -47,16 +56,27 @@ vi.mock('@aws-sdk/client-sqs', () => ({ constructor(public input: { ReceiptHandle: string }) {} }, })) +vi.mock('@/ee/whitelabeling', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getBrandConfig: () => { + if (brandLookup.fail) throw new Error('Brand lookup failed') + return actual.getBrandConfig() + }, + } +}) vi.mock('@/lib/messaging/email/mailer', () => emailMailerMock) vi.mock('@/lib/core/utils/request', () => requestUtilsMock) import { linkAwsMarketplaceLicense, + listOrganizationAwsMarketplaceLicenses, registerAwsMarketplaceLicenseToAccount, } from '@/lib/billing/aws-marketplace/application/use-cases' import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' import { reconcileAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/reconcile' -import { POST as register } from '@/app/api/marketplace/aws/register/route' +import { POST as register, GET as revisit } from '@/app/api/marketplace/aws/register/route' /** What GetEntitlements answers for each license ARN; an Error is thrown instead. */ const entitlementsByLicense = new Map() @@ -70,12 +90,15 @@ function uniqueArn(): string { return arn } +function uniqueAccount(): string { + return generateId().replace(/\D/g, '').padEnd(12, '7').slice(0, 12) +} + function currentEntitlement(overrides: Partial = {}): Entitlement { return { ProductCode: PRODUCT, Dimension: 'enterprise', - CustomerIdentifier: 'customer-placeholder', - CustomerAWSAccountId: '000000000000', + CustomerAWSAccountId: ACCOUNT, Value: { IntegerValue: 25 }, ExpirationDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), ...overrides, @@ -107,10 +130,10 @@ function registration(body: string, contentType = 'application/x-www-form-urlenc function resolvesTo(licenseArn: string, productCode = PRODUCT) { meteringSend.mockResolvedValueOnce({ - CustomerIdentifier: 'customer-placeholder', - CustomerAWSAccountId: '000000000000', + CustomerAWSAccountId: ACCOUNT, ProductCode: productCode, LicenseArn: licenseArn, + Metadata: { AgreementId: AGREEMENT }, }) } @@ -167,7 +190,7 @@ async function createClaimedLicense( id, licenseArn, productCode: PRODUCT, - customerIdentifier: 'customer-placeholder', + customerAwsAccountId: ACCOUNT, status: 'active', dimension: 'enterprise', quantity: 25, @@ -179,6 +202,21 @@ async function createClaimedLicense( return { id, licenseArn, token } } +interface SentEmail { + to: string[] + subject: string + text: string +} + +function sentEmails(to: RegExp): SentEmail[] { + return emailMailerMockFns.mockSendEmail.mock.calls + .map(([email]) => email as SentEmail) + .filter((email) => email.to.some((address) => to.test(address))) +} + +const TEAM = /^enterprise@/ +const BUYER = /^buyer@example\.test$/ + function rejection(promise: Promise): Promise { return promise.then( () => { @@ -290,6 +328,124 @@ describe('POST /api/marketplace/aws/register', () => { }) }) +describe('the fulfillment URL', () => { + it('registers a new-product buyer that ResolveCustomer names by account ID alone', async () => { + const licenseArn = uniqueArn() + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + + const response = await registration('x-amzn-marketplace-token=new-product-token') + + expect(response.headers.get('set-cookie')).toMatch(/sim_aws_marketplace_claim=/) + expect(await readLicense(licenseArn)).toMatchObject({ + status: 'active', + customerAwsAccountId: ACCOUNT, + customerIdentifier: null, + agreementId: AGREEMENT, + }) + }) + + it('does not let an older read restore a license the reconcile has since marked inactive', async () => { + const licenseArn = uniqueArn() + await db.insert(awsMarketplaceLicense).values({ + id: generateId(), + licenseArn, + productCode: PRODUCT, + customerAwsAccountId: ACCOUNT, + status: 'inactive', + lastVerifiedAt: new Date(Date.now() + 5 * 60 * 1000), + }) + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + + const response = await registration('x-amzn-marketplace-token=slow-token') + + expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( + 'no-subscription' + ) + expect(response.headers.get('set-cookie')).toBeNull() + expect(await readLicense(licenseArn)).toMatchObject({ + status: 'inactive', + claimTokenHash: null, + }) + }) + + it('sends a buyer whose agreement has not started back on its start date, saving nothing', async () => { + const account = uniqueAccount() + meteringSend.mockResolvedValueOnce({ CustomerAWSAccountId: account, ProductCode: PRODUCT }) + + const response = await registration('x-amzn-marketplace-token=future-dated-token') + + expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( + 'not-started' + ) + expect(response.headers.get('set-cookie')).toBeNull() + expect(entitlementSend).not.toHaveBeenCalled() + expect( + await db + .select({ id: awsMarketplaceLicense.id }) + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.customerAwsAccountId, account)) + ).toEqual([]) + }) + + it('accepts the token as multipart form data', async () => { + const licenseArn = uniqueArn() + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + const form = new FormData() + form.set('x-amzn-marketplace-token', 'multipart-token') + + const response = await register( + new NextRequest('http://localhost:3000/api/marketplace/aws/register', { + method: 'POST', + body: form, + }), + {} + ) + + expect(response.status).toBe(303) + expect(response.headers.get('set-cookie')).toMatch(/sim_aws_marketplace_claim=/) + expect(meteringSend.mock.calls[0][0].input.RegistrationToken).toBe('multipart-token') + }) + + it('sends a refreshed or bookmarked fulfillment URL to the onboarding page', async () => { + const response = await revisit( + new NextRequest('http://localhost:3000/api/marketplace/aws/register'), + {} + ) + + expect(response.status).toBe(303) + expect(new URL(response.headers.get('location') ?? '').pathname).toBe('/marketplace/aws') + expect(response.headers.get('set-cookie')).toBeNull() + }) +}) + +describe('an organization reading its Marketplace subscription', () => { + it('shows members only the licenses linked to their organization and refuses others', async () => { + const memberId = await createUser() + const outsiderId = await createUser() + const organizationId = await createOrganization([{ userId: memberId, role: 'member' }]) + const otherOrganizationId = await createOrganization([{ userId: outsiderId, role: 'owner' }]) + const ours = await createClaimedLicense({ organizationId, claimTokenHash: null }) + await createClaimedLicense({ organizationId: otherOrganizationId, claimTokenHash: null }) + + const licenses = await listOrganizationAwsMarketplaceLicenses.execute({ + principal: createSessionPrincipal({ userId: memberId }), + input: { organizationId }, + }) + const refusal = await rejection( + listOrganizationAwsMarketplaceLicenses.execute({ + principal: createSessionPrincipal({ userId: outsiderId }), + input: { organizationId }, + }) + ) + + expect(licenses).toEqual([expect.objectContaining({ id: ours.id, status: 'active' })]) + expect(refusal).toMatchObject({ code: 'not_found' }) + }) +}) + describe('linking a claimed license', () => { let ownerId: string let memberId: string @@ -307,7 +463,7 @@ describe('linking a claimed license', () => { function link(userId: string, input: { licenseId: string; claimToken: string | null }) { return linkAwsMarketplaceLicense.execute({ principal: createSessionPrincipal({ userId }), - input: { organizationId, ...input }, + input: { ...CONTACT, organizationId, ...input }, }) } @@ -374,9 +530,65 @@ describe('linking a claimed license', () => { organizationId: null, status: 'inactive', }) + expect(sentEmails(BUYER)).toEqual([]) + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), + ]) + }) + + it('refuses a claim whose read predates a newer inactive verification, and emails no one', async () => { + const license = await createClaimedLicense({ + status: 'inactive', + lastVerifiedAt: new Date(Date.now() + 5 * 60 * 1000), + }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + link(ownerId, { licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'conflict' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId: null, + status: 'inactive', + claimTokenHash: hashClaimToken(license.token), + }) expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() }) + it('links under a newer active read without overwriting it with its own older one', async () => { + const license = await createClaimedLicense({ lastVerifiedAt: new Date(Date.now() + 60_000) }) + entitlementsByLicense.set(license.licenseArn, [ + currentEntitlement({ Value: { IntegerValue: 40 } }), + ]) + + await link(ownerId, { licenseId: license.id, claimToken: license.token }) + + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId, + quantity: 25, + claimTokenHash: null, + }) + }) + + it('keeps a committed link when a notice lookup fails after the claim is spent', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + brandLookup.fail = true + + try { + await link(ownerId, { licenseId: license.id, claimToken: license.token }) + } finally { + brandLookup.fail = false + } + + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId, + claimTokenHash: null, + }) + expect(sentEmails(BUYER)).toEqual([]) + }) + it('links once, consumes the claim, audits the link, and notifies the team', async () => { const license = await createClaimedLicense() entitlementsByLicense.set(license.licenseArn, [ @@ -391,10 +603,16 @@ describe('linking a claimed license', () => { quantity: 40, claimTokenHash: null, claimExpiresAt: null, + contactEmail: CONTACT.contactEmail, + companyName: CONTACT.companyName, }) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) - expect(emailMailerMockFns.mockSendEmail.mock.calls[0][0].to).toEqual([ - expect.stringMatching(/^enterprise@/), + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/Company: Example Co/) }), + ]) + expect(sentEmails(BUYER)).toEqual([ + expect.objectContaining({ + text: expect.stringMatching(/within 2 business days[\s\S]*Questions\? Contact \S+@\S+/), + }), ]) await vi.waitFor(async () => { const entries = await db @@ -408,7 +626,7 @@ describe('linking a claimed license', () => { link(ownerId, { licenseId: license.id, claimToken: license.token }) ) expect(replay).toMatchObject({ code: 'forbidden' }) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) }) }) @@ -422,7 +640,7 @@ describe('registering a claimed license to an account', () => { function registerToAccount(input: { licenseId: string; claimToken: string | null }) { return registerAwsMarketplaceLicenseToAccount.execute({ principal: createSessionPrincipal({ userId: buyerId }), - input, + input: { ...CONTACT, ...input }, }) } @@ -440,11 +658,12 @@ describe('registering a claimed license to an account', () => { claimExpiresAt: null, }) expect(row?.linkedAt).toBeInstanceOf(Date) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) - expect(emailMailerMockFns.mockSendEmail.mock.calls[0][0]).toMatchObject({ - to: [expect.stringMatching(/^enterprise@/)], - text: expect.stringMatching(/no organization yet/), - }) + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/no organization yet/) }), + ]) + expect(sentEmails(BUYER)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/within 2 business days/) }), + ]) await vi.waitFor(async () => { const entries = await db .select({ action: auditLog.action }) @@ -457,7 +676,7 @@ describe('registering a claimed license to an account', () => { registerToAccount({ licenseId: license.id, claimToken: license.token }) ) expect(replay).toMatchObject({ code: 'forbidden' }) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(1) + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) }) it('refuses a browser that holds no claim', async () => { @@ -495,7 +714,10 @@ describe('registering a claimed license to an account', () => { status: 'inactive', claimTokenHash: hashClaimToken(license.token), }) - expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + expect(sentEmails(BUYER)).toEqual([]) + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), + ]) }) it('refuses a license already linked to an organization', async () => { @@ -515,6 +737,22 @@ describe('registering a claimed license to an account', () => { }) }) + it('refuses a claim whose verification predates a newer inactive read', async () => { + const license = await createClaimedLicense({ + status: 'inactive', + lastVerifiedAt: new Date(Date.now() + 60_000), + }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + + const error = await rejection( + registerToAccount({ licenseId: license.id, claimToken: license.token }) + ) + + expect(error).toMatchObject({ code: 'conflict' }) + expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() + expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + }) + it('lets a license registered to an account be linked to an organization later', async () => { const license = await createClaimedLicense() entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) @@ -530,7 +768,7 @@ describe('registering a claimed license to an account', () => { await linkAwsMarketplaceLicense.execute({ principal: createSessionPrincipal({ userId: buyerId }), - input: { organizationId, licenseId: license.id, claimToken: token }, + input: { ...CONTACT, organizationId, licenseId: license.id, claimToken: token }, }) expect(await readLicense(license.licenseArn)).toMatchObject({ @@ -538,7 +776,7 @@ describe('registering a claimed license to an account', () => { linkedByUserId: buyerId, claimTokenHash: null, }) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) + expect(sentEmails(TEAM)).toHaveLength(2) }) }) @@ -552,7 +790,7 @@ describe('reconcileAwsMarketplaceLicenses', () => { id: generateId(), licenseArn: eventArn, productCode: PRODUCT, - customerIdentifier: 'customer-placeholder', + customerAwsAccountId: ACCOUNT, status: 'active', lastVerifiedAt: new Date(Date.now() - 60_000), }, @@ -560,7 +798,7 @@ describe('reconcileAwsMarketplaceLicenses', () => { id: generateId(), licenseArn: staleArn, productCode: PRODUCT, - customerIdentifier: 'customer-placeholder', + customerAwsAccountId: ACCOUNT, status: 'active', dimension: 'enterprise', lastVerifiedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), @@ -605,5 +843,288 @@ describe('reconcileAwsMarketplaceLicenses', () => { const stale = await readLicense(staleArn) expect(stale).toMatchObject({ status: 'inactive', dimension: 'enterprise' }) expect(stale?.lastVerifiedAt.getTime()).toBeGreaterThan(Date.now() - 60_000) + expect(sentEmails(TEAM).map((email) => email.subject)).toEqual([ + expect.stringMatching(/active → inactive/), + expect.stringMatching(/active → inactive/), + ]) + }) + + it('sends the team nothing when a re-verify finds the same terms', async () => { + const licenseArn = uniqueArn() + const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) + await db.insert(awsMarketplaceLicense).values({ + id: generateId(), + licenseArn, + productCode: PRODUCT, + customerAwsAccountId: ACCOUNT, + status: 'active', + quantity: 25, + expiresAt, + lastVerifiedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), + }) + entitlementsByLicense.set(licenseArn, [currentEntitlement({ ExpirationDate: expiresAt })]) + queueSend.mockResolvedValue({ Messages: [] }) + + const summary = await reconcileAwsMarketplaceLicenses() + + expect(summary).toMatchObject({ reverified: 1 }) + expect((await readLicense(licenseArn))?.lastVerifiedAt.getTime()).toBeGreaterThan( + Date.now() - 60_000 + ) + expect(sentEmails(TEAM)).toEqual([]) + }) + + it('records a license an event names that nobody registered and tells the team who may own it', async () => { + const account = uniqueAccount() + const ownerId = await createUser() + const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) + await createClaimedLicense({ + organizationId, + customerAwsAccountId: account, + claimTokenHash: null, + }) + const renewedArn = uniqueArn() + entitlementsByLicense.set(renewedArn, [ + currentEntitlement({ CustomerAWSAccountId: account, Value: { IntegerValue: 60 } }), + ]) + let delivered = false + queueSend.mockImplementation(async (command: { kind: string }) => { + if (command.kind !== 'receive') return {} + if (delivered) return { Messages: [] } + delivered = true + return { + Messages: [ + { + MessageId: 'renewal', + ReceiptHandle: 'r-renewal', + Body: JSON.stringify({ + source: 'aws.agreement-marketplace', + 'detail-type': 'License Updated - Manufacturer', + detail: { license: { arn: renewedArn }, product: { code: PRODUCT } }, + }), + }, + ], + } + }) + + await reconcileAwsMarketplaceLicenses() + + expect(await readLicense(renewedArn)).toMatchObject({ + customerAwsAccountId: account, + organizationId: null, + status: 'active', + quantity: 60, + }) + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ + subject: expect.stringMatching(/New license/), + text: expect.stringContaining(`Marketplace organization (${organizationId})`), + }), + ]) + }) + it('stops receiving batches once the run budget cannot fit another', async () => { + vi.useFakeTimers({ toFake: ['Date'] }) + try { + queueSend.mockImplementation(async (command: { kind: string }) => { + if (command.kind !== 'receive') return {} + vi.setSystemTime(Date.now() + 60_000) + return { + Messages: [{ MessageId: generateId(), ReceiptHandle: generateId(), Body: '{not json' }], + } + }) + + const summary = await reconcileAwsMarketplaceLicenses() + + const receives = queueSend.mock.calls.filter(([command]) => command.kind === 'receive') + expect(summary?.outOfBudget).toBe(true) + expect(receives.length).toBeGreaterThan(0) + expect(receives.length).toBeLessThan(10) + } finally { + vi.useRealTimers() + } + }) + + it('keeps licenses AWS keeps refusing from starving the rest of the re-verify', async () => { + const failing = Array.from({ length: 50 }, () => uniqueArn()) + const healthyArn = uniqueArn() + const longAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000) + await db.insert(awsMarketplaceLicense).values([ + ...failing.map((licenseArn) => ({ + id: generateId(), + licenseArn, + productCode: PRODUCT, + customerAwsAccountId: ACCOUNT, + status: 'active' as const, + lastVerifiedAt: longAgo(3), + })), + { + id: generateId(), + licenseArn: healthyArn, + productCode: PRODUCT, + customerAwsAccountId: ACCOUNT, + status: 'active' as const, + lastVerifiedAt: longAgo(2), + }, + ]) + const rejected = Object.assign(new Error('Invalid license'), { + name: 'InvalidParameterException', + $metadata: { httpStatusCode: 400 }, + }) + for (const licenseArn of failing) entitlementsByLicense.set(licenseArn, rejected) + entitlementsByLicense.set(healthyArn, [currentEntitlement()]) + queueSend.mockResolvedValue({ Messages: [] }) + + const first = await reconcileAwsMarketplaceLicenses() + const second = await reconcileAwsMarketplaceLicenses() + + expect(first).toMatchObject({ reverified: 0, reverifyFailed: 50 }) + expect(second).toMatchObject({ reverified: 1, reverifyFailed: 0 }) + expect((await readLicense(healthyArn))?.lastVerifiedAt.getTime()).toBeGreaterThan( + Date.now() - 60_000 + ) + }) + + /** One batch of `events`, then an empty queue; returns the receipt handles deleted. */ + function deliverOnce(events: Array>) { + const deleted: string[] = [] + let delivered = false + queueSend.mockImplementation( + async (command: { kind: string; input: { ReceiptHandle: string } }) => { + if (command.kind === 'delete') { + deleted.push(command.input.ReceiptHandle) + return {} + } + if (delivered) return { Messages: [] } + delivered = true + return { + Messages: events.map((event, index) => ({ + MessageId: `m-${index}`, + ReceiptHandle: `r-${index}`, + Body: JSON.stringify({ source: 'aws.agreement-marketplace', ...event }), + })), + } + } + ) + return deleted + } + + it('re-verifies the license of an ended agreement, from an event that names no product', async () => { + const agreementId = `agmt-${generateId()}` + const license = await createClaimedLicense({ agreementId, claimTokenHash: null }) + entitlementsByLicense.set(license.licenseArn, []) + const deleted = deliverOnce([ + { + 'detail-type': 'Purchase Agreement Ended - Proposer', + detail: { agreement: { id: agreementId } }, + }, + ]) + + const summary = await reconcileAwsMarketplaceLicenses() + + expect(summary).toMatchObject({ applied: 1, discarded: 0 }) + expect(deleted).toEqual(['r-0']) + expect((await readLicense(license.licenseArn))?.status).toBe('inactive') + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), + ]) + }) + + it('discards an ended agreement it holds no license for without asking AWS', async () => { + const deleted = deliverOnce([ + { + 'detail-type': 'Purchase Agreement Ended - Manufacturer', + detail: { agreement: { id: `agmt-${generateId()}` }, product: { code: 'other-product' } }, + }, + ]) + + const summary = await reconcileAwsMarketplaceLicenses() + + expect(summary).toMatchObject({ applied: 0, discarded: 1 }) + expect(deleted).toEqual(['r-0']) + expect(entitlementSend).not.toHaveBeenCalled() + expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + }) + + it('tells the team once of a renewal delivered to both seller roles, linking nothing', async () => { + const ownerId = await createUser() + const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) + const previousAgreementId = `agmt-${generateId()}` + const renewedAgreementId = `agmt-${generateId()}` + const license = await createClaimedLicense({ + organizationId, + agreementId: previousAgreementId, + claimTokenHash: null, + }) + const renewal = { + detail: { + agreement: { id: renewedAgreementId, intent: 'RENEW', previousAgreementId }, + }, + } + const deleted = deliverOnce([ + { 'detail-type': 'Purchase Agreement Created - Proposer', ...renewal }, + { 'detail-type': 'Purchase Agreement Created - Manufacturer', ...renewal }, + ]) + + await reconcileAwsMarketplaceLicenses() + + expect(deleted.sort()).toEqual(['r-0', 'r-1']) + expect(sentEmails(TEAM)).toEqual([ + expect.objectContaining({ + subject: expect.stringMatching(/Agreement renewed/), + text: expect.stringContaining(`Marketplace organization (${organizationId})`), + }), + ]) + expect(await readLicense(license.licenseArn)).toMatchObject({ + organizationId, + agreementId: previousAgreementId, + renewedAgreementId, + }) + + emailMailerMockFns.mockSendEmail.mockClear() + entitlementsByLicense.set(license.licenseArn, []) + deliverOnce([ + { + 'detail-type': 'Purchase Agreement Ended - Manufacturer', + detail: { agreement: { id: renewedAgreementId } }, + }, + ]) + + await reconcileAwsMarketplaceLicenses() + + expect((await readLicense(license.licenseArn))?.status).toBe('inactive') + }) + + it('discards a created agreement that renews nothing it holds', async () => { + const stored = `agmt-${generateId()}` + const license = await createClaimedLicense({ agreementId: stored, claimTokenHash: null }) + const deleted = deliverOnce([ + { + 'detail-type': 'Purchase Agreement Created - Proposer', + detail: { agreement: { id: `agmt-${generateId()}`, intent: 'NEW' } }, + }, + { + 'detail-type': 'Purchase Agreement Created - Manufacturer', + detail: { + agreement: { id: `agmt-${generateId()}`, intent: 'NEW', previousAgreementId: stored }, + }, + }, + { + 'detail-type': 'Purchase Agreement Created - Manufacturer', + detail: { + agreement: { + id: `agmt-${generateId()}`, + intent: 'RENEW', + previousAgreementId: `agmt-${generateId()}`, + }, + }, + }, + ]) + + const summary = await reconcileAwsMarketplaceLicenses() + + expect(summary).toMatchObject({ applied: 0, discarded: 3 }) + expect(deleted.sort()).toEqual(['r-0', 'r-1', 'r-2']) + expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + expect((await readLicense(license.licenseArn))?.renewedAgreementId).toBeNull() }) }) diff --git a/apps/sim/lib/billing/aws-marketplace/notifications.ts b/apps/sim/lib/billing/aws-marketplace/notifications.ts new file mode 100644 index 00000000000..b64c8b64461 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/notifications.ts @@ -0,0 +1,264 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { AWS_MARKETPLACE_SETUP_TURNAROUND } from '@/lib/billing/aws-marketplace/onboarding' +import { + type AwsMarketplaceLicenseRecord, + getLinkParties, + type LicenseTerms, + type LicenseVerificationOutcome, + listOrganizationsLinkedToAccount, +} from '@/lib/billing/aws-marketplace/repository' +import { env } from '@/lib/core/config/env' +import { getEmailDomain } from '@/lib/core/utils/urls' +import { sendEmail } from '@/lib/messaging/email/mailer' +import { getFromEmailAddress } from '@/lib/messaging/email/utils' +import { getBrandConfig } from '@/ee/whitelabeling' + +const logger = createLogger('AwsMarketplaceNotifications') + +function teamInbox(): string { + return `enterprise@${env.EMAIL_DOMAIN || getEmailDomain()}` +} + +function formatTerms(terms: LicenseTerms): string { + return `${terms.status}, quantity ${terms.quantity ?? 'not provided'}, expires ${ + terms.expiresAt?.toISOString() ?? 'never' + }` +} + +function licenseLines(license: AwsMarketplaceLicenseRecord): string { + return `License: ${license.licenseArn} +AWS account: ${license.customerAwsAccountId} +Agreement: ${license.agreementId ?? 'Not provided'} +Dimension: ${license.dimension ?? 'Not provided'} +Terms: ${formatTerms(license)}` +} + +/** Provisioning is manual, so a failed send is logged and never undoes the write it reports. */ +async function send( + notice: string, + licenseId: string, + email: { to: string; subject: string; text: string; replyTo?: string | null } +): Promise { + try { + const result = await sendEmail({ + to: [email.to], + subject: email.subject, + text: email.text, + from: getFromEmailAddress(), + ...(email.replyTo && { replyTo: email.replyTo }), + emailType: 'transactional', + }) + if (result.success) { + logger.info('Sent an AWS Marketplace notice', { notice, licenseId }) + return + } + logger.warn('Could not send an AWS Marketplace notice', { + notice, + licenseId, + message: result.message, + }) + } catch (error) { + logger.warn('Could not send an AWS Marketplace notice', { + notice, + licenseId, + message: getErrorMessage(error, 'Unknown error'), + }) + } +} + +/** + * Every notice runs after its write committed, with the claim already spent, so nothing in it + * (the lookups included) may fail the request; the warning is the fallback trail. + */ +async function bestEffort( + notice: string, + licenseId: string | null, + notify: () => Promise +): Promise { + try { + await notify() + } catch (error) { + logger.warn('Could not prepare an AWS Marketplace notice', { + notice, + licenseId, + message: getErrorMessage(error, 'Unknown error'), + }) + } +} + +/** + * Tells the team a buyer took a license, for an organization or for their account alone, so + * they provision it the way an Enterprise deal is provisioned. No subscription is changed here. + */ +export function notifyTeamOfTakenLicense( + license: AwsMarketplaceLicenseRecord, + organizationId: string | null, + userId: string +): Promise { + return bestEffort('team notice of a taken AWS Marketplace license', license.id, () => + sendTakenLicenseNotice(license, organizationId, userId) + ) +} + +async function sendTakenLicenseNotice( + license: AwsMarketplaceLicenseRecord, + organizationId: string | null, + userId: string +): Promise { + const parties = await getLinkParties(organizationId, userId) + const organization = organizationId + ? `${parties.organizationName ?? 'Unknown'} (${organizationId})` + : 'None yet' + const instructions = organizationId + ? 'Provision this organization the way Enterprise deals are provisioned. No subscription was changed.' + : 'Registered to an account, no organization yet. Create the Enterprise organization for this buyer and link it. No subscription was changed.' + await send('team notice of a taken AWS Marketplace license', license.id, { + to: teamInbox(), + subject: `[AWS MARKETPLACE] ${ + organizationId + ? (parties.organizationName ?? organizationId) + : `${license.contactEmail ?? parties.userEmail ?? userId} (no organization)` + }`, + replyTo: license.contactEmail ?? parties.userEmail, + text: `AWS Marketplace license ${organizationId ? 'linked' : 'registered to an account'} +Organization: ${organization} +Taken by: ${parties.userName ?? 'Unknown'} <${parties.userEmail ?? 'unknown'}> (${userId}) +Contact email: ${license.contactEmail ?? 'Not provided'} +Company: ${license.companyName ?? 'Not provided'} +${licenseLines(license)} + +${instructions} +`, + }) +} + +/** Confirms the registration to the buyer and says what happens next. */ +export function notifyBuyerOfTakenLicense(license: AwsMarketplaceLicenseRecord): Promise { + return bestEffort('buyer confirmation of an AWS Marketplace license', license.id, () => + sendBuyerConfirmation(license) + ) +} + +async function sendBuyerConfirmation(license: AwsMarketplaceLicenseRecord): Promise { + if (!license.contactEmail) return + const brand = getBrandConfig() + await send('buyer confirmation of an AWS Marketplace license', license.id, { + to: license.contactEmail, + subject: `Your ${brand.name} AWS Marketplace subscription is registered`, + replyTo: brand.supportEmail, + text: `Your ${brand.name} subscription from AWS Marketplace is registered. + +Next steps: our team will set up your Enterprise workspace ${AWS_MARKETPLACE_SETUP_TURNAROUND} and email you when it is ready. + +Questions? Contact ${brand.supportEmail}. +`, + }) +} + +/** + * Tells the team when AWS changed what a license grants, or described a license nobody has + * registered, so a lapsed buyer does not keep Enterprise and a renewal is not missed. An + * unchanged re-verify sends nothing. A new license is never linked automatically. + */ +export function notifyTeamOfVerification(outcome: LicenseVerificationOutcome): Promise { + if (outcome.kind === 'skipped' || (outcome.kind === 'updated' && !outcome.changed)) { + return Promise.resolve() + } + return bestEffort('team notice of an AWS Marketplace license change', outcome.license.id, () => + sendVerificationNotice(outcome) + ) +} + +async function sendVerificationNotice( + outcome: Exclude +): Promise { + const { license } = outcome + if (outcome.kind === 'created') { + const linked = await listOrganizationsLinkedToAccount({ + customerAwsAccountId: license.customerAwsAccountId, + productCode: license.productCode, + excludingLicenseArn: license.licenseArn, + }) + await send('team notice of a new AWS Marketplace license', license.id, { + to: teamInbox(), + subject: `[AWS MARKETPLACE] New license for AWS account ${license.customerAwsAccountId}`, + text: `A new AWS Marketplace license appeared that no buyer has registered. +${licenseLines(license)} +Organizations linked to other licenses of this account: ${ + linked.length ? linked.map((org) => `${org.name} (${org.id})`).join(', ') : 'None' + } + +It is not linked to any organization. Link it if it renews an existing customer. +`, + }) + return + } + const parties = license.organizationId + ? await getLinkParties(license.organizationId, license.linkedByUserId) + : null + await send('team notice of a changed AWS Marketplace license', license.id, { + to: teamInbox(), + subject: `[AWS MARKETPLACE] License ${outcome.before.status} → ${license.status}: ${ + parties?.organizationName ?? `AWS account ${license.customerAwsAccountId}` + }`, + text: `AWS Marketplace changed what this license grants. +Organization: ${ + license.organizationId + ? `${parties?.organizationName ?? 'Unknown'} (${license.organizationId})` + : 'None' + } +${licenseLines(license)} +Before: ${formatTerms(outcome.before)} +After: ${formatTerms(license)} + +Update the organization's Enterprise provisioning to match. +`, + }) +} + +/** + * Tells the team an agreement renewed, naming the organizations its licenses belong to. The + * renewal may issue a new license ARN (AWS does not say), so nothing is linked automatically. + */ +export function notifyTeamOfAgreementRenewal( + licenses: AwsMarketplaceLicenseRecord[], + renewal: { previousAgreementId: string; agreementId: string } +): Promise { + return bestEffort('team notice of a renewed AWS Marketplace agreement', null, () => + sendRenewalNotice(licenses, renewal) + ) +} + +async function sendRenewalNotice( + licenses: AwsMarketplaceLicenseRecord[], + renewal: { previousAgreementId: string; agreementId: string } +): Promise { + const described = await Promise.all( + licenses.map(async (license) => { + const { organizationName } = await getLinkParties(license.organizationId, null) + const organization = license.organizationId + ? `${organizationName ?? 'Unknown'} (${license.organizationId})` + : 'None' + return { license, organization, organizationName } + }) + ) + const first = described[0] + if (!first) return + await send('team notice of a renewed AWS Marketplace agreement', first.license.id, { + to: teamInbox(), + subject: `[AWS MARKETPLACE] Agreement renewed: ${ + first.organizationName ?? `AWS account ${first.license.customerAwsAccountId}` + }`, + text: `An AWS Marketplace agreement renewed. +Previous agreement: ${renewal.previousAgreementId} +New agreement: ${renewal.agreementId} + +${described + .map(({ license, organization }) => `Organization: ${organization}\n${licenseLines(license)}`) + .join('\n\n')} + +The renewal may issue a new license. If one appears, link it to the same organization. Nothing was linked automatically. +`, + }) +} diff --git a/apps/sim/lib/billing/aws-marketplace/onboarding.ts b/apps/sim/lib/billing/aws-marketplace/onboarding.ts index 964a3db7cf0..bf463b1820d 100644 --- a/apps/sim/lib/billing/aws-marketplace/onboarding.ts +++ b/apps/sim/lib/billing/aws-marketplace/onboarding.ts @@ -5,6 +5,10 @@ export const AWS_MARKETPLACE_ONBOARDING_PATH = '/marketplace/aws' export const AWS_MARKETPLACE_ONBOARDING_ERRORS = [ 'invalid', 'no-subscription', + 'not-started', 'unavailable', ] as const export type AwsMarketplaceOnboardingError = (typeof AWS_MARKETPLACE_ONBOARDING_ERRORS)[number] + +/** What the buyer is promised on the onboarding page and in their confirmation email. */ +export const AWS_MARKETPLACE_SETUP_TURNAROUND = 'within 2 business days' diff --git a/apps/sim/lib/billing/aws-marketplace/reconcile.ts b/apps/sim/lib/billing/aws-marketplace/reconcile.ts index 2ea1c7224a0..0cc5e3271a6 100644 --- a/apps/sim/lib/billing/aws-marketplace/reconcile.ts +++ b/apps/sim/lib/billing/aws-marketplace/reconcile.ts @@ -10,7 +10,14 @@ import { verifyLicense, } from '@/lib/billing/aws-marketplace/entitlements' import { - listLicensesVerifiedBefore, + notifyTeamOfAgreementRenewal, + notifyTeamOfVerification, +} from '@/lib/billing/aws-marketplace/notifications' +import { + listLicensesDueForReverify, + listLicensesForAgreement, + markLicensesRenewed, + markVerificationAttempted, recordLicenseVerification, } from '@/lib/billing/aws-marketplace/repository' import { env } from '@/lib/core/config/env' @@ -22,13 +29,30 @@ const LICENSE_EVENT_TYPES = new Set([ 'License Updated - Manufacturer', 'License Deprovisioned - Manufacturer', ]) +const AGREEMENT_ENDED_EVENT_TYPES = new Set([ + 'Purchase Agreement Ended - Proposer', + 'Purchase Agreement Ended - Manufacturer', +]) +const AGREEMENT_CREATED_EVENT_TYPES = new Set([ + 'Purchase Agreement Created - Proposer', + 'Purchase Agreement Created - Manufacturer', +]) /** SQS caps one receive at 10 messages; a run drains at most this many batches. */ const RECEIVE_BATCH_SIZE = 10 const MAX_RECEIVE_BATCHES = 10 -/** Long enough for a batch of entitlement reads with retries; unhandled messages reappear after it. */ -const VISIBILITY_TIMEOUT_SECONDS = 120 const RECEIVE_WAIT_SECONDS = 2 +/** One message's or stale license's entitlement reads, retries included, are aborted past this. */ +const MESSAGE_BUDGET_MS = 15_000 +/** Messages in a batch are handled one after another. */ +const BATCH_BUDGET_MS = RECEIVE_BATCH_SIZE * MESSAGE_BUDGET_MS +/** + * 10 messages × 15 s = 150 s per batch, so each receive hides its batch for 180 s (above the work, + * independent of the queue's 5-minute default), and a run starts no batch or re-verify that could + * end past 6 minutes, well inside the cron's 10-minute lock. + */ +const VISIBILITY_TIMEOUT_SECONDS = 180 +const RUN_BUDGET_MS = 6 * 60 * 1000 const REVERIFY_AFTER_MS = 24 * 60 * 60 * 1000 const REVERIFY_BATCH_SIZE = 50 @@ -40,12 +64,28 @@ interface AwsMarketplaceReconcileSummary { retained: number reverified: number reverifyFailed: number + /** The run stopped draining or re-verifying because its wall-clock budget ran out. */ + outOfBudget: boolean } -type LicenseEvent = { licenseArn: string; productCode: string } | null +/** Whether work that may take up to `workMs` still ends inside the run's budget. */ +type BudgetCheck = (workMs: number) => boolean + +type MarketplaceEvent = + | { kind: 'license'; licenseArn: string; productCode: string; agreementId: string | null } + | { kind: 'agreement-ended'; agreementId: string } + | { + kind: 'agreement-created' + agreementId: string + intent: string | null + previousAgreementId: string | null + } -/** The license an agreement event names, or null for a body that is not one. */ -function parseLicenseEvent(body: string | undefined): LicenseEvent { +/** + * The event a queue message carries, or null for a body that is not one. Agreement events name no + * license, and the Proposer Ended event names no product, so they are matched by agreement ID. + */ +function parseMarketplaceEvent(body: string | undefined): MarketplaceEvent | null { if (!body) return null let event: Record try { @@ -54,56 +94,130 @@ function parseLicenseEvent(body: string | undefined): LicenseEvent { return null } const detailType = toStringOrNull(event['detail-type']) - if (event.source !== EVENT_SOURCE || !detailType || !LICENSE_EVENT_TYPES.has(detailType)) { - return null - } + if (event.source !== EVENT_SOURCE || !detailType) return null const detail = toRecord(event.detail) - const licenseArn = toStringOrNull(toRecord(detail.license).arn) - const productCode = toStringOrNull(toRecord(detail.product).code) - return licenseArn && productCode ? { licenseArn, productCode } : null + const agreement = toRecord(detail.agreement) + const agreementId = toStringOrNull(agreement.id) + if (LICENSE_EVENT_TYPES.has(detailType)) { + const licenseArn = toStringOrNull(toRecord(detail.license).arn) + const productCode = toStringOrNull(toRecord(detail.product).code) + return licenseArn && productCode + ? { kind: 'license', licenseArn, productCode, agreementId } + : null + } + if (!agreementId) return null + if (AGREEMENT_ENDED_EVENT_TYPES.has(detailType)) return { kind: 'agreement-ended', agreementId } + if (AGREEMENT_CREATED_EVENT_TYPES.has(detailType)) { + return { + kind: 'agreement-created', + agreementId, + intent: toStringOrNull(agreement.intent), + previousAgreementId: toStringOrNull(agreement.previousAgreementId), + } + } + return null } type MessageOutcome = 'applied' | 'discarded' | 'retained' +/** Stamps the attempt, asks the Entitlement Service, records its answer, and tells the team. */ +async function reverifyLicense( + productCode: string, + licenseArn: string, + signal: AbortSignal, + agreementId?: string | null +): Promise { + const verifiedAt = new Date() + await markVerificationAttempted(licenseArn, verifiedAt) + const summary = await verifyLicense(productCode, licenseArn, signal) + const outcome = await recordLicenseVerification({ + licenseArn, + productCode, + agreementId, + summary, + verifiedAt, + }) + await notifyTeamOfVerification(outcome) +} + +/** An agreement ID we hold no license for is normal: the seller account has other products. */ +async function applyAgreementEnded( + productCode: string, + agreementId: string, + messageId: string | undefined, + signal: AbortSignal +): Promise { + const licenseArns = await listLicensesForAgreement(productCode, agreementId) + if (licenseArns.length === 0) { + logger.info('Ignoring an AWS Marketplace agreement event for no stored license', { messageId }) + return 'discarded' + } + for (const licenseArn of licenseArns) { + await reverifyLicense(productCode, licenseArn, signal) + } + return 'applied' +} + +async function applyAgreementCreated( + productCode: string, + event: Extract, + messageId: string | undefined +): Promise { + const previousAgreementId = event.intent === 'RENEW' ? event.previousAgreementId : null + const renewed = previousAgreementId + ? await markLicensesRenewed({ + productCode, + previousAgreementId, + agreementId: event.agreementId, + }) + : [] + if (!previousAgreementId || renewed.length === 0) { + logger.info('Ignoring an AWS Marketplace agreement event for no stored license', { messageId }) + return 'discarded' + } + await notifyTeamOfAgreementRenewal(renewed, { + previousAgreementId, + agreementId: event.agreementId, + }) + return 'applied' +} + /** - * Re-reads the license an event names rather than trusting the event's own fields. A message that - * can never apply is discarded; one that failed for a reason that may pass is retained, so SQS + * Re-reads what an event concerns rather than trusting the event's own fields. A message that can + * never apply is discarded; one that failed for a reason that may pass is retained, so SQS * redelivers it and the dead-letter queue catches one that never succeeds. */ async function handleMessage(productCode: string, message: Message): Promise { - const event = parseLicenseEvent(message.Body) + const messageId = message.MessageId + const event = parseMarketplaceEvent(message.Body) if (!event) { - logger.warn('Discarding an AWS Marketplace event that is not a license event', { - messageId: message.MessageId, - }) + logger.warn('Discarding an unrecognized AWS Marketplace event', { messageId }) return 'discarded' } - if (event.productCode !== productCode) { - logger.warn('Discarding an AWS Marketplace event for another product', { - messageId: message.MessageId, - }) + if (event.kind === 'license' && event.productCode !== productCode) { + logger.warn('Discarding an AWS Marketplace event for another product', { messageId }) return 'discarded' } - const verifiedAt = new Date() try { - const summary = await verifyLicense(productCode, event.licenseArn) - await recordLicenseVerification({ - licenseArn: event.licenseArn, - productCode, - summary, - verifiedAt, - }) + const signal = AbortSignal.timeout(MESSAGE_BUDGET_MS) + if (event.kind === 'agreement-ended') { + return await applyAgreementEnded(productCode, event.agreementId, messageId, signal) + } + if (event.kind === 'agreement-created') { + return await applyAgreementCreated(productCode, event, messageId) + } + await reverifyLicense(productCode, event.licenseArn, signal, event.agreementId) return 'applied' } catch (error) { if (isInvalidEntitlementRequest(error)) { logger.warn('Discarding an AWS Marketplace event the Entitlement Service rejected', { - messageId: message.MessageId, + messageId, message: getErrorMessage(error, 'Unknown error'), }) return 'discarded' } logger.warn('Leaving an AWS Marketplace event for redelivery', { - messageId: message.MessageId, + messageId, message: getErrorMessage(error, 'Unknown error'), }) return 'retained' @@ -113,10 +227,15 @@ async function handleMessage(productCode: string, message: Message): Promise { const client = getEventsQueueClient() for (let batch = 0; batch < MAX_RECEIVE_BATCHES; batch++) { + if (!fits(BATCH_BUDGET_MS)) { + summary.outOfBudget = true + return + } let messages: Message[] try { const result = await client.send( @@ -157,20 +276,18 @@ async function drainEvents( async function reverifyStaleLicenses( productCode: string, - summary: AwsMarketplaceReconcileSummary + summary: AwsMarketplaceReconcileSummary, + fits: BudgetCheck ): Promise { const cutoff = new Date(Date.now() - REVERIFY_AFTER_MS) - const licenseArns = await listLicensesVerifiedBefore(cutoff, REVERIFY_BATCH_SIZE) + const licenseArns = await listLicensesDueForReverify(cutoff, REVERIFY_BATCH_SIZE) for (const licenseArn of licenseArns) { - const verifiedAt = new Date() + if (!fits(MESSAGE_BUDGET_MS)) { + summary.outOfBudget = true + return + } try { - const entitlements = await verifyLicense(productCode, licenseArn) - await recordLicenseVerification({ - licenseArn, - productCode, - summary: entitlements, - verifiedAt, - }) + await reverifyLicense(productCode, licenseArn, AbortSignal.timeout(MESSAGE_BUDGET_MS)) summary.reverified++ } catch (error) { summary.reverifyFailed++ @@ -182,9 +299,10 @@ async function reverifyStaleLicenses( } /** - * Applies queued agreement events, then re-reads licenses no event touched within a day. Both - * passes are bounded and idempotent: every write is the Entitlement Service's current answer, and - * an older read never overwrites a newer one. Returns null when the feature is off. + * Applies queued agreement events, then re-reads licenses not attempted within a day. Both passes + * are bounded in count and wall-clock time, and idempotent: every write is the Entitlement + * Service's current answer, and an older read never overwrites a newer one. Returns null when the + * feature is off. */ export async function reconcileAwsMarketplaceLicenses(): Promise { const productCode = getAwsMarketplaceProductCode() @@ -196,9 +314,12 @@ export async function reconcileAwsMarketplaceLicenses(): Promise Date.now() - startedAt + workMs <= RUN_BUDGET_MS const queueUrl = env.AWS_MARKETPLACE_EVENTS_QUEUE_URL - if (queueUrl) await drainEvents(productCode, queueUrl, summary) - await reverifyStaleLicenses(productCode, summary) + if (queueUrl) await drainEvents(productCode, queueUrl, summary, fits) + await reverifyStaleLicenses(productCode, summary, fits) return summary } diff --git a/apps/sim/lib/billing/aws-marketplace/registration.ts b/apps/sim/lib/billing/aws-marketplace/registration.ts index a6752f1da33..ce68be54f01 100644 --- a/apps/sim/lib/billing/aws-marketplace/registration.ts +++ b/apps/sim/lib/billing/aws-marketplace/registration.ts @@ -54,13 +54,19 @@ export async function registerAwsMarketplaceBuyer( logger.warn('AWS Marketplace registration is for another product') return { kind: 'invalid' } } - if (!customer.CustomerIdentifier || !customer.LicenseArn) { - logger.warn('AWS Marketplace registration resolved without a customer or license', { - hasCustomer: Boolean(customer.CustomerIdentifier), - hasLicense: Boolean(customer.LicenseArn), - }) + /** New SaaS integrations get no `CustomerIdentifier`; the account ID and license ARN name the buyer. */ + if (!customer.CustomerAWSAccountId) { + logger.warn('AWS Marketplace registration resolved without an account') return { kind: 'invalid' } } + /** + * A future-dated agreement gets its license ARN on its start date. Nothing is saved, since the + * ARN is the row's key; the License Updated event for it records the row then. + */ + if (!customer.LicenseArn) { + logger.info('AWS Marketplace registration is for an agreement that has not started') + return { kind: 'not-started' } + } const verifiedAt = new Date() let summary: EntitlementSummary @@ -75,17 +81,23 @@ export async function registerAwsMarketplaceBuyer( } const claim = summary.active ? issueClaimToken() : null - const licenseId = await saveRegisteredLicense({ + const saved = await saveRegisteredLicense({ licenseArn: customer.LicenseArn, productCode, - customerIdentifier: customer.CustomerIdentifier, - customerAwsAccountId: customer.CustomerAWSAccountId ?? null, + customerAwsAccountId: customer.CustomerAWSAccountId, + customerIdentifier: customer.CustomerIdentifier ?? null, + agreementId: customer.Metadata?.AgreementId ?? null, summary, verifiedAt, claim: claim ? { hash: claim.hash, expiresAt: new Date(Date.now() + AWS_MARKETPLACE_CLAIM_TTL_MS) } : null, }) - logger.info('Registered an AWS Marketplace license', { licenseId, active: summary.active }) - return claim ? { kind: 'claimed', claimToken: claim.token } : { kind: 'no-subscription' } + logger.info('Registered an AWS Marketplace license', { + licenseId: saved.id, + active: saved.active, + }) + return claim && saved.active + ? { kind: 'claimed', claimToken: claim.token } + : { kind: 'no-subscription' } } diff --git a/apps/sim/lib/billing/aws-marketplace/repository.ts b/apps/sim/lib/billing/aws-marketplace/repository.ts index 54d88c68842..e708cca642a 100644 --- a/apps/sim/lib/billing/aws-marketplace/repository.ts +++ b/apps/sim/lib/billing/aws-marketplace/repository.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { awsMarketplaceLicense, member, organization, user } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, asc, eq, inArray, lte } from 'drizzle-orm' +import { and, asc, eq, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm' import type { EntitlementSummary } from '@/lib/billing/aws-marketplace/entitlements' import type { DbTransaction } from '@/lib/db/types' @@ -30,85 +30,240 @@ function verificationFields(summary: EntitlementSummary, verifiedAt: Date) { } } +/** Whether a read that started at `verifiedAt` may overwrite what the row last recorded. */ +function isNewerRead(license: AwsMarketplaceLicenseRecord, verifiedAt: Date): boolean { + return license.lastVerifiedAt.getTime() <= verifiedAt.getTime() +} + /** * Records a registration: the license row keyed by its ARN, as ResolveCustomer and the - * Entitlement Service just described it, with a fresh claim when it is active. An inactive - * registration clears any claim outstanding from an earlier one. + * Entitlement Service described it, with the claim when the license is active. Every writer of + * entitlement fields follows one rule: a read older than the row's last one changes none of them, + * so a slow registration cannot restore `active` after the reconcile recorded `inactive`. The + * claim is stored only while the newest answer is active, and an inactive one clears any claim + * outstanding from an earlier registration. */ export async function saveRegisteredLicense(input: { licenseArn: string productCode: string - customerIdentifier: string - customerAwsAccountId: string | null + customerAwsAccountId: string + customerIdentifier: string | null + agreementId: string | null summary: EntitlementSummary verifiedAt: Date claim: { hash: string; expiresAt: Date } | null -}): Promise { - const fields = { +}): Promise<{ id: string; active: boolean }> { + const identity = { productCode: input.productCode, - customerIdentifier: input.customerIdentifier, customerAwsAccountId: input.customerAwsAccountId, - ...verificationFields(input.summary, input.verifiedAt), - claimTokenHash: input.claim?.hash ?? null, - claimExpiresAt: input.claim?.expiresAt ?? null, + ...(input.customerIdentifier && { customerIdentifier: input.customerIdentifier }), + ...(input.agreementId && { agreementId: input.agreementId }), } - const [row] = await db - .insert(awsMarketplaceLicense) - .values({ id: generateId(), licenseArn: input.licenseArn, ...fields }) - .onConflictDoUpdate({ target: awsMarketplaceLicense.licenseArn, set: fields }) - .returning({ id: awsMarketplaceLicense.id }) - return row.id + const claimFields = (active: boolean) => ({ + claimTokenHash: active ? (input.claim?.hash ?? null) : null, + claimExpiresAt: active ? (input.claim?.expiresAt ?? null) : null, + }) + return db.transaction(async (tx) => { + const [created] = await tx + .insert(awsMarketplaceLicense) + .values({ + id: generateId(), + licenseArn: input.licenseArn, + ...identity, + ...verificationFields(input.summary, input.verifiedAt), + ...claimFields(input.summary.active), + }) + .onConflictDoNothing({ target: awsMarketplaceLicense.licenseArn }) + .returning({ id: awsMarketplaceLicense.id }) + if (created) return { id: created.id, active: input.summary.active } + + const [locked] = await tx + .select() + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.licenseArn, input.licenseArn)) + .for('update') + .limit(1) + const fresh = isNewerRead(locked, input.verifiedAt) + const active = fresh ? input.summary.active : locked.status === 'active' + await tx + .update(awsMarketplaceLicense) + .set({ + ...identity, + ...(fresh + ? verificationFields(input.summary, input.verifiedAt) + : { updatedAt: new Date() }), + ...claimFields(active && input.summary.active), + }) + .where(eq(awsMarketplaceLicense.id, locked.id)) + return { id: locked.id, active: active && input.summary.active } + }) +} + +/** The terms the team provisions from; a change to any of them needs their attention. */ +export interface LicenseTerms { + status: 'active' | 'inactive' + quantity: number | null + expiresAt: Date | null +} + +function termsOf(license: AwsMarketplaceLicenseRecord): LicenseTerms { + return { status: license.status, quantity: license.quantity, expiresAt: license.expiresAt } } +function termsDiffer(before: LicenseTerms, after: LicenseTerms): boolean { + return ( + before.status !== after.status || + before.quantity !== after.quantity || + (before.expiresAt?.getTime() ?? null) !== (after.expiresAt?.getTime() ?? null) + ) +} + +export type LicenseVerificationOutcome = + /** AWS described a license no row held yet; it is recorded with no organization. */ + | { kind: 'created'; license: AwsMarketplaceLicenseRecord } + | { + kind: 'updated' + before: LicenseTerms + license: AwsMarketplaceLicenseRecord + /** Status, quantity, or expiry moved; an unchanged re-verify is false. */ + changed: boolean + } + /** No row to update, or a read older than the row's last one. */ + | { kind: 'skipped' } + /** - * Applies a verification read that started at `verifiedAt`. A read older than the row's last one - * changes nothing, so overlapping reconcile runs and registrations cannot roll a license back. A - * license AWS describes but nobody registered is recorded so it is visible to the team; one AWS - * no longer describes is only updated if it exists. + * Applies a verification read that started at `verifiedAt`, comparing the terms before and after + * under one row lock. A read older than the row's last one changes nothing, so overlapping + * reconcile runs and registrations cannot roll a license back. A license AWS describes with an + * entitlement but nobody registered (a renewal may issue a new ARN) is recorded with no + * organization; one AWS no longer describes is only updated if it exists. */ export async function recordLicenseVerification(input: { licenseArn: string productCode: string + /** The agreement a license event names; a buyer who never registered has no other source. */ + agreementId?: string | null summary: EntitlementSummary verifiedAt: Date -}): Promise { - const fields = verificationFields(input.summary, input.verifiedAt) - const notNewer = lte(awsMarketplaceLicense.lastVerifiedAt, input.verifiedAt) - if (input.summary.customerIdentifier) { - await db - .insert(awsMarketplaceLicense) - .values({ - id: generateId(), - licenseArn: input.licenseArn, - productCode: input.productCode, - customerIdentifier: input.summary.customerIdentifier, - customerAwsAccountId: input.summary.customerAwsAccountId, - ...fields, - }) - .onConflictDoUpdate({ - target: awsMarketplaceLicense.licenseArn, - set: fields, - setWhere: notNewer, - }) - return +}): Promise { + const fields = { + ...verificationFields(input.summary, input.verifiedAt), + ...(input.agreementId && { agreementId: input.agreementId }), } - await db - .update(awsMarketplaceLicense) - .set(fields) - .where(and(eq(awsMarketplaceLicense.licenseArn, input.licenseArn), notNewer)) + return db.transaction(async (tx) => { + const accountId = input.summary.customerAwsAccountId + if (accountId && input.summary.entitlementCount > 0) { + const [created] = await tx + .insert(awsMarketplaceLicense) + .values({ + id: generateId(), + licenseArn: input.licenseArn, + productCode: input.productCode, + customerAwsAccountId: accountId, + customerIdentifier: input.summary.customerIdentifier, + ...fields, + }) + .onConflictDoNothing({ target: awsMarketplaceLicense.licenseArn }) + .returning() + if (created) return { kind: 'created', license: created } + } + const [locked] = await tx + .select() + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.licenseArn, input.licenseArn)) + .for('update') + .limit(1) + if (!locked || !isNewerRead(locked, input.verifiedAt)) return { kind: 'skipped' } + const [license] = await tx + .update(awsMarketplaceLicense) + .set(fields) + .where(eq(awsMarketplaceLicense.id, locked.id)) + .returning() + const before = termsOf(locked) + return { kind: 'updated', before, license, changed: termsDiffer(before, termsOf(license)) } + }) } -/** The licenses whose last verification is oldest, up to `limit`, none newer than `cutoff`. */ -export async function listLicensesVerifiedBefore(cutoff: Date, limit: number): Promise { +/** + * The licenses longest since their last re-verify attempt, up to `limit`, none attempted after + * `cutoff`. Ordering by the attempt rather than the success keeps a license AWS keeps refusing + * from holding the front of every batch. + */ +export async function listLicensesDueForReverify(cutoff: Date, limit: number): Promise { + const { lastVerificationAttemptAt, lastVerifiedAt } = awsMarketplaceLicense const rows = await db .select({ licenseArn: awsMarketplaceLicense.licenseArn }) .from(awsMarketplaceLicense) - .where(lte(awsMarketplaceLicense.lastVerifiedAt, cutoff)) - .orderBy(asc(awsMarketplaceLicense.lastVerifiedAt)) + .where( + or( + lt(lastVerificationAttemptAt, cutoff), + and(isNull(lastVerificationAttemptAt), lt(lastVerifiedAt, cutoff)) + ) + ) + .orderBy(asc(sql`coalesce(${lastVerificationAttemptAt}, ${lastVerifiedAt})`)) .limit(limit) return rows.map((row) => row.licenseArn) } +const MAX_LICENSES_PER_AGREEMENT = 20 + +function belongsToAgreement(productCode: string, agreementId: string) { + return and( + eq(awsMarketplaceLicense.productCode, productCode), + or( + eq(awsMarketplaceLicense.agreementId, agreementId), + eq(awsMarketplaceLicense.renewedAgreementId, agreementId) + ) + ) +} + +/** The licenses an agreement covers, by the ID they registered with or last renewed to. */ +export async function listLicensesForAgreement( + productCode: string, + agreementId: string +): Promise { + const rows = await db + .select({ licenseArn: awsMarketplaceLicense.licenseArn }) + .from(awsMarketplaceLicense) + .where(belongsToAgreement(productCode, agreementId)) + .orderBy(asc(awsMarketplaceLicense.createdAt)) + .limit(MAX_LICENSES_PER_AGREEMENT) + return rows.map((row) => row.licenseArn) +} + +/** + * Moves the licenses of the previous agreement to the renewal and returns those this call moved. + * The seller receives one renewal as both a Proposer and a Manufacturer event, and SQS may + * redeliver either; only the first finds rows to move, so the team hears of it once. + */ +export async function markLicensesRenewed(input: { + productCode: string + previousAgreementId: string + agreementId: string +}): Promise { + return db + .update(awsMarketplaceLicense) + .set({ renewedAgreementId: input.agreementId, updatedAt: new Date() }) + .where( + and( + belongsToAgreement(input.productCode, input.previousAgreementId), + or( + isNull(awsMarketplaceLicense.renewedAgreementId), + ne(awsMarketplaceLicense.renewedAgreementId, input.agreementId) + ) + ) + ) + .returning() +} + +/** Stamps an attempt to verify a license, before its outcome is known. */ +export async function markVerificationAttempted(licenseArn: string, attemptedAt: Date) { + await db + .update(awsMarketplaceLicense) + .set({ lastVerificationAttemptAt: attemptedAt }) + .where(eq(awsMarketplaceLicense.licenseArn, licenseArn)) +} + export async function findLicenseByClaimHash( claimTokenHash: string ): Promise { @@ -134,23 +289,28 @@ export async function lockLicense( } /** - * Consumes the claim and records the verification it relied on, taking the license when `link` - * is given: for an organization, or for an account alone when its organization is null. A - * license already taken by the same party keeps its original link. + * Consumes the claim and records the verification it relied on unless a newer one is stored, + * taking the license when `link` is given: for an organization, or for an account alone when its + * organization is null. A license already taken by the same party keeps its original link. */ export async function consumeLicenseClaim( tx: DbTransaction, input: { licenseId: string link: { organizationId: string | null; userId: string } | null - summary: EntitlementSummary - verifiedAt: Date + contact: { email: string; companyName: string | null } + /** Null when the row already holds a newer read, which this one must not overwrite. */ + verification: { summary: EntitlementSummary; verifiedAt: Date } | null } ): Promise { const [row] = await tx .update(awsMarketplaceLicense) .set({ - ...verificationFields(input.summary, input.verifiedAt), + ...(input.verification + ? verificationFields(input.verification.summary, input.verification.verifiedAt) + : { updatedAt: new Date() }), + contactEmail: input.contact.email, + companyName: input.contact.companyName, ...(input.link && { organizationId: input.link.organizationId, linkedByUserId: input.link.userId, @@ -182,7 +342,7 @@ export async function listAdministeredOrganizations( /** Who linked a license to which organization, for the team's provisioning notice. */ export async function getLinkParties( organizationId: string | null, - userId: string + userId: string | null ): Promise<{ organizationName: string | null; userName: string | null; userEmail: string | null }> { const [[org], [person]] = await Promise.all([ organizationId @@ -192,11 +352,13 @@ export async function getLinkParties( .where(eq(organization.id, organizationId)) .limit(1) : Promise.resolve([]), - db - .select({ name: user.name, email: user.email }) - .from(user) - .where(eq(user.id, userId)) - .limit(1), + userId + ? db + .select({ name: user.name, email: user.email }) + .from(user) + .where(eq(user.id, userId)) + .limit(1) + : Promise.resolve([]), ]) return { organizationName: org?.name ?? null, @@ -204,3 +366,39 @@ export async function getLinkParties( userEmail: person?.email ?? null, } } + +/** What an organization's members see of the Marketplace licenses linked to it. */ +export async function listOrganizationLicenses(organizationId: string) { + return db + .select({ + id: awsMarketplaceLicense.id, + status: awsMarketplaceLicense.status, + dimension: awsMarketplaceLicense.dimension, + quantity: awsMarketplaceLicense.quantity, + expiresAt: awsMarketplaceLicense.expiresAt, + lastVerifiedAt: awsMarketplaceLicense.lastVerifiedAt, + }) + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.organizationId, organizationId)) + .orderBy(asc(awsMarketplaceLicense.createdAt)) +} + +/** Organizations already linked to another license of this AWS account, for the team to match. */ +export async function listOrganizationsLinkedToAccount(input: { + customerAwsAccountId: string + productCode: string + excludingLicenseArn: string +}): Promise> { + return db + .selectDistinct({ id: organization.id, name: organization.name }) + .from(awsMarketplaceLicense) + .innerJoin(organization, eq(awsMarketplaceLicense.organizationId, organization.id)) + .where( + and( + eq(awsMarketplaceLicense.customerAwsAccountId, input.customerAwsAccountId), + eq(awsMarketplaceLicense.productCode, input.productCode), + ne(awsMarketplaceLicense.licenseArn, input.excludingLicenseArn) + ) + ) + .orderBy(asc(organization.name)) +} diff --git a/packages/db/migrations/0454_aws_marketplace_license.sql b/packages/db/migrations/0454_aws_marketplace_license.sql index 51aa10208cc..54281adf49a 100644 --- a/packages/db/migrations/0454_aws_marketplace_license.sql +++ b/packages/db/migrations/0454_aws_marketplace_license.sql @@ -3,18 +3,23 @@ CREATE TABLE "aws_marketplace_license" ( "id" text PRIMARY KEY NOT NULL, "license_arn" text NOT NULL, "product_code" text NOT NULL, - "customer_identifier" text NOT NULL, - "customer_aws_account_id" text, + "customer_identifier" text, + "customer_aws_account_id" text NOT NULL, + "agreement_id" text, + "renewed_agreement_id" text, "organization_id" text, "status" "aws_marketplace_license_status" DEFAULT 'inactive' NOT NULL, "dimension" text, "quantity" integer, "expires_at" timestamp, "last_verified_at" timestamp NOT NULL, + "last_verification_attempt_at" timestamp, "claim_token_hash" text, "claim_expires_at" timestamp, "linked_by_user_id" text, "linked_at" timestamp, + "contact_email" text, + "company_name" text, "created_at" timestamp DEFAULT now() NOT NULL, "updated_at" timestamp DEFAULT now() NOT NULL ); @@ -22,6 +27,9 @@ CREATE TABLE "aws_marketplace_license" ( ALTER TABLE "aws_marketplace_license" ADD CONSTRAINT "aws_marketplace_license_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "aws_marketplace_license" ADD CONSTRAINT "aws_marketplace_license_linked_by_user_id_user_id_fk" FOREIGN KEY ("linked_by_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint CREATE UNIQUE INDEX "aws_marketplace_license_arn_unique" ON "aws_marketplace_license" USING btree ("license_arn");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_agreement_id_idx" ON "aws_marketplace_license" USING btree ("agreement_id");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_renewed_agreement_id_idx" ON "aws_marketplace_license" USING btree ("renewed_agreement_id");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_account_product_idx" ON "aws_marketplace_license" USING btree ("customer_aws_account_id","product_code");--> statement-breakpoint CREATE UNIQUE INDEX "aws_marketplace_license_claim_token_hash_unique" ON "aws_marketplace_license" USING btree ("claim_token_hash");--> statement-breakpoint -CREATE INDEX "aws_marketplace_license_last_verified_at_idx" ON "aws_marketplace_license" USING btree ("last_verified_at");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_reverify_due_idx" ON "aws_marketplace_license" USING btree (coalesce("last_verification_attempt_at", "last_verified_at"));--> statement-breakpoint CREATE INDEX "aws_marketplace_license_organization_id_idx" ON "aws_marketplace_license" USING btree ("organization_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0454_snapshot.json b/packages/db/migrations/meta/0454_snapshot.json index a3f561e1812..1922aeb3161 100644 --- a/packages/db/migrations/meta/0454_snapshot.json +++ b/packages/db/migrations/meta/0454_snapshot.json @@ -1,5 +1,5 @@ { - "id": "dcb900b6-83fb-4bcc-8843-c22bff6659b3", + "id": "54cc9081-fd36-49c9-ac52-0d35d68f54a1", "prevId": "ae1c3abd-b4a0-4658-8dd6-4858c603d078", "version": "7", "dialect": "postgresql", @@ -1154,12 +1154,24 @@ "name": "customer_identifier", "type": "text", "primaryKey": false, - "notNull": true + "notNull": false }, "customer_aws_account_id": { "name": "customer_aws_account_id", "type": "text", "primaryKey": false, + "notNull": true + }, + "agreement_id": { + "name": "agreement_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "renewed_agreement_id": { + "name": "renewed_agreement_id", + "type": "text", + "primaryKey": false, "notNull": false }, "organization_id": { @@ -1200,6 +1212,12 @@ "primaryKey": false, "notNull": true }, + "last_verification_attempt_at": { + "name": "last_verification_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, "claim_token_hash": { "name": "claim_token_hash", "type": "text", @@ -1224,6 +1242,18 @@ "primaryKey": false, "notNull": false }, + "contact_email": { + "name": "contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "company_name": { + "name": "company_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, "created_at": { "name": "created_at", "type": "timestamp", @@ -1255,6 +1285,57 @@ "method": "btree", "with": {} }, + "aws_marketplace_license_agreement_id_idx": { + "name": "aws_marketplace_license_agreement_id_idx", + "columns": [ + { + "expression": "agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "aws_marketplace_license_renewed_agreement_id_idx": { + "name": "aws_marketplace_license_renewed_agreement_id_idx", + "columns": [ + { + "expression": "renewed_agreement_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "aws_marketplace_license_account_product_idx": { + "name": "aws_marketplace_license_account_product_idx", + "columns": [ + { + "expression": "customer_aws_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "product_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, "aws_marketplace_license_claim_token_hash_unique": { "name": "aws_marketplace_license_claim_token_hash_unique", "columns": [ @@ -1270,13 +1351,13 @@ "method": "btree", "with": {} }, - "aws_marketplace_license_last_verified_at_idx": { - "name": "aws_marketplace_license_last_verified_at_idx", + "aws_marketplace_license_reverify_due_idx": { + "name": "aws_marketplace_license_reverify_due_idx", "columns": [ { - "expression": "last_verified_at", - "isExpression": false, + "expression": "coalesce(\"last_verification_attempt_at\", \"last_verified_at\")", "asc": true, + "isExpression": true, "nulls": "last" } ], diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 4745537f3b5..664d506ad24 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -3176,7 +3176,7 @@ { "idx": 454, "version": "7", - "when": 1793931258443, + "when": 1793935763803, "tag": "0454_aws_marketplace_license", "breakpoints": true } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index c8052c6b0b4..9ce1c74a8eb 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1934,7 +1934,7 @@ export const awsMarketplaceLicenseStatusEnum = pgEnum('aws_marketplace_license_s /** * An AWS Marketplace SaaS contract license, one row per license ARN: one AWS account can hold - * several concurrent agreements. `status` mirrors the last Entitlement Service read, never a + * several concurrent agreements, so the account ID identifies the buyer but not the license. `status` mirrors the last Entitlement Service read, never a * contract amount. The buyer's browser holds a one-time claim token whose hash and expiry live * here until an organization owner or admin links the license to their organization. */ @@ -1944,8 +1944,13 @@ export const awsMarketplaceLicense = pgTable( id: text('id').primaryKey(), licenseArn: text('license_arn').notNull(), productCode: text('product_code').notNull(), - customerIdentifier: text('customer_identifier').notNull(), - customerAwsAccountId: text('customer_aws_account_id'), + /** Legacy SaaS integrations only; ResolveCustomer leaves it empty for new products. */ + customerIdentifier: text('customer_identifier'), + /** Not unique: one AWS account can hold several concurrent licenses. */ + customerAwsAccountId: text('customer_aws_account_id').notNull(), + agreementId: text('agreement_id'), + /** The agreement a renewal event last moved this license to, so each renewal is told once. */ + renewedAgreementId: text('renewed_agreement_id'), organizationId: text('organization_id').references(() => organization.id, { onDelete: 'set null', }), @@ -1954,17 +1959,30 @@ export const awsMarketplaceLicense = pgTable( quantity: integer('quantity'), expiresAt: timestamp('expires_at'), lastVerifiedAt: timestamp('last_verified_at').notNull(), + /** Every re-verify attempt, failed ones too, so a license AWS keeps refusing cannot starve the rest. */ + lastVerificationAttemptAt: timestamp('last_verification_attempt_at'), claimTokenHash: text('claim_token_hash'), claimExpiresAt: timestamp('claim_expires_at'), linkedByUserId: text('linked_by_user_id').references(() => user.id, { onDelete: 'set null' }), linkedAt: timestamp('linked_at'), + /** Whom to contact about provisioning, as the buyer entered it when taking the license. */ + contactEmail: text('contact_email'), + companyName: text('company_name'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), }, (table) => [ uniqueIndex('aws_marketplace_license_arn_unique').on(table.licenseArn), + index('aws_marketplace_license_agreement_id_idx').on(table.agreementId), + index('aws_marketplace_license_renewed_agreement_id_idx').on(table.renewedAgreementId), + index('aws_marketplace_license_account_product_idx').on( + table.customerAwsAccountId, + table.productCode + ), uniqueIndex('aws_marketplace_license_claim_token_hash_unique').on(table.claimTokenHash), - index('aws_marketplace_license_last_verified_at_idx').on(table.lastVerifiedAt), + index('aws_marketplace_license_reverify_due_idx').on( + sql`coalesce(${table.lastVerificationAttemptAt}, ${table.lastVerifiedAt})` + ), index('aws_marketplace_license_organization_id_idx').on(table.organizationId), ] ) diff --git a/packages/testing/src/mocks/schema-tables.generated.ts b/packages/testing/src/mocks/schema-tables.generated.ts index 89ad55585b3..45a7f8a0ec1 100644 --- a/packages/testing/src/mocks/schema-tables.generated.ts +++ b/packages/testing/src/mocks/schema-tables.generated.ts @@ -455,16 +455,21 @@ export const GENERATED_SCHEMA_TABLES = { 'productCode', 'customerIdentifier', 'customerAwsAccountId', + 'agreementId', + 'renewedAgreementId', 'organizationId', 'status', 'dimension', 'quantity', 'expiresAt', 'lastVerifiedAt', + 'lastVerificationAttemptAt', 'claimTokenHash', 'claimExpiresAt', 'linkedByUserId', 'linkedAt', + 'contactEmail', + 'companyName', 'createdAt', 'updatedAt', ], From a0f42ddc82da37105b1c3e8c0aed4b921736b2f5 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 20:23:56 -0800 Subject: [PATCH 4/7] fix(billing): make AWS Marketplace notices durable and carry licenses across agreement replacements - Team notices and the buyer confirmation go through the outbox, enqueued in the transaction that changes the license, so a failed send is retried and reconcile never waits on email. Every writer decides under its row lock whether terms moved, dimension included, and a new registration tells the team at once. - A license with no entitlements yet is pending, not lapsed: it holds a claim, the buyer can leave contact details, and taking it waits for a read that says it is current. The notice reports whether the License Updated event has arrived. A Deprovisioned event the read contradicts stays queued, and a verification recording inactive clears the claim. - Agreement replacement keys on previousAgreementId, works in either arrival order, hands the owner on to the new license, refuses a late A to B over B to C, and uses the event time so a redelivered license event cannot roll the agreement back. A superseded license ending does not ask the team to cancel. Advisory events reach the team. - Provisioning stays manual. The buyer is told the account is being set up, to wait, the turnaround, and the contact. The team notice says to provision with payment collection paused and never to invoice through Stripe, and names the cancel flow for a lapse. An admin route lists unlinked licenses and links one, with audit. - Status for account-only buyers, and billing hides plan entry points while a license is active. The link card shows a masked account and the signed-in email with no preselected organization, and the claim lasts 4 hours. - Clients use request timeouts and SDK default retries, signing through fromHttp from @aws-sdk/credential-providers. Each license of an ended agreement gets its own read bound. The fulfillment URL always answers with a redirect and takes only URL-encoded forms. --- .../app/_shell/providers/theme-provider.tsx | 1 + .../marketplace/aws/claim/contact/route.ts | 25 + .../api/marketplace/aws/claim/link/route.ts | 5 +- .../app/api/marketplace/aws/licenses/route.ts | 20 + .../app/api/marketplace/aws/register/route.ts | 47 +- .../[id]/aws-marketplace-licenses/route.ts | 8 +- .../aws-marketplace-licenses/route.ts | 78 ++ .../aws/aws-marketplace-onboarding.tsx | 234 ++-- .../settings/components/billing/billing.tsx | 15 +- .../aws-marketplace-subscription-section.tsx | 39 +- apps/sim/app/workspace/page.tsx | 10 +- apps/sim/hooks/queries/aws-marketplace.ts | 53 +- apps/sim/lib/api/contracts/aws-marketplace.ts | 86 +- .../lib/api/contracts/v1/admin/dashboard.ts | 40 + .../aws-marketplace/application/admin.ts | 79 ++ .../aws-marketplace/application/operations.ts | 12 + .../aws-marketplace/application/use-cases.ts | 214 ++- apps/sim/lib/billing/aws-marketplace/claim.ts | 7 +- .../sim/lib/billing/aws-marketplace/client.ts | 52 +- .../aws-marketplace/entitlements.test.ts | 142 +- .../billing/aws-marketplace/entitlements.ts | 66 +- .../aws-marketplace/lifecycle.integration.ts | 1226 +++++++++++------ .../billing/aws-marketplace/notice-events.ts | 33 + .../billing/aws-marketplace/notifications.ts | 399 +++--- .../lib/billing/aws-marketplace/reconcile.ts | 301 ++-- .../billing/aws-marketplace/registration.ts | 80 +- .../lib/billing/aws-marketplace/repository.ts | 681 ++++++--- apps/sim/lib/core/outbox/handlers.ts | 13 + apps/sim/lib/navigation/auth-surfaces.ts | 1 + apps/sim/lib/navigation/paths.ts | 3 + apps/sim/package.json | 2 +- bun.lock | 24 +- .../0454_aws_marketplace_license.sql | 17 +- .../db/migrations/meta/0454_snapshot.json | 67 +- packages/db/migrations/meta/_journal.json | 2 +- packages/db/schema.ts | 36 +- .../src/mocks/schema-tables.generated.ts | 6 +- 37 files changed, 2644 insertions(+), 1480 deletions(-) create mode 100644 apps/sim/app/api/marketplace/aws/claim/contact/route.ts create mode 100644 apps/sim/app/api/marketplace/aws/licenses/route.ts create mode 100644 apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/application/admin.ts create mode 100644 apps/sim/lib/billing/aws-marketplace/notice-events.ts diff --git a/apps/sim/app/_shell/providers/theme-provider.tsx b/apps/sim/app/_shell/providers/theme-provider.tsx index b114efebd12..1f105f7883e 100644 --- a/apps/sim/app/_shell/providers/theme-provider.tsx +++ b/apps/sim/app/_shell/providers/theme-provider.tsx @@ -28,6 +28,7 @@ const NON_LANDING_LIGHT_SEGMENTS = [ 'unsubscribe', 'cli', 'credential-groups', + 'marketplace', ] as const /** diff --git a/apps/sim/app/api/marketplace/aws/claim/contact/route.ts b/apps/sim/app/api/marketplace/aws/claim/contact/route.ts new file mode 100644 index 00000000000..cdd89a3ab9e --- /dev/null +++ b/apps/sim/app/api/marketplace/aws/claim/contact/route.ts @@ -0,0 +1,25 @@ +import { saveAwsMarketplaceClaimContactContract } from '@/lib/api/contracts/aws-marketplace' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { saveAwsMarketplaceClaimContact } from '@/lib/billing/aws-marketplace/application/use-cases' +import { AWS_MARKETPLACE_CLAIM_COOKIE } from '@/lib/billing/aws-marketplace/claim' + +export const POST = defineInternalJsonRoute({ + contract: saveAwsMarketplaceClaimContactContract, + operation: saveAwsMarketplaceClaimContact.operation, + useCase: saveAwsMarketplaceClaimContact, + auth: internalSessionAuth, + rateLimit: internalRateLimits.user({ bucketName: 'aws-marketplace-link' }), + errorPolicy: internalOrchestrationErrorPolicy, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, + mapInput: ({ body }, { request }) => ({ + licenseId: body.licenseId, + contactEmail: body.contactEmail, + companyName: body.companyName || null, + claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, + }), +}) diff --git a/apps/sim/app/api/marketplace/aws/claim/link/route.ts b/apps/sim/app/api/marketplace/aws/claim/link/route.ts index b5b4ecdf9ae..d531c65bd55 100644 --- a/apps/sim/app/api/marketplace/aws/claim/link/route.ts +++ b/apps/sim/app/api/marketplace/aws/claim/link/route.ts @@ -26,10 +26,7 @@ export const POST = defineInternalJsonRoute({ companyName: body.companyName || null, claimToken: request.cookies.get(AWS_MARKETPLACE_CLAIM_COOKIE)?.value ?? null, }), - present: (_linked, { input }) => ({ - success: true as const, - organizationId: input.organizationId, - }), + present: () => ({ success: true as const }), /** The claim is spent once the link commits; the cookie that carried it goes with it. */ finalizeResponse: () => ({ headers: { 'set-cookie': serializeClaimCookie(null) } }), }) diff --git a/apps/sim/app/api/marketplace/aws/licenses/route.ts b/apps/sim/app/api/marketplace/aws/licenses/route.ts new file mode 100644 index 00000000000..55a664e581b --- /dev/null +++ b/apps/sim/app/api/marketplace/aws/licenses/route.ts @@ -0,0 +1,20 @@ +import { listAccountAwsMarketplaceLicensesContract } from '@/lib/api/contracts/aws-marketplace' +import { + defineInternalJsonRoute, + internalOrchestrationErrorPolicy, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { listAccountAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/application/use-cases' + +export const GET = defineInternalJsonRoute({ + contract: listAccountAwsMarketplaceLicensesContract, + operation: listAccountAwsMarketplaceLicenses.operation, + useCase: listAccountAwsMarketplaceLicenses, + auth: internalSessionAuth, + rateLimit: internalRateLimits.user({ bucketName: 'aws-marketplace-licenses' }), + errorPolicy: internalOrchestrationErrorPolicy, + staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, + mapInput: () => ({}), + present: (licenses) => ({ licenses }), +}) diff --git a/apps/sim/app/api/marketplace/aws/register/route.ts b/apps/sim/app/api/marketplace/aws/register/route.ts index 16af5e1d589..0c4181072ae 100644 --- a/apps/sim/app/api/marketplace/aws/register/route.ts +++ b/apps/sim/app/api/marketplace/aws/register/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { awsMarketplaceRegistrationFormSchema } from '@/lib/api/contracts/aws-marketplace' import { serializeClaimCookie } from '@/lib/billing/aws-marketplace/claim' @@ -17,9 +18,6 @@ export const dynamic = 'force-dynamic' const logger = createLogger('AwsMarketplaceRegisterAPI') -/** AWS posts a URL-encoded form; multipart is accepted too, under the same cap. */ -const FORM_MEDIA_TYPES = new Set(['application/x-www-form-urlencoded', 'multipart/form-data']) - /** The form carries one opaque token; anything larger is not from AWS. */ const MAX_FORM_BYTES = 16 * 1024 @@ -37,7 +35,7 @@ function redirectToOnboarding(error?: AwsMarketplaceOnboardingError): NextRespon async function readRegistrationToken(request: NextRequest): Promise { const mediaType = request.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() - if (!mediaType || !FORM_MEDIA_TYPES.has(mediaType)) return null + if (mediaType !== 'application/x-www-form-urlencoded') return null let form: FormData try { form = await readFormDataWithLimit(request, { @@ -58,32 +56,37 @@ async function readRegistrationToken(request: NextRequest): Promise { const productCode = getAwsMarketplaceProductCode() if (!productCode) return NextResponse.json({ error: 'Not found' }, { status: 404 }) - const rateLimited = await enforceIpRateLimit( - 'aws-marketplace-register', - request, - REGISTER_RATE_LIMIT - ) - if (rateLimited) return rateLimited + try { + const rateLimited = await enforceIpRateLimit( + 'aws-marketplace-register', + request, + REGISTER_RATE_LIMIT + ) + if (rateLimited) return redirectToOnboarding('unavailable') - const token = await readRegistrationToken(request) - if (!token) { - logger.warn('AWS Marketplace registration arrived without a usable token') - return redirectToOnboarding('invalid') - } + const token = await readRegistrationToken(request) + if (!token) { + logger.warn('AWS Marketplace registration arrived without a usable token') + return redirectToOnboarding('invalid') + } - const outcome = await registerAwsMarketplaceBuyer(productCode, token) - if (outcome.kind !== 'claimed') return redirectToOnboarding(outcome.kind) + const outcome = await registerAwsMarketplaceBuyer(productCode, token) + if (outcome.kind !== 'claimed') return redirectToOnboarding(outcome.kind) - const response = redirectToOnboarding() - response.headers.append('set-cookie', serializeClaimCookie(outcome.claimToken)) - response.headers.set('Cache-Control', 'no-store') - return response + const response = redirectToOnboarding() + response.headers.append('set-cookie', serializeClaimCookie(outcome.claimToken)) + response.headers.set('Cache-Control', 'no-store') + return response + } catch (error) { + logger.error('AWS Marketplace registration failed', { error: getErrorMessage(error) }) + return redirectToOnboarding('unavailable') + } }) /** A refresh, back button, or bookmark of the fulfillment URL lands on the onboarding page. */ diff --git a/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts b/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts index a6d4bb352ca..8794a608396 100644 --- a/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts +++ b/apps/sim/app/api/organizations/[id]/aws-marketplace-licenses/route.ts @@ -16,11 +16,5 @@ export const GET = defineInternalJsonRoute({ errorPolicy: internalOrchestrationErrorPolicy, staticResponseHeaders: { 'Cache-Control': 'private, no-store' }, mapInput: ({ params }) => ({ organizationId: params.id }), - present: (licenses) => ({ - licenses: licenses.map((license) => ({ - ...license, - expiresAt: license.expiresAt?.toISOString() ?? null, - lastVerifiedAt: license.lastVerifiedAt.toISOString(), - })), - }), + present: (licenses) => ({ licenses }), }) diff --git a/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts b/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts new file mode 100644 index 00000000000..f7cf237fc78 --- /dev/null +++ b/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts @@ -0,0 +1,78 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { + adminDashboardLinkAwsMarketplaceLicenseContract, + adminDashboardListAwsMarketplaceLicensesContract, +} from '@/lib/api/contracts/v1/admin/dashboard' +import { parseRequest } from '@/lib/api/server' +import { + linkAwsMarketplaceLicenseAsAdmin, + listUnlinkedAwsMarketplaceLicenses, +} from '@/lib/billing/aws-marketplace/application/admin' +import { asOrchestrationError } from '@/lib/core/orchestration/types' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor' +import { withAdminAuth } from '@/app/api/v1/admin/middleware' +import { + adminConflictResponse, + adminInternalErrorResponse, + adminInvalidJsonResponse, + adminNotFoundResponse, + adminValidationErrorResponse, + listResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +const logger = createLogger('AdminAwsMarketplaceLicensesAPI') + +/** The Marketplace licenses no organization holds yet. */ +export const GET = withRouteHandler( + withAdminAuth(async (request) => { + const parsed = await parseRequest( + adminDashboardListAwsMarketplaceLicensesContract, + request, + {}, + { validationErrorResponse: adminValidationErrorResponse } + ) + if (!parsed.success) return parsed.response + try { + const { limit, offset } = parsed.data.query + const { total, licenses } = await listUnlinkedAwsMarketplaceLicenses({ limit, offset }) + return listResponse(licenses, { + total, + limit, + offset, + hasMore: offset + licenses.length < total, + }) + } catch (error) { + logger.error('Failed to list AWS Marketplace licenses', { error }) + return adminInternalErrorResponse(getErrorMessage(error, 'Failed to list licenses')) + } + }) +) + +/** Links a Marketplace license to an organization once the team has provisioned it. */ +export const POST = withRouteHandler( + withAdminAuth(async (request) => { + const parsed = await parseRequest( + adminDashboardLinkAwsMarketplaceLicenseContract, + request, + {}, + { + validationErrorResponse: adminValidationErrorResponse, + invalidJsonResponse: adminInvalidJsonResponse, + } + ) + if (!parsed.success) return parsed.response + try { + const actor = await getAdminAuditActor(request) + return singleResponse(await linkAwsMarketplaceLicenseAsAdmin(parsed.data.body, actor)) + } catch (error) { + const refusal = asOrchestrationError(error) + if (refusal?.code === 'not_found') return adminNotFoundResponse(refusal.message) + if (refusal?.code === 'conflict') return adminConflictResponse(refusal.message) + logger.error('Failed to link an AWS Marketplace license', { error }) + return adminInternalErrorResponse(getErrorMessage(error, 'Failed to link the license')) + } + }) +) diff --git a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx index 5c8ba29bbc2..ed555942d81 100644 --- a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx +++ b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx @@ -9,15 +9,16 @@ import { AuthTag, AuthTextLink, } from '@sim/auth-ui' +import { formatDate } from '@sim/utils/formatting' import { useRouter } from 'next/navigation' import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' -import { useSession } from '@/lib/auth/client' +import { client, useSession } from '@/lib/auth/client' import { AWS_MARKETPLACE_ONBOARDING_PATH, AWS_MARKETPLACE_SETUP_TURNAROUND, type AwsMarketplaceOnboardingError, } from '@/lib/billing/aws-marketplace/onboarding' -import { APP_ENTRY_PATH } from '@/lib/navigation/paths' +import { organizationRoutes, WORKSPACE_BILLING_PATH } from '@/lib/navigation/paths' import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect' import { InviteLayout, InviteStatusCard } from '@/app/invite/components' import { useBrandConfig } from '@/ee/whitelabeling' @@ -25,6 +26,7 @@ import { useAwsMarketplaceClaim, useLinkAwsMarketplaceLicense, useRegisterAwsMarketplaceLicenseToAccount, + useSaveAwsMarketplaceClaimContact, } from '@/hooks/queries/aws-marketplace' const RESTART_FROM_AWS = @@ -54,6 +56,9 @@ const REGISTRATION_ERROR_COPY: Record< }, } +/** AWS's manual-setup guidance: being set up, wait to be contacted, turnaround, contact. */ +const BEING_SET_UP = `Your account is being set up. Please wait for us to contact you. We expect to finish ${AWS_MARKETPLACE_SETUP_TURNAROUND}.` + function authLink(path: '/login' | '/signup'): string { return buildAuthCrossLink(path, { callbackUrl: AWS_MARKETPLACE_ONBOARDING_PATH, @@ -61,14 +66,13 @@ function authLink(path: '/login' | '/signup'): string { }) } -const SETUP_NEXT_STEP = `Our team will set up your Enterprise workspace ${AWS_MARKETPLACE_SETUP_TURNAROUND} and email you when it is ready.` - interface SupportContactProps { - supportEmail: string + supportEmail: string | undefined } /** AWS requires support contact options on the fulfillment landing page. */ function SupportContact({ supportEmail }: SupportContactProps) { + if (!supportEmail) return null return (

Questions? Email {supportEmail} @@ -76,10 +80,6 @@ function SupportContact({ supportEmail }: SupportContactProps) { ) } -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString(undefined, { dateStyle: 'medium' }) -} - interface AwsMarketplaceOnboardingProps { registrationError: AwsMarketplaceOnboardingError | null registrationDisabled: boolean @@ -96,9 +96,15 @@ export function AwsMarketplaceOnboarding({ const linkLicense = useLinkAwsMarketplaceLicense() const registerToAccount = useRegisterAwsMarketplaceLicenseToAccount() const { supportEmail } = useBrandConfig() - const supportContact = supportEmail ? : undefined + const supportContact = const returnHome = { label: 'Return to Home', onClick: () => router.push('/') } + /** A revoked session's cookie would send `/login` straight to the app, losing the way back. */ + const goToAuth = async (path: '/login' | '/signup') => { + await client.signOut().catch(() => {}) + router.push(authLink(path)) + } + if (registrationError) { const copy = REGISTRATION_ERROR_COPY[registrationError] return ( @@ -107,6 +113,7 @@ export function AwsMarketplaceOnboarding({ type='error' title={copy.title} description={copy.description} + details={supportContact} actions={[returnHome]} /> @@ -128,43 +135,42 @@ export function AwsMarketplaceOnboarding({ type='login' title='Set up your AWS Marketplace subscription' description='Sign in to Sim to link this subscription to your organization.' + details={supportContact} actions={[ ...(registrationDisabled ? [] - : [{ label: 'Create an account', onClick: () => router.push(authLink('/signup')) }]), - { - label: 'I already have an account', - onClick: () => router.push(authLink('/login')), - }, + : [{ label: 'Create an account', onClick: () => goToAuth('/signup') }]), + { label: 'I already have an account', onClick: () => goToAuth('/login') }, ]} /> ) } - if (linkLicense.isSuccess) { - return ( - - router.push(APP_ENTRY_PATH) }]} - /> - - ) - } - - if (registerToAccount.isSuccess) { + if (linkLicense.isSuccess || registerToAccount.isSuccess) { + const organizationId = linkLicense.variables?.organizationId return ( router.push(APP_ENTRY_PATH) }]} + title='Your account is being set up' + description={BEING_SET_UP} + details={ +

+

+ + View subscription status + +

+ {supportContact} +
+ } /> ) @@ -185,6 +191,7 @@ export function AwsMarketplaceOnboarding({ type='error' title='Could not load your subscription' description={claimQuery.error.message} + details={supportContact} actions={[{ label: 'Try again', onClick: () => claimQuery.refetch() }, returnHome]} /> @@ -192,26 +199,58 @@ export function AwsMarketplaceOnboarding({ } const claim = claimQuery.data - if (claim.status !== 'ready') { + if (claim.status === 'missing' || claim.status === 'expired' || claim.status === 'inactive') { + const title = { + missing: 'No subscription to set up', + expired: 'Setup link expired', + inactive: 'No active subscription found', + }[claim.status] return ( + + ) + } + + if (claim.status === 'linked') { + return ( + + + {claim.organizationId && ( +

+ + View subscription status + +

+ )} + {supportContact} +
+ } actions={[returnHome]} /> ) } - const pending = linkLicense.isPending || registerToAccount.isPending return ( - - defaultContactEmail: string +interface ClaimCardProps { + claim: Extract + signedInAs: string supportContact: ReactNode - pending: boolean linking: boolean registering: boolean error: string | null @@ -247,52 +285,82 @@ interface LinkLicenseCardProps { } /** - * Links the license to an organization the viewer administers, or registers it to their - * account when they administer none or would rather not pick one. + * Shows the buyer whose subscription this is and who is signed in, so a claim planted in someone + * else's browser is noticed. A confirmed license links to an organization the viewer administers, + * or registers to their account. While AWS is still confirming it, the buyer leaves contact + * details and asks again. */ -function LinkLicenseCard({ +function ClaimCard({ claim, - defaultContactEmail, + signedInAs, supportContact, - pending, linking, registering, error, onLink, onRegister, onCancel, -}: LinkLicenseCardProps) { - const { license, organizations } = claim - const [selectedOrganizationId, setSelectedOrganizationId] = useState() - const [contactEmail, setContactEmail] = useState(defaultContactEmail) - const [companyName, setCompanyName] = useState('') +}: ClaimCardProps) { + const { license } = claim + const organizations = claim.status === 'ready' ? claim.organizations : [] + const saveContact = useSaveAwsMarketplaceClaimContact() + const [organizationId, setOrganizationId] = useState() + const [contactEmail, setContactEmail] = useState(license.contactEmail ?? signedInAs) + const [companyName, setCompanyName] = useState(license.companyName ?? '') const contact: BuyerContact = { contactEmail: contactEmail.trim(), ...(companyName.trim() && { companyName: companyName.trim() }), } const hasContact = contact.contactEmail.length > 0 - const linkedOrganization = organizations.find((org) => org.id === license.organizationId) - const organizationId = - selectedOrganizationId ?? - linkedOrganization?.id ?? - (organizations.length === 1 ? organizations[0]?.id : undefined) + const pending = linking || registering || saveContact.isPending const rows = [ + { label: 'AWS account', value: license.awsAccount }, + { label: 'Signed in as', value: signedInAs }, { label: 'Plan', value: license.dimension ?? 'Not specified' }, { label: 'Quantity', value: license.quantity?.toLocaleString() ?? 'Not specified' }, { label: 'Expires', - value: license.expiresAt ? formatDate(license.expiresAt) : 'No expiry', + value: license.expiresAt ? formatDate(new Date(license.expiresAt)) : 'No expiry', }, ] const hasOrganizations = organizations.length > 0 - const registerAction = { - label: hasOrganizations ? 'Register to my account instead' : 'Register to my account', - onClick: () => onRegister(contact), - disabled: pending || !hasContact, - loading: registering, - } + const actions = + claim.status === 'pending' + ? [ + { + label: 'Save and check again', + onClick: () => saveContact.mutate({ licenseId: license.id, ...contact }), + disabled: pending || !hasContact, + loading: saveContact.isPending, + }, + ] + : [ + ...(hasOrganizations + ? [ + { + label: 'Link subscription', + onClick: () => organizationId && onLink(organizationId, contact), + disabled: !organizationId || !hasContact || pending, + loading: linking, + }, + ] + : []), + { + label: hasOrganizations ? 'Register to my account instead' : 'Register to my account', + onClick: () => onRegister(contact), + disabled: pending || !hasContact, + loading: registering, + }, + ] + + const description = + claim.status === 'pending' + ? 'AWS is still confirming your subscription. Leave your contact details and check again in a few minutes.' + : hasOrganizations + ? `Choose the organization this subscription is for, or register it to your account. We expect to finish setting it up ${AWS_MARKETPLACE_SETUP_TURNAROUND}.` + : `Register this subscription to your account. We expect to finish setting it up ${AWS_MARKETPLACE_SETUP_TURNAROUND}.` return ( @@ -303,11 +371,7 @@ function LinkLicenseCard({ ? 'Link your AWS Marketplace subscription' : 'Set up your AWS Marketplace subscription' } - description={ - hasOrganizations - ? `Choose the organization this subscription is for, or register it to your account. ${SETUP_NEXT_STEP}` - : `Register this subscription to your account. ${SETUP_NEXT_STEP}` - } + description={description} details={
@@ -320,7 +384,7 @@ function LinkLicenseCard({
Status
- {license.status === 'active' ? 'Active' : 'Inactive'} + {claim.status === 'pending' ? 'Confirming with AWS' : 'Active'}
@@ -330,7 +394,7 @@ function LinkLicenseCard({ id='aws-marketplace-organization' options={organizations.map((org) => ({ label: org.name, value: org.id }))} value={organizationId} - onChange={setSelectedOrganizationId} + onChange={setOrganizationId} placeholder='Select an organization' fullWidth /> @@ -354,24 +418,18 @@ function LinkLicenseCard({ onChange={(event) => setCompanyName(event.target.value)} /> - {error && {error}} + {saveContact.isSuccess && claim.status === 'pending' && ( + + We saved your details. AWS is still confirming your subscription. + + )} + {(error ?? saveContact.error?.message) && ( + {error ?? saveContact.error?.message} + )} {supportContact}
} - actions={[ - ...(hasOrganizations - ? [ - { - label: 'Link subscription', - onClick: () => organizationId && onLink(organizationId, contact), - disabled: !organizationId || !hasContact || pending, - loading: linking, - }, - ] - : []), - registerAction, - { label: 'Not now', onClick: onCancel }, - ]} + actions={[...actions, { label: 'Not now', onClick: onCancel }]} />
) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx index 4b11f63c01e..5f06bcf2c99 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx @@ -53,6 +53,7 @@ import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/ import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { RESOURCE_ROW_ARROW_CLASSES } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { useAwsMarketplaceLicenses } from '@/hooks/queries/aws-marketplace' import { useBillingUsageNotifications, useUpdateGeneralSetting, @@ -134,6 +135,10 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps enabled: !isOrganizationScope, }) const billingOrganizationId = isOrganizationScope ? (organizationId ?? null) : null + const { data: marketplaceLicenses } = useAwsMarketplaceLicenses(billingOrganizationId) + const billedThroughMarketplace = marketplaceLicenses?.some( + (license) => license.status !== 'inactive' + ) const { data: organizationBillingData, @@ -453,8 +458,9 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps ? `Organization ${planName} plan ended` : `Organization ${planName} plan` : `Personal ${planName} plan` - const priceText = - organizationSubscriptionState === 'free' + const priceText = billedThroughMarketplace + ? 'Billed through AWS Marketplace' + : organizationSubscriptionState === 'free' ? 'No active organization subscription' : organizationSubscriptionState === 'lapsed' ? 'Choose a new plan for this organization' @@ -515,6 +521,7 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps {!subscription.isEnterprise && + !billedThroughMarketplace && (canExplorePlans && upgradeHref ? ( )} - {billingOrganizationId && ( - - )} + {!subscription.isFree && invoices.length > 0 && ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx index 5a0b579f165..0fcacb762bd 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx @@ -1,43 +1,62 @@ 'use client' import { Badge } from '@sim/emcn' +import { formatDate } from '@sim/utils/formatting' +import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' -import { useOrganizationAwsMarketplaceLicenses } from '@/hooks/queries/aws-marketplace' +import { useAwsMarketplaceLicenses } from '@/hooks/queries/aws-marketplace' -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString() -} +const STATUS_BADGES = { + active: { label: 'Active', variant: 'green' }, + pending: { label: 'Confirming with AWS', variant: 'amber' }, + inactive: { label: 'Inactive', variant: 'gray' }, +} as const interface AwsMarketplaceSubscriptionSectionProps { - organizationId: string + /** Null reads the licenses registered to the viewer's own account. */ + organizationId: string | null } /** Read-only: AWS requires buyers to see their subscription status in the product. */ export function AwsMarketplaceSubscriptionSection({ organizationId, }: AwsMarketplaceSubscriptionSectionProps) { - const { data: licenses } = useOrganizationAwsMarketplaceLicenses(organizationId) + const { data: licenses, error, isFetching, refetch } = useAwsMarketplaceLicenses(organizationId) + if (error && !licenses) { + return ( + + refetch()} + /> + + ) + } if (!licenses?.length) return null return (
{licenses.map((license) => { + const badge = STATUS_BADGES[license.status] const rows = [ { label: 'Plan', value: license.dimension ?? 'Not specified' }, { label: 'Quantity', value: license.quantity?.toLocaleString() ?? 'Not specified' }, { label: 'Expires', - value: license.expiresAt ? formatDate(license.expiresAt) : 'No expiry', + value: license.expiresAt ? formatDate(new Date(license.expiresAt)) : 'No expiry', }, - { label: 'Last verified', value: formatDate(license.lastVerifiedAt) }, + { label: 'Last verified', value: formatDate(new Date(license.lastVerifiedAt)) }, ] return (
Status - - {license.status === 'active' ? 'Active' : 'Inactive'} + + {badge.label}
{rows.map((row) => ( diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index 2013b453fcb..fbc234eecfb 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -132,7 +132,9 @@ export default function WorkspacePage() { ? buildUpgradeHref(id, isUpgradeReason(rawReason) ? rawReason : undefined) : redirectTarget === 'settings' ? `/workspace/${id}/settings/general` - : `/workspace/${id}` + : redirectTarget === 'billing' + ? `/workspace/${id}/settings/billing` + : `/workspace/${id}` const { workspaces, lastActiveWorkspaceId, creationPolicy, forwardOrganizationId } = data @@ -141,7 +143,11 @@ export default function WorkspacePage() { hasRedirectedRef.current = true const routes = organizationRoutes(forwardOrganizationId) router.replace( - redirectTarget === 'settings' ? routes.settingsSection('general') : routes.home + redirectTarget === 'settings' + ? routes.settingsSection('general') + : redirectTarget === 'billing' + ? routes.settingsSection('billing') + : routes.home ) return } diff --git a/apps/sim/hooks/queries/aws-marketplace.ts b/apps/sim/hooks/queries/aws-marketplace.ts index 527ae167b85..0845b12ecfd 100644 --- a/apps/sim/hooks/queries/aws-marketplace.ts +++ b/apps/sim/hooks/queries/aws-marketplace.ts @@ -4,8 +4,10 @@ import type { ContractBodyInput } from '@/lib/api/contracts' import { getAwsMarketplaceClaimContract, linkAwsMarketplaceLicenseContract, + listAccountAwsMarketplaceLicensesContract, listOrganizationAwsMarketplaceLicensesContract, registerAwsMarketplaceLicenseToAccountContract, + saveAwsMarketplaceClaimContactContract, } from '@/lib/api/contracts/aws-marketplace' const AWS_MARKETPLACE_CLAIM_STALE_TIME = 30 * 1000 @@ -16,9 +18,10 @@ const awsMarketplaceKeys = { claims: () => [...awsMarketplaceKeys.all, 'claim'] as const, /** The claim rides in an httpOnly cookie; the viewer scopes which organizations it lists. */ claim: (viewerId: string) => [...awsMarketplaceKeys.claims(), viewerId] as const, - organizationLicenses: () => [...awsMarketplaceKeys.all, 'organization-licenses'] as const, - organizationLicenseList: (organizationId: string) => - [...awsMarketplaceKeys.organizationLicenses(), organizationId] as const, + licenses: () => [...awsMarketplaceKeys.all, 'licenses'] as const, + /** An organization's licenses, or with null the viewer's own account's. */ + licenseList: (organizationId: string | null) => + [...awsMarketplaceKeys.licenses(), organizationId ?? ''] as const, } export function useAwsMarketplaceClaim(viewerId: string | null) { @@ -31,38 +34,50 @@ export function useAwsMarketplaceClaim(viewerId: string | null) { }) } -export function useLinkAwsMarketplaceLicense() { +/** Taking a license spends the claim and changes what billing shows, so both are read again. */ +function useInvalidateAwsMarketplace() { const queryClient = useQueryClient() + return () => queryClient.invalidateQueries({ queryKey: awsMarketplaceKeys.all }) +} + +export function useLinkAwsMarketplaceLicense() { + const invalidate = useInvalidateAwsMarketplace() return useMutation({ mutationFn: (body: ContractBodyInput) => requestJson(linkAwsMarketplaceLicenseContract, { body }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: awsMarketplaceKeys.claims() }) - }, + onSuccess: invalidate, }) } export function useRegisterAwsMarketplaceLicenseToAccount() { - const queryClient = useQueryClient() + const invalidate = useInvalidateAwsMarketplace() return useMutation({ mutationFn: (body: ContractBodyInput) => requestJson(registerAwsMarketplaceLicenseToAccountContract, { body }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: awsMarketplaceKeys.claims() }) - }, + onSuccess: invalidate, + }) +} + +export function useSaveAwsMarketplaceClaimContact() { + const invalidate = useInvalidateAwsMarketplace() + return useMutation({ + mutationFn: (body: ContractBodyInput) => + requestJson(saveAwsMarketplaceClaimContactContract, { body }), + onSuccess: invalidate, }) } -/** The Marketplace subscriptions linked to an organization, for its billing page. */ -export function useOrganizationAwsMarketplaceLicenses(organizationId: string | null) { +/** The Marketplace subscriptions an organization holds, or with null the viewer's own account. */ +export function useAwsMarketplaceLicenses(organizationId: string | null) { return useQuery({ - queryKey: awsMarketplaceKeys.organizationLicenseList(organizationId ?? ''), + queryKey: awsMarketplaceKeys.licenseList(organizationId), queryFn: ({ signal }) => - requestJson(listOrganizationAwsMarketplaceLicensesContract, { - params: { id: organizationId ?? '' }, - signal, - }), - enabled: Boolean(organizationId), + organizationId + ? requestJson(listOrganizationAwsMarketplaceLicensesContract, { + params: { id: organizationId }, + signal, + }) + : requestJson(listAccountAwsMarketplaceLicensesContract, { signal }), staleTime: AWS_MARKETPLACE_LICENSES_STALE_TIME, select: (data) => data.licenses, }) diff --git a/apps/sim/lib/api/contracts/aws-marketplace.ts b/apps/sim/lib/api/contracts/aws-marketplace.ts index 9d8f9085e34..a1c11862b7e 100644 --- a/apps/sim/lib/api/contracts/aws-marketplace.ts +++ b/apps/sim/lib/api/contracts/aws-marketplace.ts @@ -7,7 +7,7 @@ import { organizationIdSchema } from '@/lib/api/contracts/primitives' * browser submits it as `application/x-www-form-urlencoded`, and the route answers with a redirect. */ export const awsMarketplaceRegistrationFormSchema = z.object({ - 'x-amzn-marketplace-token': z.string().min(1, 'Registration token is required').max(4096), + 'x-amzn-marketplace-token': z.string().regex(/^\S+$/, 'Registration token is required'), }) /** AWS requires the registration page to collect the buyer's email; the company is optional. */ @@ -18,34 +18,50 @@ const buyerContactSchema = z.object({ .min(1, 'Contact email is required') .max(254, 'Contact email must be at most 254 characters') .email('Enter a valid contact email'), - companyName: z.string().trim().max(200, 'Company name must be at most 200 characters').optional(), + companyName: z + .string() + .trim() + .max(200, 'Company name must be at most 200 characters') + .regex(/^[^\r\n]*$/, 'Company name must be a single line') + .optional(), +}) + +const claimActionBodySchema = buyerContactSchema.extend({ + licenseId: z.string().min(1, 'License ID is required').max(255), }) -const awsMarketplaceLicenseSummarySchema = z.object({ +const awsMarketplaceLicenseSchema = z.object({ id: z.string(), - status: z.enum(['active', 'inactive']), + status: z.enum(['pending', 'active', 'inactive']), dimension: z.string().nullable(), quantity: z.number().int().nullable(), expiresAt: z.iso.datetime().nullable(), - /** The organization this license is already linked to, if any. */ - organizationId: z.string().nullable(), }) -const awsMarketplaceClaimOrganizationSchema = z.object({ - id: z.string(), - name: z.string(), - role: z.enum(['owner', 'admin']), +const claimedLicenseSchema = awsMarketplaceLicenseSchema.extend({ + /** The buyer's AWS account, masked: only they should recognize it. */ + awsAccount: z.string(), + contactEmail: z.string().nullable(), + companyName: z.string().nullable(), }) const awsMarketplaceClaimSchema = z.discriminatedUnion('status', [ /** This browser holds no claim, or one already used. */ z.object({ status: z.literal('missing') }), z.object({ status: z.literal('expired') }), + z.object({ status: z.literal('inactive') }), + /** AWS has returned no entitlement yet; the buyer can leave contact details meanwhile. */ + z.object({ status: z.literal('pending'), license: claimedLicenseSchema }), + z.object({ + status: z.literal('linked'), + /** Named only when the viewer administers it. */ + organizationId: z.string().nullable(), + }), z.object({ status: z.literal('ready'), - license: awsMarketplaceLicenseSummarySchema, + license: claimedLicenseSchema, /** Organizations the viewer owns or administers, in name order. */ - organizations: z.array(awsMarketplaceClaimOrganizationSchema), + organizations: z.array(z.object({ id: z.string(), name: z.string() })), }), ]) export type AwsMarketplaceClaim = z.output @@ -60,33 +76,31 @@ export const getAwsMarketplaceClaimContract = defineRouteContract({ export const linkAwsMarketplaceLicenseContract = defineRouteContract({ method: 'POST', path: '/api/marketplace/aws/claim/link', - body: buyerContactSchema.extend({ - licenseId: z.string().min(1, 'License ID is required').max(255), - organizationId: organizationIdSchema, - }), - response: { - mode: 'json', - schema: z.object({ success: z.literal(true), organizationId: z.string() }), - }, + body: claimActionBodySchema.extend({ organizationId: organizationIdSchema }), + response: { mode: 'json', schema: z.object({ success: z.literal(true) }) }, }) /** Registers the claimed license to the signed-in account when no organization is chosen. */ export const registerAwsMarketplaceLicenseToAccountContract = defineRouteContract({ method: 'POST', path: '/api/marketplace/aws/claim/register-account', - body: buyerContactSchema.extend({ - licenseId: z.string().min(1, 'License ID is required').max(255), - }), + body: claimActionBodySchema, response: { mode: 'json', schema: z.object({ success: z.literal(true) }) }, }) -const organizationAwsMarketplaceLicenseSchema = z.object({ - id: z.string(), - status: z.enum(['active', 'inactive']), - dimension: z.string().nullable(), - quantity: z.number().int().nullable(), - expiresAt: z.iso.datetime().nullable(), - lastVerifiedAt: z.iso.datetime(), +/** Saves a pending buyer's contact details and asks AWS again; the claim stays open. */ +export const saveAwsMarketplaceClaimContactContract = defineRouteContract({ + method: 'POST', + path: '/api/marketplace/aws/claim/contact', + body: claimActionBodySchema, + response: { + mode: 'json', + schema: z.object({ status: z.enum(['pending', 'active', 'inactive']) }), + }, +}) + +const awsMarketplaceLicenseListSchema = z.object({ + licenses: z.array(awsMarketplaceLicenseSchema.extend({ lastVerifiedAt: z.iso.datetime() })), }) /** The Marketplace licenses linked to an organization, for its billing page. */ @@ -94,8 +108,12 @@ export const listOrganizationAwsMarketplaceLicensesContract = defineRouteContrac method: 'GET', path: '/api/organizations/[id]/aws-marketplace-licenses', params: z.object({ id: organizationIdSchema }), - response: { - mode: 'json', - schema: z.object({ licenses: z.array(organizationAwsMarketplaceLicenseSchema) }), - }, + response: { mode: 'json', schema: awsMarketplaceLicenseListSchema }, +}) + +/** The licenses registered to the viewer's account with no organization, for personal billing. */ +export const listAccountAwsMarketplaceLicensesContract = defineRouteContract({ + method: 'GET', + path: '/api/marketplace/aws/licenses', + response: { mode: 'json', schema: awsMarketplaceLicenseListSchema }, }) diff --git a/apps/sim/lib/api/contracts/v1/admin/dashboard.ts b/apps/sim/lib/api/contracts/v1/admin/dashboard.ts index 26ae84dde19..55627b285fa 100644 --- a/apps/sim/lib/api/contracts/v1/admin/dashboard.ts +++ b/apps/sim/lib/api/contracts/v1/admin/dashboard.ts @@ -749,6 +749,46 @@ export const adminDashboardIssueEnterpriseContract = defineRouteContract({ }, }) +const adminDashboardAwsMarketplaceLicenseSchema = z.object({ + id: z.string(), + licenseArn: z.string(), + customerAwsAccountId: z.string(), + agreementId: z.string().nullable(), + status: z.enum(['pending', 'active', 'inactive']), + dimension: z.string().nullable(), + quantity: z.number().int().nullable(), + expiresAt: z.string().nullable(), + lastVerifiedAt: z.string(), + licenseEventReceivedAt: z.string().nullable(), + linkedByUserId: z.string().nullable(), + contactEmail: z.string().nullable(), + companyName: z.string().nullable(), + createdAt: z.string(), +}) + +export const adminDashboardListAwsMarketplaceLicensesContract = defineRouteContract({ + method: 'GET', + path: '/api/v1/admin/dashboard/aws-marketplace-licenses', + query: adminV1PaginationQuerySchema, + response: { + mode: 'json', + schema: adminV1ListResponseSchema(adminDashboardAwsMarketplaceLicenseSchema), + }, +}) + +export const adminDashboardLinkAwsMarketplaceLicenseContract = defineRouteContract({ + method: 'POST', + path: '/api/v1/admin/dashboard/aws-marketplace-licenses', + body: z.object({ + licenseId: z.string().min(1, 'License ID is required').max(255), + organizationId: z.string().min(1, 'Organization ID is required').max(255), + }), + response: { + mode: 'json', + schema: adminV1SingleResponseSchema(z.object({ id: z.string(), organizationId: z.string() })), + }, +}) + export const adminDashboardCreateEnterpriseOwnerClaimContract = defineRouteContract({ method: 'POST', path: '/api/v1/admin/dashboard/enterprise-owner-claims', diff --git a/apps/sim/lib/billing/aws-marketplace/application/admin.ts b/apps/sim/lib/billing/aws-marketplace/application/admin.ts new file mode 100644 index 00000000000..4154364c62a --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/application/admin.ts @@ -0,0 +1,79 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' +import { db } from '@sim/db' +import type { AdminMutationActor } from '@/lib/admin/dashboard' +import { + licenseAuditMetadata, + requireLinkableTo, +} from '@/lib/billing/aws-marketplace/application/use-cases' +import { + countUnlinkedLicenses, + linkLicenseForTeam, + listUnlinkedLicenses, + lockLicense, + organizationExists, +} from '@/lib/billing/aws-marketplace/repository' +import { OrchestrationError } from '@/lib/core/orchestration/types' + +/** The licenses no organization holds yet, the ones the team's notices ask to link. */ +export async function listUnlinkedAwsMarketplaceLicenses(page: { limit: number; offset: number }) { + const [total, rows] = await Promise.all([countUnlinkedLicenses(), listUnlinkedLicenses(page)]) + return { + total, + licenses: rows.map((license) => ({ + id: license.id, + licenseArn: license.licenseArn, + customerAwsAccountId: license.customerAwsAccountId, + agreementId: license.agreementId, + status: license.status, + dimension: license.dimension, + quantity: license.quantity, + expiresAt: license.expiresAt?.toISOString() ?? null, + lastVerifiedAt: license.lastVerifiedAt.toISOString(), + licenseEventReceivedAt: license.licenseEventReceivedAt?.toISOString() ?? null, + linkedByUserId: license.linkedByUserId, + contactEmail: license.contactEmail, + companyName: license.companyName, + createdAt: license.createdAt.toISOString(), + })), + } +} + +/** + * The team links a license to the organization it provisioned for the buyer. A license linked + * elsewhere is refused, as it is for a buyer; linking it to the same organization again changes + * nothing and is not audited. + */ +export async function linkAwsMarketplaceLicenseAsAdmin( + input: { licenseId: string; organizationId: string }, + actor: AdminMutationActor +): Promise<{ id: string; organizationId: string }> { + const { license, newlyLinked } = await db.transaction(async (tx) => { + const locked = await lockLicense(tx, input.licenseId) + if (!locked) throw new OrchestrationError('not_found', 'License') + requireLinkableTo(locked, input.organizationId) + if (!(await organizationExists(tx, input.organizationId))) { + throw new OrchestrationError('not_found', 'Organization') + } + if (locked.organizationId === input.organizationId) { + return { license: locked, newlyLinked: false } + } + return { + license: await linkLicenseForTeam(tx, input.licenseId, input.organizationId), + newlyLinked: true, + } + }) + if (newlyLinked) { + recordAudit({ + actorId: actor.id, + actorName: actor.name, + actorEmail: actor.email, + action: AuditAction.AWS_MARKETPLACE_LICENSE_LINKED, + resourceType: AuditResourceType.AWS_MARKETPLACE_LICENSE, + resourceId: license.id, + resourceName: license.dimension ?? 'AWS Marketplace license', + description: 'Admin linked an AWS Marketplace subscription to an organization', + metadata: { ...licenseAuditMetadata(license), organizationId: input.organizationId }, + }) + } + return { id: license.id, organizationId: input.organizationId } +} diff --git a/apps/sim/lib/billing/aws-marketplace/application/operations.ts b/apps/sim/lib/billing/aws-marketplace/application/operations.ts index c0610844148..e2b4c8cdf5c 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/operations.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/operations.ts @@ -18,6 +18,18 @@ export const awsMarketplaceOperations = { principalKinds: ['session'], capability: 'none', }), + // permission-group-exempt: a buyer waiting on AWS leaves contact details for their own purchase. + saveClaimContact: defineOperation({ + id: 'aws_marketplace.claim.save_contact', + principalKinds: ['session'], + capability: 'none', + }), + // permission-group-exempt: a buyer sees the status of a subscription registered to their own account. + listAccountLicenses: defineOperation({ + id: 'aws_marketplace.license.list_account', + principalKinds: ['session'], + capability: 'none', + }), // permission-group-exempt: members see their organization's subscription status, as the billing page shows its plan. listOrganizationLicenses: defineOrganizationOperation({ id: 'organization.aws_marketplace_license.list', diff --git a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts index 714eeedfd6c..2ec11396e28 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts @@ -4,20 +4,18 @@ import type { AwsMarketplaceClaim } from '@/lib/api/contracts/aws-marketplace' import { awsMarketplaceOperations } from '@/lib/billing/aws-marketplace/application/operations' import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' -import { type EntitlementSummary, verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' -import { - notifyBuyerOfTakenLicense, - notifyTeamOfTakenLicense, - notifyTeamOfVerification, -} from '@/lib/billing/aws-marketplace/notifications' +import { verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' import { type AwsMarketplaceLicenseRecord, consumeLicenseClaim, findLicenseByClaimHash, + isNewerRead, + listAccountLicenses, listAdministeredOrganizations, listOrganizationLicenses, lockLicense, recordLicenseVerification, + saveLicenseContact, } from '@/lib/billing/aws-marketplace/repository' import { defineAuthorizedOrganizationUseCase, @@ -40,19 +38,19 @@ function isClaimOpen(license: AwsMarketplaceLicenseRecord, now: Date): boolean { } const NO_ACTIVE_SUBSCRIPTION = 'AWS Marketplace reports no active subscription for this license.' +const SUBSCRIPTION_UNCONFIRMED = + 'AWS is still confirming your subscription. Try again in a few minutes.' /** - * Under the row lock: a newer verification than this request's wins. If it says the license - * lapsed, the take is refused; otherwise it stands and this older read is not written. + * Under the row lock: a newer verification than this request's wins, so a license a newer read + * says is not active cannot be taken. */ -function newerVerificationGate( - locked: AwsMarketplaceLicenseRecord, - verifiedAt: Date, - summary: EntitlementSummary -) { - if (locked.lastVerifiedAt.getTime() <= verifiedAt.getTime()) return { summary, verifiedAt } - if (locked.status !== 'active') throw new OrchestrationError('conflict', NO_ACTIVE_SUBSCRIPTION) - return null +function requireNotOverruled(locked: AwsMarketplaceLicenseRecord, verifiedAt: Date): void { + if (isNewerRead(locked, verifiedAt) || locked.status === 'active') return + throw new OrchestrationError( + 'conflict', + locked.status === 'pending' ? SUBSCRIPTION_UNCONFIRMED : NO_ACTIVE_SUBSCRIPTION + ) } const CLAIM_NOT_FOUND = @@ -77,7 +75,7 @@ function requireOpenClaim( } /** A license linked to one organization is never moved, and never taken without one. */ -function requireLinkableTo( +export function requireLinkableTo( license: AwsMarketplaceLicenseRecord, organizationId: string | null ): void { @@ -89,7 +87,28 @@ function requireLinkableTo( } } -/** The pending claim this browser holds, and where the signed-in person could link it. */ +/** Enough for a buyer to recognize their account, and no more for someone holding their claim. */ +function maskAwsAccount(accountId: string): string { + return `••••${accountId.slice(-4)}` +} + +function claimedLicenseView(license: AwsMarketplaceLicenseRecord) { + return { + id: license.id, + status: license.status, + dimension: license.dimension, + quantity: license.quantity, + expiresAt: license.expiresAt?.toISOString() ?? null, + awsAccount: maskAwsAccount(license.customerAwsAccountId), + contactEmail: license.contactEmail, + companyName: license.companyName, + } +} + +/** + * The pending claim this browser holds, and where the signed-in person could link it. Who a linked + * license belongs to is said only to someone who administers that organization. + */ export const readAwsMarketplaceClaim: OperationUseCase< typeof awsMarketplaceOperations.readClaim, { claimToken: string | null }, @@ -103,18 +122,20 @@ export const readAwsMarketplaceClaim: OperationUseCase< const license = await findLicenseByClaimHash(hashClaimToken(input.claimToken)) if (!license) return { status: 'missing' } if (!isClaimOpen(license, new Date())) return { status: 'expired' } - return { - status: 'ready', - license: { - id: license.id, - status: license.status, - dimension: license.dimension, - quantity: license.quantity, - expiresAt: license.expiresAt?.toISOString() ?? null, - organizationId: license.organizationId, - }, - organizations: await listAdministeredOrganizations(principal.userId), + if (license.status === 'inactive') return { status: 'inactive' } + if (license.status === 'pending') { + return { status: 'pending', license: claimedLicenseView(license) } } + const organizations = await listAdministeredOrganizations(principal.userId) + if (license.organizationId) { + return { + status: 'linked', + organizationId: organizations.some((org) => org.id === license.organizationId) + ? license.organizationId + : null, + } + } + return { status: 'ready', license: claimedLicenseView(license), organizations } }, } @@ -122,12 +143,17 @@ interface ClaimInput { licenseId: string /** The raw claim token from this browser's httpOnly cookie, or null when it holds none. */ claimToken: string | null +} + +interface BuyerContact { /** Whom to contact about provisioning; AWS asks the registration page to collect it. */ contactEmail: string companyName: string | null } -interface LinkLicenseInput extends ClaimInput { +type TakeInput = ClaimInput & BuyerContact + +interface LinkLicenseInput extends TakeInput { organizationId: string } @@ -137,9 +163,13 @@ interface LinkedLicense { newlyLinked: boolean } +function contactOf(input: BuyerContact) { + return { email: input.contactEmail, companyName: input.companyName } +} + /** * Checks this browser's claim, then asks the Entitlement Service again: a license is taken only - * while AWS says it is current. A lapsed answer is recorded before the refusal. The caller + * while AWS says it is current. An answer that is not is recorded before the refusal. The caller * rechecks the claim under the row lock, so two tabs racing one claim take it at most once. */ async function verifyClaimedLicense(input: ClaimInput, organizationId: string | null) { @@ -154,21 +184,24 @@ async function verifyClaimedLicense(input: ClaimInput, organizationId: string | const verifiedAt = new Date() const summary = await verifyLicense(productCode, claimed.licenseArn) + const verification = { summary, verifiedAt } if (!summary.active) { - await notifyTeamOfVerification( - await recordLicenseVerification({ - licenseArn: claimed.licenseArn, - productCode, - summary, - verifiedAt, - }) + const outcome = await recordLicenseVerification({ + licenseArn: claimed.licenseArn, + productCode, + verification, + }) + throw new OrchestrationError( + 'conflict', + outcome.kind === 'updated' && outcome.license.status === 'pending' + ? SUBSCRIPTION_UNCONFIRMED + : NO_ACTIVE_SUBSCRIPTION ) - throw new OrchestrationError('conflict', NO_ACTIVE_SUBSCRIPTION) } - return { claimHash, summary, verifiedAt } + return { claimHash, verification } } -function licenseAuditMetadata(license: AwsMarketplaceLicenseRecord) { +export function licenseAuditMetadata(license: AwsMarketplaceLicenseRecord) { return { dimension: license.dimension, quantity: license.quantity, @@ -183,17 +216,11 @@ function licenseAuditMetadata(license: AwsMarketplaceLicenseRecord) { */ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ operation: awsMarketplaceOperations.linkLicense, - authorizeResource: () => { - requireProductCode() - }, async execute({ input, context, }: OrganizationUseCaseContext): Promise { - const { claimHash, summary, verifiedAt } = await verifyClaimedLicense( - input, - input.organizationId - ) + const { claimHash, verification } = await verifyClaimedLicense(input, input.organizationId) return db.transaction(async (tx) => { const locked = requireOpenClaim( await lockLicense(tx, input.licenseId), @@ -201,7 +228,8 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ claimHash ) requireLinkableTo(locked, input.organizationId) - /** Holds the admin's membership until commit, so a concurrent demotion waits for the link. */ + requireNotOverruled(locked, verification.verifiedAt) + // Holds the admin's membership until commit, so a concurrent demotion waits for the link. await requireOrganizationSubjectMembership( context.userId, input.organizationId, @@ -211,11 +239,10 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ { executor: tx, forShare: true } ) const newlyLinked = locked.organizationId !== input.organizationId - const license = await consumeLicenseClaim(tx, { - licenseId: input.licenseId, + const license = await consumeLicenseClaim(tx, locked, { link: newlyLinked ? { organizationId: input.organizationId, userId: context.userId } : null, - contact: { email: input.contactEmail, companyName: input.companyName }, - verification: newerVerificationGate(locked, verifiedAt, summary), + contact: contactOf(input), + verification, }) return { license, newlyLinked } }) @@ -223,7 +250,6 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ projectAudit: ({ result }) => result.newlyLinked ? { - workspaceId: null, action: AuditAction.AWS_MARKETPLACE_LICENSE_LINKED, resourceType: AuditResourceType.AWS_MARKETPLACE_LICENSE, resourceId: result.license.id, @@ -232,12 +258,6 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ metadata: licenseAuditMetadata(result.license), } : [], - afterSuccess: async ({ result, input, context }) => { - if (result.newlyLinked) { - await notifyTeamOfTakenLicense(result.license, input.organizationId, context.userId) - await notifyBuyerOfTakenLicense(result.license) - } - }, }) /** @@ -248,13 +268,13 @@ export const linkAwsMarketplaceLicense = defineAuthorizedOrganizationUseCase({ */ export const registerAwsMarketplaceLicenseToAccount: OperationUseCase< typeof awsMarketplaceOperations.registerToAccount, - ClaimInput, + TakeInput, { licenseId: string } > = { operation: awsMarketplaceOperations.registerToAccount, async execute({ principal, input, request }) { assertOperationPrincipal(principal, awsMarketplaceOperations.registerToAccount) - const { claimHash, summary, verifiedAt } = await verifyClaimedLicense(input, null) + const { claimHash, verification } = await verifyClaimedLicense(input, null) const { license, newlyRegistered } = await db.transaction(async (tx) => { const locked = requireOpenClaim( await lockLicense(tx, input.licenseId), @@ -262,12 +282,12 @@ export const registerAwsMarketplaceLicenseToAccount: OperationUseCase< claimHash ) requireLinkableTo(locked, null) + requireNotOverruled(locked, verification.verifiedAt) const newlyRegistered = locked.linkedByUserId !== principal.userId - const consumed = await consumeLicenseClaim(tx, { - licenseId: input.licenseId, + const consumed = await consumeLicenseClaim(tx, locked, { link: newlyRegistered ? { organizationId: null, userId: principal.userId } : null, - contact: { email: input.contactEmail, companyName: input.companyName }, - verification: newerVerificationGate(locked, verifiedAt, summary), + contact: contactOf(input), + verification, }) return { license: consumed, newlyRegistered } }) @@ -288,13 +308,55 @@ export const registerAwsMarketplaceLicenseToAccount: OperationUseCase< }, ] ) - await notifyTeamOfTakenLicense(license, null, principal.userId) - await notifyBuyerOfTakenLicense(license) } return { licenseId: license.id } }, } +/** + * While AWS is still confirming a license, its buyer leaves contact details here and asks AWS + * again. The claim stays open: linking waits for a read that says the license is current. + */ +export const saveAwsMarketplaceClaimContact: OperationUseCase< + typeof awsMarketplaceOperations.saveClaimContact, + TakeInput, + { status: 'pending' | 'active' | 'inactive' } +> = { + operation: awsMarketplaceOperations.saveClaimContact, + async execute({ principal, input }) { + assertOperationPrincipal(principal, awsMarketplaceOperations.saveClaimContact) + const productCode = requireProductCode() + const claimHash = input.claimToken ? hashClaimToken(input.claimToken) : null + const claimed = requireOpenClaim( + claimHash ? await findLicenseByClaimHash(claimHash) : null, + input.licenseId, + claimHash + ) + const verifiedAt = new Date() + const summary = await verifyLicense(productCode, claimed.licenseArn) + return db.transaction(async (tx) => { + const locked = requireOpenClaim( + await lockLicense(tx, input.licenseId), + input.licenseId, + claimHash + ) + const license = await saveLicenseContact(tx, locked, { + contact: contactOf(input), + verification: { summary, verifiedAt }, + }) + return { status: license.status } + }) + }, +} + +function toLicenseStatusList(rows: Awaited>) { + return rows.map((row) => ({ + ...row, + expiresAt: row.expiresAt?.toISOString() ?? null, + lastVerifiedAt: row.lastVerifiedAt.toISOString(), + })) +} + /** * The Marketplace licenses linked to an organization, so its members can see their subscription * in Sim. Empty where Sim does not sell on AWS, so the billing page shows nothing there. @@ -303,6 +365,20 @@ export const listOrganizationAwsMarketplaceLicenses = defineAuthorizedOrganizati operation: awsMarketplaceOperations.listOrganizationLicenses, async execute({ context }: OrganizationUseCaseContext<{ organizationId: string }>) { if (!getAwsMarketplaceProductCode()) return [] - return listOrganizationLicenses(context.organizationId) + return toLicenseStatusList(await listOrganizationLicenses(context.organizationId)) }, }) + +/** The licenses registered to the caller's account with no organization yet, for their billing page. */ +export const listAccountAwsMarketplaceLicenses: OperationUseCase< + typeof awsMarketplaceOperations.listAccountLicenses, + Record, + ReturnType +> = { + operation: awsMarketplaceOperations.listAccountLicenses, + async execute({ principal }) { + assertOperationPrincipal(principal, awsMarketplaceOperations.listAccountLicenses) + if (!getAwsMarketplaceProductCode()) return [] + return toLicenseStatusList(await listAccountLicenses(principal.userId)) + }, +} diff --git a/apps/sim/lib/billing/aws-marketplace/claim.ts b/apps/sim/lib/billing/aws-marketplace/claim.ts index 7b9760ceb31..d339514c083 100644 --- a/apps/sim/lib/billing/aws-marketplace/claim.ts +++ b/apps/sim/lib/billing/aws-marketplace/claim.ts @@ -4,7 +4,8 @@ import { generateShortId } from '@sim/utils/id' /** Holds the buyer's one-time claim between the AWS redirect and linking an organization. */ export const AWS_MARKETPLACE_CLAIM_COOKIE = 'sim_aws_marketplace_claim' -export const AWS_MARKETPLACE_CLAIM_TTL_MS = 24 * 60 * 60 * 1000 +/** Matches how long AWS keeps a registration token valid. */ +export const AWS_MARKETPLACE_CLAIM_TTL_MS = 4 * 60 * 60 * 1000 /** ~190 random bits; only its hash is stored. */ export function issueClaimToken(): { token: string; hash: string } { @@ -18,8 +19,8 @@ export function hashClaimToken(token: string): string { /** * The `Set-Cookie` value carrying a claim token, or clearing it when `token` is null. Lax is the - * strictest policy that still reaches the onboarding page: AWS opens it with a cross-site form - * POST whose 303 lands as a top-level GET. + * strictest policy that lets the first read work: with Strict, the page AWS's cross-site POST + * redirects to would not send the cookie. Lax also keeps it off cross-site POSTs to the claim routes. */ export function serializeClaimCookie(token: string | null): string { const maxAge = token === null ? 0 : Math.floor(AWS_MARKETPLACE_CLAIM_TTL_MS / 1000) diff --git a/apps/sim/lib/billing/aws-marketplace/client.ts b/apps/sim/lib/billing/aws-marketplace/client.ts index 172b29b7a98..44734f79345 100644 --- a/apps/sim/lib/billing/aws-marketplace/client.ts +++ b/apps/sim/lib/billing/aws-marketplace/client.ts @@ -1,13 +1,26 @@ import { MarketplaceEntitlementServiceClient } from '@aws-sdk/client-marketplace-entitlement-service' import { MarketplaceMeteringClient } from '@aws-sdk/client-marketplace-metering' import { SQSClient } from '@aws-sdk/client-sqs' -import { fromHttp } from '@aws-sdk/credential-provider-http' +import { fromHttp } from '@aws-sdk/credential-providers' import { getAwsCredentials } from '@/lib/core/config/aws' import { env } from '@/lib/core/config/env' -/** The Marketplace metering and entitlement endpoints for SaaS products live only here. */ +/** + * The Entitlement Service exists only here. Metering is multi-region, but a registration token is + * resolved here too, and infra runs the event queue in this region. + */ const MARKETPLACE_REGION = 'us-east-1' +/** + * A stalled connection fails instead of hanging. The SDK's default three attempts retry throttling, + * 5xx, and these timeouts. + */ +const REQUEST_HANDLER = { + connectionTimeout: 3_000, + requestTimeout: 10_000, + throwOnRequestTimeout: true, +} + let meteringClient: MarketplaceMeteringClient | null = null let entitlementClient: MarketplaceEntitlementServiceClient | null = null let queueClient: SQSClient | null = null @@ -20,41 +33,34 @@ let queueClient: SQSClient | null = null * so whenever the container credential endpoint is present these clients sign as the task role. * Elsewhere (local development, tests) they keep the default chain. */ -function marketplaceCredentials() { +function marketplaceClientConfig() { const relativeUri = env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI const fullUri = env.AWS_CONTAINER_CREDENTIALS_FULL_URI - if (relativeUri || fullUri) { - return fromHttp({ - awsContainerCredentialsRelativeUri: relativeUri, - awsContainerCredentialsFullUri: fullUri, - }) + return { + region: MARKETPLACE_REGION, + requestHandler: REQUEST_HANDLER, + credentials: + relativeUri || fullUri + ? fromHttp({ + awsContainerCredentialsRelativeUri: relativeUri, + awsContainerCredentialsFullUri: fullUri, + }) + : getAwsCredentials(), } - return getAwsCredentials() } /** Built on first use, so a deployment with the feature off never reaches for credentials. */ export function getMeteringClient(): MarketplaceMeteringClient { - meteringClient ??= new MarketplaceMeteringClient({ - region: MARKETPLACE_REGION, - credentials: marketplaceCredentials(), - }) + meteringClient ??= new MarketplaceMeteringClient(marketplaceClientConfig()) return meteringClient } -/** Single attempt per call: `fetchLicenseEntitlements` owns the retry loop and its pacing. */ export function getEntitlementClient(): MarketplaceEntitlementServiceClient { - entitlementClient ??= new MarketplaceEntitlementServiceClient({ - region: MARKETPLACE_REGION, - credentials: marketplaceCredentials(), - maxAttempts: 1, - }) + entitlementClient ??= new MarketplaceEntitlementServiceClient(marketplaceClientConfig()) return entitlementClient } export function getEventsQueueClient(): SQSClient { - queueClient ??= new SQSClient({ - region: MARKETPLACE_REGION, - credentials: marketplaceCredentials(), - }) + queueClient ??= new SQSClient(marketplaceClientConfig()) return queueClient } diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts index d251061371f..a297689a73f 100644 --- a/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts @@ -12,10 +12,7 @@ vi.mock('@aws-sdk/client-marketplace-entitlement-service', () => ({ }, })) -import { - fetchLicenseEntitlements, - summarizeEntitlements, -} from '@/lib/billing/aws-marketplace/entitlements' +import { verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' const PRODUCT = 'product-placeholder' const LICENSE = 'license-placeholder' @@ -25,91 +22,62 @@ function entitlement(overrides: Partial = {}): Entitlement { return { ProductCode: PRODUCT, Dimension: 'enterprise', - CustomerIdentifier: 'customer-placeholder', Value: { IntegerValue: 25 }, ...overrides, } } -function awsError(name: string, status: number): Error { - return Object.assign(new Error(name), { name, $metadata: { httpStatusCode: status } }) +function read(...entitlements: Entitlement[]) { + mockSend.mockResolvedValueOnce({ Entitlements: entitlements }) + return verifyLicense(PRODUCT, LICENSE) } -describe('summarizeEntitlements', () => { - it('reads an entitlement that expired before now as inactive', () => { - const summary = summarizeEntitlements( - [entitlement({ ExpirationDate: new Date('2026-05-31T23:59:59.000Z') })], - PRODUCT, - NOW - ) - expect(summary).toMatchObject({ active: false, entitlementCount: 1, quantity: 25 }) - }) - - it('reads an entitlement with a future expiry, or none at all, as active', () => { - expect( - summarizeEntitlements( - [entitlement({ ExpirationDate: new Date('2027-06-01T00:00:00.000Z') })], - PRODUCT, - NOW - ).active - ).toBe(true) - expect(summarizeEntitlements([entitlement()], PRODUCT, NOW).active).toBe(true) +describe('verifyLicense', () => { + beforeEach(() => { + mockSend.mockReset() + vi.useFakeTimers({ toFake: ['Date'] }) + vi.setSystemTime(NOW) }) - it('treats an entitlement expiring exactly now as lapsed', () => { - expect(summarizeEntitlements([entitlement({ ExpirationDate: NOW })], PRODUCT, NOW).active).toBe( - false - ) + afterEach(() => { + vi.useRealTimers() }) - it('ignores an entitlement for another product', () => { - const summary = summarizeEntitlements( - [entitlement({ ProductCode: 'other-product' })], - PRODUCT, - NOW + it('reads an entitlement that expired before now as inactive', async () => { + const summary = await read( + entitlement({ ExpirationDate: new Date('2026-05-31T23:59:59.000Z') }) ) - expect(summary).toMatchObject({ active: false, entitlementCount: 0, dimension: null }) + expect(summary).toMatchObject({ active: false, entitlementCount: 1, quantity: 25 }) }) - it('describes the current entitlement when another one has lapsed', () => { - const summary = summarizeEntitlements( - [ - entitlement({ - Dimension: 'a-lapsed', - ExpirationDate: new Date('2026-01-01T00:00:00.000Z'), - Value: { IntegerValue: 5 }, - }), - entitlement({ - Dimension: 'b-current', - ExpirationDate: new Date('2027-01-01T00:00:00.000Z'), - Value: { IntegerValue: 50 }, - }), - ], - PRODUCT, - NOW - ) - expect(summary).toMatchObject({ active: true, dimension: 'b-current', quantity: 50 }) + it('reads an entitlement with a future expiry, or none at all, as active', async () => { + const future = entitlement({ ExpirationDate: new Date('2027-06-01T00:00:00.000Z') }) + expect((await read(future)).active).toBe(true) + expect((await read(entitlement())).active).toBe(true) }) - it('reads quantity only from an integer value, never from another value kind', () => { - expect( - summarizeEntitlements([entitlement({ Value: { DoubleValue: 1200.5 } })], PRODUCT, NOW) - .quantity - ).toBeNull() - expect( - summarizeEntitlements([entitlement({ Value: { IntegerValue: 2.5 } })], PRODUCT, NOW).quantity - ).toBeNull() + it('treats an entitlement expiring exactly now as lapsed', async () => { + expect((await read(entitlement({ ExpirationDate: NOW }))).active).toBe(false) }) -}) -describe('fetchLicenseEntitlements', () => { - beforeEach(() => { - mockSend.mockReset() - vi.useFakeTimers() + it('describes the current entitlement when another one has lapsed', async () => { + const summary = await read( + entitlement({ + Dimension: 'a-lapsed', + ExpirationDate: new Date('2026-01-01T00:00:00.000Z'), + Value: { IntegerValue: 5 }, + }), + entitlement({ + Dimension: 'b-current', + ExpirationDate: new Date('2027-01-01T00:00:00.000Z'), + Value: { IntegerValue: 50 }, + }) + ) + expect(summary).toMatchObject({ active: true, dimension: 'b-current', quantity: 50 }) }) - afterEach(() => { - vi.useRealTimers() + it('reads quantity only from an integer value, never from another value kind', async () => { + expect((await read(entitlement({ Value: { DoubleValue: 1200.5 } }))).quantity).toBeNull() }) it('follows NextToken through an empty page to the last one', async () => { @@ -118,45 +86,13 @@ describe('fetchLicenseEntitlements', () => { .mockResolvedValueOnce({ Entitlements: [], NextToken: 't2' }) .mockResolvedValueOnce({ Entitlements: [entitlement({ Dimension: 'two' })] }) - const entitlements = await fetchLicenseEntitlements(PRODUCT, LICENSE) + const summary = await verifyLicense(PRODUCT, LICENSE) - expect(entitlements.map((row) => row.Dimension)).toEqual(['one', 'two']) + expect(summary).toMatchObject({ entitlementCount: 2, dimension: 'one' }) expect(mockSend.mock.calls.map(([command]) => command.input.NextToken)).toEqual([ undefined, 't1', 't2', ]) }) - - it('retries a throttled page and returns once it succeeds', async () => { - mockSend - .mockRejectedValueOnce(awsError('ThrottlingException', 400)) - .mockRejectedValueOnce(awsError('InternalServiceErrorException', 500)) - .mockResolvedValueOnce({ Entitlements: [entitlement()] }) - - const pending = fetchLicenseEntitlements(PRODUCT, LICENSE) - await vi.runAllTimersAsync() - - expect(await pending).toHaveLength(1) - expect(mockSend).toHaveBeenCalledTimes(3) - }) - - it('gives up after three throttled attempts', async () => { - mockSend.mockRejectedValue(awsError('ThrottlingException', 400)) - - const pending = fetchLicenseEntitlements(PRODUCT, LICENSE).catch((error: unknown) => error) - await vi.runAllTimersAsync() - - expect(await pending).toMatchObject({ name: 'ThrottlingException' }) - expect(mockSend).toHaveBeenCalledTimes(3) - }) - - it('does not retry a request the service rejected as invalid', async () => { - mockSend.mockRejectedValue(awsError('InvalidParameterException', 400)) - - await expect(fetchLicenseEntitlements(PRODUCT, LICENSE)).rejects.toMatchObject({ - name: 'InvalidParameterException', - }) - expect(mockSend).toHaveBeenCalledTimes(1) - }) }) diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.ts index 2234e0a17f7..b5d96fd1f29 100644 --- a/apps/sim/lib/billing/aws-marketplace/entitlements.ts +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.ts @@ -1,28 +1,22 @@ import { type Entitlement, GetEntitlementsCommand, - type GetEntitlementsCommandOutput, } from '@aws-sdk/client-marketplace-entitlement-service' -import { interruptibleSleep } from '@sim/utils/helpers' -import { backoffWithJitter } from '@sim/utils/retry' import { compareStrings } from '@sim/utils/string' import { getEntitlementClient } from '@/lib/billing/aws-marketplace/client' -const MAX_ATTEMPTS = 3 -const RETRY_BACKOFF = { baseMs: 200, maxMs: 2_000 } /** A license carries one entitlement per purchased dimension; this bounds a runaway `NextToken`. */ const MAX_PAGES = 20 /** What the last Entitlement Service read says about one license. */ export interface EntitlementSummary { - /** At least one entitlement for our product has no expiry or expires in the future. */ + /** At least one entitlement has no expiry or expires in the future. */ active: boolean - /** Entitlements AWS returned for our product, current or lapsed; zero once deprovisioned. */ + /** Entitlements AWS returned, current or lapsed; zero is ambiguous, see the license's status. */ entitlementCount: number dimension: string | null quantity: number | null expiresAt: Date | null - customerIdentifier: string | null customerAwsAccountId: string | null } @@ -36,73 +30,37 @@ function isCurrent(entitlement: Entitlement, now: Date): boolean { * still describes what lapsed. Quantity is read only from an integer value: a contract's dollar * amount is never inferred from it. */ -export function summarizeEntitlements( +function summarizeEntitlements( entitlements: readonly Entitlement[], - productCode: string, now: Date ): EntitlementSummary { - const ours = entitlements.filter((entitlement) => entitlement.ProductCode === productCode) - const current = ours + const current = entitlements .filter((entitlement) => isCurrent(entitlement, now)) .sort((left, right) => compareStrings(left.Dimension ?? '', right.Dimension ?? '')) - const lapsed = [...ours].sort( + const lapsed = [...entitlements].sort( (left, right) => (right.ExpirationDate?.getTime() ?? 0) - (left.ExpirationDate?.getTime() ?? 0) ) const chosen = current[0] ?? lapsed[0] - const integerValue = chosen?.Value?.IntegerValue return { active: current.length > 0, - entitlementCount: ours.length, + entitlementCount: entitlements.length, dimension: chosen?.Dimension ?? null, - quantity: - typeof integerValue === 'number' && Number.isInteger(integerValue) ? integerValue : null, + quantity: chosen?.Value?.IntegerValue ?? null, expiresAt: chosen?.ExpirationDate ?? null, - customerIdentifier: chosen?.CustomerIdentifier ?? null, customerAwsAccountId: chosen?.CustomerAWSAccountId ?? null, } } -interface AwsServiceErrorShape { - name?: string - $metadata?: { httpStatusCode?: number } -} - -/** Throttling, a server fault, or no HTTP response at all (network, timeout). */ -function isRetryable(error: unknown): boolean { - const shape = error as AwsServiceErrorShape - if (shape?.name === 'ThrottlingException' || shape?.name === 'InternalServiceErrorException') { - return true - } - const status = shape?.$metadata?.httpStatusCode - if (typeof status !== 'number') return true - return status === 429 || status >= 500 -} - /** * The Entitlement Service rejected the request itself (a malformed license ARN or product code). * Asking again cannot succeed; every other failure, including a misconfigured IAM grant, may. */ export function isInvalidEntitlementRequest(error: unknown): boolean { - return (error as AwsServiceErrorShape)?.name === 'InvalidParameterException' -} - -async function getEntitlementsPage( - command: GetEntitlementsCommand, - signal?: AbortSignal -): Promise { - for (let attempt = 1; ; attempt++) { - signal?.throwIfAborted() - try { - return await getEntitlementClient().send(command, { abortSignal: signal }) - } catch (error) { - if (attempt >= MAX_ATTEMPTS || !isRetryable(error) || signal?.aborted) throw error - await interruptibleSleep(backoffWithJitter(attempt, null, RETRY_BACKOFF), signal) - } - } + return error instanceof Error && error.name === 'InvalidParameterException' } /** Every entitlement AWS holds for one license of our product, across all pages. */ -export async function fetchLicenseEntitlements( +async function fetchLicenseEntitlements( productCode: string, licenseArn: string, signal?: AbortSignal @@ -110,13 +68,13 @@ export async function fetchLicenseEntitlements( const entitlements: Entitlement[] = [] let nextToken: string | undefined for (let page = 0; page < MAX_PAGES; page++) { - const result = await getEntitlementsPage( + const result = await getEntitlementClient().send( new GetEntitlementsCommand({ ProductCode: productCode, Filter: { LICENSE_ARN: [licenseArn] }, NextToken: nextToken, }), - signal + { abortSignal: signal } ) entitlements.push(...(result.Entitlements ?? [])) nextToken = result.NextToken @@ -132,5 +90,5 @@ export async function verifyLicense( signal?: AbortSignal ): Promise { const entitlements = await fetchLicenseEntitlements(productCode, licenseArn, signal) - return summarizeEntitlements(entitlements, productCode, new Date()) + return summarizeEntitlements(entitlements, new Date()) } diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts index a6de5c15725..c809040a2cf 100644 --- a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -1,11 +1,18 @@ import type { Entitlement } from '@aws-sdk/client-marketplace-entitlement-service' import { db } from '@sim/db' -import { auditLog, awsMarketplaceLicense, member, organization, user } from '@sim/db/schema' +import { + auditLog, + awsMarketplaceLicense, + member, + organization, + outboxEvent, + user, +} from '@sim/db/schema' import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' import { emailMailerMock, emailMailerMockFns } from '@sim/testing/mocks/email-mailer.mock' import { requestUtilsMock, requestUtilsMockFns } from '@sim/testing/mocks/request.mock' import { generateId } from '@sim/utils/id' -import { and, eq, inArray } from 'drizzle-orm' +import { and, asc, eq, inArray } from 'drizzle-orm' import { NextRequest } from 'next/server' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' @@ -15,16 +22,12 @@ const ACCOUNT = '000000000000' const AGREEMENT = 'agmt-placeholder' const CONTACT = { contactEmail: 'buyer@example.test', companyName: 'Example Co' } -const { meteringSend, entitlementSend, queueSend, brandLookup } = vi.hoisted(() => { +const { meteringSend, entitlementSend, queueSend } = vi.hoisted(() => { process.env.AWS_MARKETPLACE_PRODUCT_CODE = 'product-placeholder' process.env.AWS_MARKETPLACE_EVENTS_QUEUE_URL = 'https://queue.example.test/aws-marketplace-placeholder' - return { - meteringSend: vi.fn(), - entitlementSend: vi.fn(), - queueSend: vi.fn(), - brandLookup: { fail: false }, - } + process.env.ADMIN_API_KEY = 'admin-key-placeholder' + return { meteringSend: vi.fn(), entitlementSend: vi.fn(), queueSend: vi.fn() } }) vi.mock('@aws-sdk/client-marketplace-metering', () => ({ @@ -55,28 +58,40 @@ vi.mock('@aws-sdk/client-sqs', () => ({ readonly kind = 'delete' constructor(public input: { ReceiptHandle: string }) {} }, + ChangeMessageVisibilityCommand: class { + readonly kind = 'extend' + constructor(public input: { ReceiptHandle: string }) {} + }, })) -vi.mock('@/ee/whitelabeling', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - getBrandConfig: () => { - if (brandLookup.fail) throw new Error('Brand lookup failed') - return actual.getBrandConfig() - }, - } -}) vi.mock('@/lib/messaging/email/mailer', () => emailMailerMock) vi.mock('@/lib/core/utils/request', () => requestUtilsMock) import { linkAwsMarketplaceLicense, + listAccountAwsMarketplaceLicenses, listOrganizationAwsMarketplaceLicenses, + readAwsMarketplaceClaim, registerAwsMarketplaceLicenseToAccount, + saveAwsMarketplaceClaimContact, } from '@/lib/billing/aws-marketplace/application/use-cases' import { hashClaimToken } from '@/lib/billing/aws-marketplace/claim' +import { + AWS_MARKETPLACE_BUYER_NOTICE_EVENT, + AWS_MARKETPLACE_TEAM_NOTICE_EVENT, +} from '@/lib/billing/aws-marketplace/notice-events' +import { awsMarketplaceOutboxHandlers } from '@/lib/billing/aws-marketplace/notifications' import { reconcileAwsMarketplaceLicenses } from '@/lib/billing/aws-marketplace/reconcile' +import { processOutboxEventById } from '@/lib/core/outbox/service' import { POST as register, GET as revisit } from '@/app/api/marketplace/aws/register/route' +import { + POST as adminLink, + GET as adminList, +} from '@/app/api/v1/admin/dashboard/aws-marketplace-licenses/route' + +const NOTICE_EVENTS = [AWS_MARKETPLACE_TEAM_NOTICE_EVENT, AWS_MARKETPLACE_BUYER_NOTICE_EVENT] +const DAY_MS = 24 * 60 * 60 * 1000 +const TEAM = /^enterprise@/ +const BUYER = /^buyer@example\.test$/ /** What GetEntitlements answers for each license ARN; an Error is thrown instead. */ const entitlementsByLicense = new Map() @@ -94,13 +109,17 @@ function uniqueAccount(): string { return generateId().replace(/\D/g, '').padEnd(12, '7').slice(0, 12) } +function uniqueAgreement(): string { + return `agmt-${generateId()}` +} + function currentEntitlement(overrides: Partial = {}): Entitlement { return { ProductCode: PRODUCT, Dimension: 'enterprise', CustomerAWSAccountId: ACCOUNT, Value: { IntegerValue: 25 }, - ExpirationDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), + ExpirationDate: new Date(Date.now() + 365 * DAY_MS), ...overrides, } } @@ -117,7 +136,7 @@ function throttled(): Error { } function registration(body: string, contentType = 'application/x-www-form-urlencoded') { - requestUtilsMockFns.mockGetClientIp.mockReturnValue(`198.51.100.${licenseArns.length + 1}`) + requestUtilsMockFns.mockGetClientIp.mockReturnValue(`198.51.100.${licenseArns.length % 250}`) return register( new NextRequest('http://localhost:3000/api/marketplace/aws/register', { method: 'POST', @@ -137,6 +156,16 @@ function resolvesTo(licenseArn: string, productCode = PRODUCT) { }) } +function errorOf(response: Response): string | null { + return new URL(response.headers.get('location') ?? '').searchParams.get('error') +} + +function claimTokenOf(response: Response): string | null { + return ( + /sim_aws_marketplace_claim=([^;]+)/.exec(response.headers.get('set-cookie') ?? '')?.[1] ?? null + ) +} + async function readLicense(licenseArn: string) { const [row] = await db .select() @@ -179,13 +208,10 @@ async function createOrganization(members: Array<{ userId: string; role: string return id } -/** A registered, active license whose claim `token` this browser holds. */ -async function createClaimedLicense( - overrides: Partial = {} -) { - const licenseArn = uniqueArn() - const token = generateId() +async function insertLicense(overrides: Partial = {}) { + const licenseArn = overrides.licenseArn ?? uniqueArn() const id = generateId() + const lastVerifiedAt = overrides.lastVerifiedAt ?? new Date() await db.insert(awsMarketplaceLicense).values({ id, licenseArn, @@ -194,12 +220,33 @@ async function createClaimedLicense( status: 'active', dimension: 'enterprise', quantity: 25, - lastVerifiedAt: new Date(), + lastVerifiedAt, + lastVerificationAttemptAt: lastVerifiedAt, + ...overrides, + }) + return { id, licenseArn } +} + +/** A registered, active license whose claim `token` this browser holds. */ +async function createClaimedLicense( + overrides: Partial = {} +) { + const token = generateId() + const license = await insertLicense({ claimTokenHash: hashClaimToken(token), claimExpiresAt: new Date(Date.now() + 60 * 60 * 1000), ...overrides, }) - return { id, licenseArn, token } + return { ...license, token } +} + +async function pendingNoticeIds(): Promise { + const rows = await db + .select({ id: outboxEvent.id }) + .from(outboxEvent) + .where(and(inArray(outboxEvent.eventType, NOTICE_EVENTS), eq(outboxEvent.status, 'pending'))) + .orderBy(asc(outboxEvent.createdAt)) + return rows.map((row) => row.id) } interface SentEmail { @@ -208,15 +255,16 @@ interface SentEmail { text: string } -function sentEmails(to: RegExp): SentEmail[] { +/** Runs every queued notice through the outbox handlers, then reads what reached the mailer. */ +async function emailsTo(to: RegExp): Promise { + for (const id of await pendingNoticeIds()) { + await processOutboxEventById(id, awsMarketplaceOutboxHandlers) + } return emailMailerMockFns.mockSendEmail.mock.calls .map(([email]) => email as SentEmail) .filter((email) => email.to.some((address) => to.test(address))) } -const TEAM = /^enterprise@/ -const BUYER = /^buyer@example\.test$/ - function rejection(promise: Promise): Promise { return promise.then( () => { @@ -226,6 +274,50 @@ function rejection(promise: Promise): Promise { ) } +/** One batch of `events`, then an empty queue; returns the receipt handles deleted. */ +function deliverOnce(events: Array>) { + const deleted: string[] = [] + let delivered = false + queueSend.mockImplementation( + async (command: { kind: string; input: { ReceiptHandle: string } }) => { + if (command.kind === 'delete') { + deleted.push(command.input.ReceiptHandle) + return {} + } + if (command.kind === 'extend') return {} + if (delivered) return { Messages: [] } + delivered = true + return { + Messages: events.map((event, index) => ({ + MessageId: `m-${generateId()}`, + ReceiptHandle: `r-${index}`, + Body: JSON.stringify({ source: 'aws.agreement-marketplace', ...event }), + })), + } + } + ) + return deleted +} + +function licenseUpdated(licenseArn: string, agreementId?: string, time?: Date) { + return { + 'detail-type': 'License Updated - Manufacturer', + ...(time && { time: time.toISOString() }), + detail: { + license: { arn: licenseArn }, + product: { code: PRODUCT }, + ...(agreementId && { agreement: { id: agreementId } }), + }, + } +} + +function agreementCreated(agreementId: string, previousAgreementId?: string) { + return { + 'detail-type': 'Purchase Agreement Created - Proposer', + detail: { agreement: { id: agreementId, previousAgreementId } }, + } +} + beforeAll(() => { entitlementSend.mockImplementation( async (command: { input: { Filter: { LICENSE_ARN: string[] } } }) => { @@ -236,14 +328,17 @@ beforeAll(() => { ) }) -beforeEach(() => { +beforeEach(async () => { meteringSend.mockReset() queueSend.mockReset() + entitlementSend.mockClear() emailMailerMockFns.mockSendEmail.mockReset() emailMailerMockFns.mockSendEmail.mockResolvedValue({ success: true, message: 'sent' }) + await db.delete(outboxEvent).where(inArray(outboxEvent.eventType, NOTICE_EVENTS)) }) afterAll(async () => { + await db.delete(outboxEvent).where(inArray(outboxEvent.eventType, NOTICE_EVENTS)) await db .delete(awsMarketplaceLicense) .where(inArray(awsMarketplaceLicense.licenseArn, licenseArns)) @@ -261,37 +356,47 @@ describe('POST /api/marketplace/aws/register', () => { const response = await registration('unrelated=1') expect(response.status).toBe(303) - expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( - 'invalid' - ) + expect(errorOf(response)).toBe('invalid') expect(response.headers.get('set-cookie')).toBeNull() expect(meteringSend).not.toHaveBeenCalled() }) + it('answers a rate-limited browser with a redirect, not raw JSON', async () => { + requestUtilsMockFns.mockGetClientIp.mockReturnValueOnce(null) + + const limited = await register( + new NextRequest('http://localhost:3000/api/marketplace/aws/register', { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: 'x-amzn-marketplace-token=any', + }), + {} + ) + + expect(limited.status).toBe(303) + expect(errorOf(limited)).toBe('unavailable') + }) + it('rejects a token that resolves to another product without recording it', async () => { const licenseArn = uniqueArn() resolvesTo(licenseArn, 'other-product') const response = await registration('x-amzn-marketplace-token=opaque%2Btoken') - expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( - 'invalid' - ) + expect(errorOf(response)).toBe('invalid') expect(response.headers.get('set-cookie')).toBeNull() expect(await readLicense(licenseArn)).toBeUndefined() expect(meteringSend.mock.calls[0][0].input.RegistrationToken).toBe('opaque+token') }) - it('records an unentitled license as inactive and issues no claim', async () => { + it('records a lapsed license as inactive and issues no claim', async () => { const licenseArn = uniqueArn() resolvesTo(licenseArn) entitlementsByLicense.set(licenseArn, [lapsedEntitlement()]) const response = await registration('x-amzn-marketplace-token=lapsed-token') - expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( - 'no-subscription' - ) + expect(errorOf(response)).toBe('no-subscription') expect(response.headers.get('set-cookie')).toBeNull() expect(await readLicense(licenseArn)).toMatchObject({ status: 'inactive', @@ -300,7 +405,7 @@ describe('POST /api/marketplace/aws/register', () => { }) }) - it('issues an httpOnly claim for an active license and stores only its hash', async () => { + it('issues an httpOnly four-hour claim for an active license and stores only its hash', async () => { const licenseArn = uniqueArn() resolvesTo(licenseArn) entitlementsByLicense.set(licenseArn, [currentEntitlement()]) @@ -315,7 +420,7 @@ describe('POST /api/marketplace/aws/register', () => { expect(cookie).toMatch(/HttpOnly/) expect(cookie).toMatch(/Secure/) expect(cookie).toMatch(/SameSite=Lax/) - const token = /sim_aws_marketplace_claim=([^;]+)/.exec(cookie)?.[1] ?? '' + const token = claimTokenOf(response) ?? '' expect(token.length).toBeGreaterThan(20) const row = await readLicense(licenseArn) expect(row).toMatchObject({ @@ -323,35 +428,34 @@ describe('POST /api/marketplace/aws/register', () => { quantity: 25, claimTokenHash: hashClaimToken(token), }) - expect(row?.claimTokenHash).not.toBe(token) - expect(row?.claimExpiresAt?.getTime()).toBeGreaterThan(Date.now() + 23 * 60 * 60 * 1000) + const remaining = (row?.claimExpiresAt?.getTime() ?? 0) - Date.now() + expect(remaining).toBeGreaterThan(3.9 * 60 * 60 * 1000) + expect(remaining).toBeLessThanOrEqual(4 * 60 * 60 * 1000) }) -}) -describe('the fulfillment URL', () => { - it('registers a new-product buyer that ResolveCustomer names by account ID alone', async () => { + it('registers a new-product buyer named by account ID alone, and tells the team at once', async () => { const licenseArn = uniqueArn() resolvesTo(licenseArn) entitlementsByLicense.set(licenseArn, [currentEntitlement()]) const response = await registration('x-amzn-marketplace-token=new-product-token') - expect(response.headers.get('set-cookie')).toMatch(/sim_aws_marketplace_claim=/) + expect(claimTokenOf(response)).not.toBeNull() expect(await readLicense(licenseArn)).toMatchObject({ status: 'active', customerAwsAccountId: ACCOUNT, - customerIdentifier: null, agreementId: AGREEMENT, }) + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ + subject: expect.stringMatching(/New license/), + text: expect.stringMatching(/may not finish it[\s\S]*not received yet/), + }), + ]) }) - it('does not let an older read restore a license the reconcile has since marked inactive', async () => { - const licenseArn = uniqueArn() - await db.insert(awsMarketplaceLicense).values({ - id: generateId(), - licenseArn, - productCode: PRODUCT, - customerAwsAccountId: ACCOUNT, + it('does not let an older read restore a license a newer read lapsed', async () => { + const { licenseArn } = await insertLicense({ status: 'inactive', lastVerifiedAt: new Date(Date.now() + 5 * 60 * 1000), }) @@ -360,9 +464,7 @@ describe('the fulfillment URL', () => { const response = await registration('x-amzn-marketplace-token=slow-token') - expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( - 'no-subscription' - ) + expect(errorOf(response)).toBe('no-subscription') expect(response.headers.get('set-cookie')).toBeNull() expect(await readLicense(licenseArn)).toMatchObject({ status: 'inactive', @@ -370,15 +472,25 @@ describe('the fulfillment URL', () => { }) }) + it('tells the team when a registration read moves an existing license’s terms', async () => { + const { licenseArn } = await insertLicense({ quantity: 10 }) + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, [currentEntitlement({ Value: { IntegerValue: 40 } })]) + + await registration('x-amzn-marketplace-token=moved-token') + + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/quantity 10[\s\S]*quantity 40/) }), + ]) + }) + it('sends a buyer whose agreement has not started back on its start date, saving nothing', async () => { const account = uniqueAccount() meteringSend.mockResolvedValueOnce({ CustomerAWSAccountId: account, ProductCode: PRODUCT }) const response = await registration('x-amzn-marketplace-token=future-dated-token') - expect(new URL(response.headers.get('location') ?? '').searchParams.get('error')).toBe( - 'not-started' - ) + expect(errorOf(response)).toBe('not-started') expect(response.headers.get('set-cookie')).toBeNull() expect(entitlementSend).not.toHaveBeenCalled() expect( @@ -389,26 +501,6 @@ describe('the fulfillment URL', () => { ).toEqual([]) }) - it('accepts the token as multipart form data', async () => { - const licenseArn = uniqueArn() - resolvesTo(licenseArn) - entitlementsByLicense.set(licenseArn, [currentEntitlement()]) - const form = new FormData() - form.set('x-amzn-marketplace-token', 'multipart-token') - - const response = await register( - new NextRequest('http://localhost:3000/api/marketplace/aws/register', { - method: 'POST', - body: form, - }), - {} - ) - - expect(response.status).toBe(303) - expect(response.headers.get('set-cookie')).toMatch(/sim_aws_marketplace_claim=/) - expect(meteringSend.mock.calls[0][0].input.RegistrationToken).toBe('multipart-token') - }) - it('sends a refreshed or bookmarked fulfillment URL to the onboarding page', async () => { const response = await revisit( new NextRequest('http://localhost:3000/api/marketplace/aws/register'), @@ -421,14 +513,125 @@ describe('the fulfillment URL', () => { }) }) -describe('an organization reading its Marketplace subscription', () => { +describe('a license AWS has not confirmed yet', () => { + it('holds a claim, takes contact details, refuses a take, and proceeds once AWS confirms', async () => { + const buyerId = await createUser() + const principal = createSessionPrincipal({ userId: buyerId }) + const licenseArn = uniqueArn() + resolvesTo(licenseArn) + entitlementsByLicense.set(licenseArn, []) + + const response = await registration('x-amzn-marketplace-token=early-token') + + const claimToken = claimTokenOf(response) + expect(claimToken).not.toBeNull() + const row = await readLicense(licenseArn) + expect(row).toMatchObject({ status: 'pending', linkedByUserId: null }) + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/Unconfirmed/) }), + ]) + + expect( + await readAwsMarketplaceClaim.execute({ principal, input: { claimToken } }) + ).toMatchObject({ status: 'pending' }) + const refused = await rejection( + registerAwsMarketplaceLicenseToAccount.execute({ + principal, + input: { ...CONTACT, licenseId: row.id, claimToken }, + }) + ) + expect(refused).toMatchObject({ + code: 'conflict', + message: expect.stringMatching(/still confirming/), + }) + expect((await readLicense(licenseArn))?.claimTokenHash).not.toBeNull() + + expect( + await saveAwsMarketplaceClaimContact.execute({ + principal, + input: { ...CONTACT, licenseId: row.id, claimToken }, + }) + ).toEqual({ status: 'pending' }) + expect(await readLicense(licenseArn)).toMatchObject({ + contactEmail: CONTACT.contactEmail, + linkedByUserId: null, + }) + await emailsTo(TEAM) + + emailMailerMockFns.mockSendEmail.mockClear() + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + expect( + await saveAwsMarketplaceClaimContact.execute({ + principal, + input: { ...CONTACT, licenseId: row.id, claimToken }, + }) + ).toEqual({ status: 'active' }) + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ subject: expect.stringMatching(/pending → active/) }), + ]) + await registerAwsMarketplaceLicenseToAccount.execute({ + principal, + input: { ...CONTACT, licenseId: row.id, claimToken }, + }) + expect((await readLicense(licenseArn))?.linkedByUserId).toBe(buyerId) + }) + + it('stays pending on a reverify, but a license once confirmed lapses', async () => { + const pending = await insertLicense({ + status: 'pending', + lastVerifiedAt: new Date(Date.now() - 2 * DAY_MS), + }) + const confirmed = await insertLicense({ + claimTokenHash: hashClaimToken(generateId()), + claimExpiresAt: new Date(Date.now() + 60_000), + lastVerifiedAt: new Date(Date.now() - 2 * DAY_MS), + }) + entitlementsByLicense.set(pending.licenseArn, []) + entitlementsByLicense.set(confirmed.licenseArn, []) + deliverOnce([]) + + await reconcileAwsMarketplaceLicenses() + + expect((await readLicense(pending.licenseArn))?.status).toBe('pending') + expect(await readLicense(confirmed.licenseArn)).toMatchObject({ + status: 'inactive', + claimTokenHash: null, + }) + }) +}) + +describe('reading a claim', () => { + it('shows a masked account, and says who holds a linked license only to its administrators', async () => { + const adminId = await createUser() + const strangerId = await createUser() + const organizationId = await createOrganization([{ userId: adminId, role: 'admin' }]) + const open = await createClaimedLicense({ customerAwsAccountId: '123456781234' }) + const linked = await createClaimedLicense({ organizationId }) + const lapsed = await createClaimedLicense({ status: 'inactive' }) + const read = (userId: string, claimToken: string) => + readAwsMarketplaceClaim.execute({ + principal: createSessionPrincipal({ userId }), + input: { claimToken }, + }) + + expect(await read(strangerId, open.token)).toMatchObject({ + status: 'ready', + license: { awsAccount: '••••1234' }, + }) + expect(await read(adminId, linked.token)).toEqual({ status: 'linked', organizationId }) + expect(await read(strangerId, linked.token)).toEqual({ status: 'linked', organizationId: null }) + expect(await read(strangerId, lapsed.token)).toEqual({ status: 'inactive' }) + }) +}) + +describe('reading subscription status', () => { it('shows members only the licenses linked to their organization and refuses others', async () => { const memberId = await createUser() const outsiderId = await createUser() const organizationId = await createOrganization([{ userId: memberId, role: 'member' }]) const otherOrganizationId = await createOrganization([{ userId: outsiderId, role: 'owner' }]) - const ours = await createClaimedLicense({ organizationId, claimTokenHash: null }) - await createClaimedLicense({ organizationId: otherOrganizationId, claimTokenHash: null }) + const ours = await insertLicense({ organizationId }) + await insertLicense({ organizationId: otherOrganizationId }) const licenses = await listOrganizationAwsMarketplaceLicenses.execute({ principal: createSessionPrincipal({ userId: memberId }), @@ -444,6 +647,22 @@ describe('an organization reading its Marketplace subscription', () => { expect(licenses).toEqual([expect.objectContaining({ id: ours.id, status: 'active' })]) expect(refusal).toMatchObject({ code: 'not_found' }) }) + + it('shows a buyer the licenses registered to their account alone', async () => { + const buyerId = await createUser() + const otherId = await createUser() + const organizationId = await createOrganization([{ userId: buyerId, role: 'owner' }]) + const registered = await insertLicense({ linkedByUserId: buyerId }) + await insertLicense({ linkedByUserId: buyerId, organizationId }) + await insertLicense({ linkedByUserId: otherId }) + + const licenses = await listAccountAwsMarketplaceLicenses.execute({ + principal: createSessionPrincipal({ userId: buyerId }), + input: {}, + }) + + expect(licenses).toEqual([expect.objectContaining({ id: registered.id })]) + }) }) describe('linking a claimed license', () => { @@ -467,27 +686,6 @@ describe('linking a claimed license', () => { }) } - it('refuses a browser that holds no claim', async () => { - const license = await createClaimedLicense() - - const error = await rejection(link(ownerId, { licenseId: license.id, claimToken: null })) - - expect(error).toMatchObject({ code: 'forbidden' }) - expect((await readLicense(license.licenseArn))?.organizationId).toBeNull() - }) - - it('refuses an expired claim', async () => { - const license = await createClaimedLicense({ claimExpiresAt: new Date(Date.now() - 1000) }) - entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - - const error = await rejection( - link(ownerId, { licenseId: license.id, claimToken: license.token }) - ) - - expect(error).toMatchObject({ code: 'forbidden', message: expect.stringMatching(/expired/) }) - expect((await readLicense(license.licenseArn))?.organizationId).toBeNull() - }) - it('refuses an organization member who is not an owner or admin', async () => { const license = await createClaimedLicense() entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) @@ -517,45 +715,6 @@ describe('linking a claimed license', () => { expect((await readLicense(license.licenseArn))?.organizationId).toBe(otherOrganizationId) }) - it('refuses when AWS no longer reports a current entitlement', async () => { - const license = await createClaimedLicense() - entitlementsByLicense.set(license.licenseArn, [lapsedEntitlement()]) - - const error = await rejection( - link(ownerId, { licenseId: license.id, claimToken: license.token }) - ) - - expect(error).toMatchObject({ code: 'conflict' }) - expect(await readLicense(license.licenseArn)).toMatchObject({ - organizationId: null, - status: 'inactive', - }) - expect(sentEmails(BUYER)).toEqual([]) - expect(sentEmails(TEAM)).toEqual([ - expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), - ]) - }) - - it('refuses a claim whose read predates a newer inactive verification, and emails no one', async () => { - const license = await createClaimedLicense({ - status: 'inactive', - lastVerifiedAt: new Date(Date.now() + 5 * 60 * 1000), - }) - entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - - const error = await rejection( - link(ownerId, { licenseId: license.id, claimToken: license.token }) - ) - - expect(error).toMatchObject({ code: 'conflict' }) - expect(await readLicense(license.licenseArn)).toMatchObject({ - organizationId: null, - status: 'inactive', - claimTokenHash: hashClaimToken(license.token), - }) - expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() - }) - it('links under a newer active read without overwriting it with its own older one', async () => { const license = await createClaimedLicense({ lastVerifiedAt: new Date(Date.now() + 60_000) }) entitlementsByLicense.set(license.licenseArn, [ @@ -571,25 +730,25 @@ describe('linking a claimed license', () => { }) }) - it('keeps a committed link when a notice lookup fails after the claim is spent', async () => { + it('takes a claim at most once when two tabs race it on Postgres', async () => { const license = await createClaimedLicense() - entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - brandLookup.fail = true + entitlementsByLicense.set(license.licenseArn, [ + currentEntitlement({ ExpirationDate: undefined }), + ]) + const take = () => link(ownerId, { licenseId: license.id, claimToken: license.token }) - try { - await link(ownerId, { licenseId: license.id, claimToken: license.token }) - } finally { - brandLookup.fail = false - } + const results = await Promise.allSettled([take(), take()]) + expect(results.map((result) => result.status).sort()).toEqual(['fulfilled', 'rejected']) expect(await readLicense(license.licenseArn)).toMatchObject({ organizationId, claimTokenHash: null, }) - expect(sentEmails(BUYER)).toEqual([]) + expect(await emailsTo(TEAM)).toHaveLength(1) + expect(await emailsTo(BUYER)).toHaveLength(1) }) - it('links once, consumes the claim, audits the link, and notifies the team', async () => { + it('links once, consumes the claim, audits it, and queues both notices with their terms', async () => { const license = await createClaimedLicense() entitlementsByLicense.set(license.licenseArn, [ currentEntitlement({ Value: { IntegerValue: 40 } }), @@ -606,12 +765,19 @@ describe('linking a claimed license', () => { contactEmail: CONTACT.contactEmail, companyName: CONTACT.companyName, }) - expect(sentEmails(TEAM)).toEqual([ - expect.objectContaining({ text: expect.stringMatching(/Company: Example Co/) }), + const team = await emailsTo(TEAM) + expect(team.map((email) => email.subject)).toEqual([ + expect.stringMatching(/active → active/), + expect.stringMatching(/Linked to an organization/), ]) - expect(sentEmails(BUYER)).toEqual([ + expect(team[1].text).toMatch(/Company: Example Co/) + expect(team[1].text).toMatch(/pausePaymentCollection: true/) + expect(team[1].text).toMatch(/Never send a Stripe invoice/) + expect(await emailsTo(BUYER)).toEqual([ expect.objectContaining({ - text: expect.stringMatching(/within 2 business days[\s\S]*Questions\? Contact \S+@\S+/), + text: expect.stringMatching( + /being set up\. Please wait for us to contact you\. We expect to finish within 2 business days\.[\s\S]*Questions\? Contact \S+@\S+/ + ), }), ]) await vi.waitFor(async () => { @@ -621,103 +787,127 @@ describe('linking a claimed license', () => { .where(and(eq(auditLog.resourceId, license.id), eq(auditLog.actorId, ownerId))) expect(entries).toEqual([{ action: 'aws_marketplace_license.linked' }]) }) - - const replay = await rejection( - link(ownerId, { licenseId: license.id, claimToken: license.token }) - ) - expect(replay).toMatchObject({ code: 'forbidden' }) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) }) }) -describe('registering a claimed license to an account', () => { - let buyerId: string +describe.each([ + { + take: 'linking', + run: async (input: { licenseId: string; claimToken: string | null }) => { + const userId = await createUser() + const organizationId = await createOrganization([{ userId, role: 'owner' }]) + return linkAwsMarketplaceLicense.execute({ + principal: createSessionPrincipal({ userId }), + input: { ...CONTACT, organizationId, ...input }, + }) + }, + }, + { + take: 'registering', + run: async (input: { licenseId: string; claimToken: string | null }) => + registerAwsMarketplaceLicenseToAccount.execute({ + principal: createSessionPrincipal({ userId: await createUser() }), + input: { ...CONTACT, ...input }, + }), + }, +])('$take refuses', ({ run }) => { + it('a browser that holds no claim', async () => { + const license = await createClaimedLicense() - beforeAll(async () => { - buyerId = await createUser() + const error = await rejection(run({ licenseId: license.id, claimToken: null })) + + expect(error).toMatchObject({ code: 'forbidden' }) + expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() }) - function registerToAccount(input: { licenseId: string; claimToken: string | null }) { - return registerAwsMarketplaceLicenseToAccount.execute({ - principal: createSessionPrincipal({ userId: buyerId }), - input: { ...CONTACT, ...input }, - }) - } + it('an expired claim', async () => { + const license = await createClaimedLicense({ claimExpiresAt: new Date(Date.now() - 1000) }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - it('registers to a buyer with no organization, consumes the claim, audits, and emails once', async () => { + const error = await rejection(run({ licenseId: license.id, claimToken: license.token })) + + expect(error).toMatchObject({ code: 'forbidden', message: expect.stringMatching(/expired/) }) + expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() + }) + + it('a lapsed license, clearing its claim and telling only the team', async () => { const license = await createClaimedLicense() - entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + entitlementsByLicense.set(license.licenseArn, [lapsedEntitlement()]) - await registerToAccount({ licenseId: license.id, claimToken: license.token }) + const error = await rejection(run({ licenseId: license.id, claimToken: license.token })) - const row = await readLicense(license.licenseArn) - expect(row).toMatchObject({ - organizationId: null, - linkedByUserId: buyerId, + expect(error).toMatchObject({ code: 'conflict' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + linkedByUserId: null, + status: 'inactive', claimTokenHash: null, - claimExpiresAt: null, }) - expect(row?.linkedAt).toBeInstanceOf(Date) - expect(sentEmails(TEAM)).toEqual([ - expect.objectContaining({ text: expect.stringMatching(/no organization yet/) }), - ]) - expect(sentEmails(BUYER)).toEqual([ - expect.objectContaining({ text: expect.stringMatching(/within 2 business days/) }), + expect(await emailsTo(BUYER)).toEqual([]) + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), ]) - await vi.waitFor(async () => { - const entries = await db - .select({ action: auditLog.action }) - .from(auditLog) - .where(and(eq(auditLog.resourceId, license.id), eq(auditLog.actorId, buyerId))) - expect(entries).toEqual([{ action: 'aws_marketplace_license.registered' }]) - }) - - const replay = await rejection( - registerToAccount({ licenseId: license.id, claimToken: license.token }) - ) - expect(replay).toMatchObject({ code: 'forbidden' }) - expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) }) - it('refuses a browser that holds no claim', async () => { - const license = await createClaimedLicense() + it('a claim whose read predates a newer inactive verification', async () => { + const license = await createClaimedLicense({ + status: 'inactive', + lastVerifiedAt: new Date(Date.now() + 5 * 60 * 1000), + }) + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - const error = await rejection(registerToAccount({ licenseId: license.id, claimToken: null })) + const error = await rejection(run({ licenseId: license.id, claimToken: license.token })) - expect(error).toMatchObject({ code: 'forbidden' }) - expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() + expect(error).toMatchObject({ code: 'conflict' }) + expect(await readLicense(license.licenseArn)).toMatchObject({ + linkedByUserId: null, + status: 'inactive', + claimTokenHash: hashClaimToken(license.token), + }) + expect(await emailsTo(/./)).toEqual([]) }) +}) - it('refuses an expired claim', async () => { - const license = await createClaimedLicense({ claimExpiresAt: new Date(Date.now() - 1000) }) - entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - - const error = await rejection( - registerToAccount({ licenseId: license.id, claimToken: license.token }) - ) +describe('registering a claimed license to an account', () => { + let buyerId: string - expect(error).toMatchObject({ code: 'forbidden', message: expect.stringMatching(/expired/) }) - expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() + beforeAll(async () => { + buyerId = await createUser() }) - it('refuses when AWS no longer reports a current entitlement', async () => { + function registerToAccount(input: { licenseId: string; claimToken: string | null }) { + return registerAwsMarketplaceLicenseToAccount.execute({ + principal: createSessionPrincipal({ userId: buyerId }), + input: { ...CONTACT, ...input }, + }) + } + + it('registers to a buyer with no organization, consumes the claim, audits, and tells both', async () => { const license = await createClaimedLicense() - entitlementsByLicense.set(license.licenseArn, [lapsedEntitlement()]) + entitlementsByLicense.set(license.licenseArn, [ + currentEntitlement({ ExpirationDate: undefined }), + ]) - const error = await rejection( - registerToAccount({ licenseId: license.id, claimToken: license.token }) - ) + await registerToAccount({ licenseId: license.id, claimToken: license.token }) - expect(error).toMatchObject({ code: 'conflict' }) expect(await readLicense(license.licenseArn)).toMatchObject({ - linkedByUserId: null, - status: 'inactive', - claimTokenHash: hashClaimToken(license.token), + organizationId: null, + linkedByUserId: buyerId, + claimTokenHash: null, + claimExpiresAt: null, }) - expect(sentEmails(BUYER)).toEqual([]) - expect(sentEmails(TEAM)).toEqual([ - expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/no organization yet/) }), ]) + expect(await emailsTo(BUYER)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/within 2 business days/) }), + ]) + await vi.waitFor(async () => { + const entries = await db + .select({ action: auditLog.action }) + .from(auditLog) + .where(and(eq(auditLog.resourceId, license.id), eq(auditLog.actorId, buyerId))) + expect(entries).toEqual([{ action: 'aws_marketplace_license.registered' }]) + }) }) it('refuses a license already linked to an organization', async () => { @@ -737,22 +927,6 @@ describe('registering a claimed license to an account', () => { }) }) - it('refuses a claim whose verification predates a newer inactive read', async () => { - const license = await createClaimedLicense({ - status: 'inactive', - lastVerifiedAt: new Date(Date.now() + 60_000), - }) - entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) - - const error = await rejection( - registerToAccount({ licenseId: license.id, claimToken: license.token }) - ) - - expect(error).toMatchObject({ code: 'conflict' }) - expect((await readLicense(license.licenseArn))?.linkedByUserId).toBeNull() - expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() - }) - it('lets a license registered to an account be linked to an organization later', async () => { const license = await createClaimedLicense() entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) @@ -760,9 +934,7 @@ describe('registering a claimed license to an account', () => { resolvesTo(license.licenseArn) const response = await registration('x-amzn-marketplace-token=second-visit') - const token = - /sim_aws_marketplace_claim=([^;]+)/.exec(response.headers.get('set-cookie') ?? '')?.[1] ?? - null + const token = claimTokenOf(response) expect(token).not.toBeNull() const organizationId = await createOrganization([{ userId: buyerId, role: 'owner' }]) @@ -776,94 +948,75 @@ describe('registering a claimed license to an account', () => { linkedByUserId: buyerId, claimTokenHash: null, }) - expect(sentEmails(TEAM)).toHaveLength(2) + }) +}) + +describe('notice delivery', () => { + it('retries a failed send through the outbox without losing the notice', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + await registerAwsMarketplaceLicenseToAccount.execute({ + principal: createSessionPrincipal({ userId: await createUser() }), + input: { ...CONTACT, licenseId: license.id, claimToken: license.token }, + }) + emailMailerMockFns.mockSendEmail.mockResolvedValueOnce({ success: false, message: 'down' }) + const [teamNotice] = await pendingNoticeIds() + + expect(await processOutboxEventById(teamNotice, awsMarketplaceOutboxHandlers)).toBe('pending') + await db + .update(outboxEvent) + .set({ availableAt: new Date(Date.now() - 1000) }) + .where(eq(outboxEvent.id, teamNotice)) + expect(await processOutboxEventById(teamNotice, awsMarketplaceOutboxHandlers)).toBe('completed') + + expect(emailMailerMockFns.mockSendEmail).toHaveBeenCalledTimes(2) }) }) describe('reconcileAwsMarketplaceLicenses', () => { it('discards poison events, keeps a throttled one for redelivery, and marks lapsed licenses inactive', async () => { - const eventArn = uniqueArn() + const event = await insertLicense({ lastVerifiedAt: new Date(Date.now() - 60_000) }) const throttledArn = uniqueArn() - const staleArn = uniqueArn() - await db.insert(awsMarketplaceLicense).values([ - { - id: generateId(), - licenseArn: eventArn, - productCode: PRODUCT, - customerAwsAccountId: ACCOUNT, - status: 'active', - lastVerifiedAt: new Date(Date.now() - 60_000), - }, - { - id: generateId(), - licenseArn: staleArn, - productCode: PRODUCT, - customerAwsAccountId: ACCOUNT, - status: 'active', - dimension: 'enterprise', - lastVerifiedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), - }, - ]) - entitlementsByLicense.set(eventArn, [lapsedEntitlement()]) + const stale = await insertLicense({ lastVerifiedAt: new Date(Date.now() - 2 * DAY_MS) }) + entitlementsByLicense.set(event.licenseArn, [lapsedEntitlement()]) entitlementsByLicense.set(throttledArn, throttled()) - entitlementsByLicense.set(staleArn, []) - - const licenseEvent = (arn: string, productCode = PRODUCT) => - JSON.stringify({ - source: 'aws.agreement-marketplace', - 'detail-type': 'License Deprovisioned - Manufacturer', - detail: { license: { arn }, product: { code: productCode } }, - }) - const messages = [ - { MessageId: 'malformed', ReceiptHandle: 'r-malformed', Body: '{not json' }, - { MessageId: 'foreign', ReceiptHandle: 'r-foreign', Body: licenseEvent(eventArn, 'other') }, - { MessageId: 'throttled', ReceiptHandle: 'r-throttled', Body: licenseEvent(throttledArn) }, - { MessageId: 'lapsed', ReceiptHandle: 'r-lapsed', Body: licenseEvent(eventArn) }, - ] - let delivered = false - queueSend.mockImplementation(async (command: { kind: string }) => { - if (command.kind === 'receive') { - if (delivered) return { Messages: [] } - delivered = true - return { Messages: messages } - } - return {} + entitlementsByLicense.set(stale.licenseArn, []) + const deprovisioned = (arn: string, productCode = PRODUCT) => ({ + 'detail-type': 'License Deprovisioned - Manufacturer', + detail: { license: { arn }, product: { code: productCode } }, }) + const deleted = deliverOnce([ + { source: 'not-marketplace' }, + deprovisioned(event.licenseArn, 'other'), + deprovisioned(throttledArn), + deprovisioned(event.licenseArn), + ]) const summary = await reconcileAwsMarketplaceLicenses() - const deleted = queueSend.mock.calls - .map(([command]) => command) - .filter((command) => command.kind === 'delete') - .map((command) => command.input.ReceiptHandle) - expect(deleted.sort()).toEqual(['r-foreign', 'r-lapsed', 'r-malformed']) + expect(deleted.sort()).toEqual(['r-0', 'r-1', 'r-3']) expect(summary).toMatchObject({ received: 4, applied: 1, discarded: 2, retained: 1 }) - expect((await readLicense(eventArn))?.status).toBe('inactive') + expect((await readLicense(event.licenseArn))?.status).toBe('inactive') expect(await readLicense(throttledArn)).toBeUndefined() - const stale = await readLicense(staleArn) - expect(stale).toMatchObject({ status: 'inactive', dimension: 'enterprise' }) - expect(stale?.lastVerifiedAt.getTime()).toBeGreaterThan(Date.now() - 60_000) - expect(sentEmails(TEAM).map((email) => email.subject)).toEqual([ + const staleRow = await readLicense(stale.licenseArn) + expect(staleRow).toMatchObject({ status: 'inactive', dimension: 'enterprise' }) + expect(staleRow?.lastVerifiedAt.getTime()).toBeGreaterThan(Date.now() - 60_000) + const notices = await emailsTo(TEAM) + expect(notices.map((email) => email.subject)).toEqual([ expect.stringMatching(/active → inactive/), expect.stringMatching(/active → inactive/), ]) + expect(notices[0].text).toMatch(/DELETE \/api\/v1\/admin\/subscriptions/) }) it('sends the team nothing when a re-verify finds the same terms', async () => { - const licenseArn = uniqueArn() - const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) - await db.insert(awsMarketplaceLicense).values({ - id: generateId(), - licenseArn, - productCode: PRODUCT, - customerAwsAccountId: ACCOUNT, - status: 'active', - quantity: 25, + const expiresAt = new Date(Date.now() + 30 * DAY_MS) + const { licenseArn } = await insertLicense({ expiresAt, - lastVerifiedAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000), + lastVerifiedAt: new Date(Date.now() - 2 * DAY_MS), }) entitlementsByLicense.set(licenseArn, [currentEntitlement({ ExpirationDate: expiresAt })]) - queueSend.mockResolvedValue({ Messages: [] }) + deliverOnce([]) const summary = await reconcileAwsMarketplaceLicenses() @@ -871,57 +1024,108 @@ describe('reconcileAwsMarketplaceLicenses', () => { expect((await readLicense(licenseArn))?.lastVerifiedAt.getTime()).toBeGreaterThan( Date.now() - 60_000 ) - expect(sentEmails(TEAM)).toEqual([]) + expect(await emailsTo(TEAM)).toEqual([]) + }) + + it('tells the team when only the dimension changed', async () => { + const expiresAt = new Date(Date.now() + 30 * DAY_MS) + const { licenseArn } = await insertLicense({ + expiresAt, + lastVerifiedAt: new Date(Date.now() - 2 * DAY_MS), + }) + entitlementsByLicense.set(licenseArn, [ + currentEntitlement({ ExpirationDate: expiresAt, Dimension: 'premier' }), + ]) + deliverOnce([]) + + await reconcileAwsMarketplaceLicenses() + + expect(await emailsTo(TEAM)).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/After: active, dimension premier/) }), + ]) + }) + + it('stamps a License Updated event, which the provisioning notice then reports', async () => { + const { id, licenseArn } = await insertLicense() + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + deliverOnce([licenseUpdated(licenseArn)]) + + await reconcileAwsMarketplaceLicenses() + + const row = await readLicense(licenseArn) + expect(row?.licenseEventReceivedAt).toBeInstanceOf(Date) + expect(row?.id).toBe(id) + }) + + it('keeps a Deprovisioned event for redelivery while AWS still reports the license current', async () => { + const { licenseArn } = await insertLicense() + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + const deleted = deliverOnce([ + { + 'detail-type': 'License Deprovisioned - Manufacturer', + detail: { license: { arn: licenseArn }, product: { code: PRODUCT } }, + }, + ]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ retained: 1 }) + expect(deleted).toEqual([]) + }) + + it('leaves a license event with no ARN for the dead-letter queue', async () => { + const deleted = deliverOnce([ + { 'detail-type': 'License Updated - Manufacturer', detail: { product: { code: PRODUCT } } }, + ]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ retained: 1 }) + expect(deleted).toEqual([]) + }) + + it('skips an unknown license AWS describes nothing for, and warns rather than applying', async () => { + const licenseArn = uniqueArn() + entitlementsByLicense.set(licenseArn, []) + const deleted = deliverOnce([licenseUpdated(licenseArn)]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ skipped: 1, applied: 0 }) + expect(deleted).toEqual(['r-0']) + expect(await readLicense(licenseArn)).toBeUndefined() }) it('records a license an event names that nobody registered and tells the team who may own it', async () => { const account = uniqueAccount() const ownerId = await createUser() const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) - await createClaimedLicense({ - organizationId, - customerAwsAccountId: account, - claimTokenHash: null, - }) - const renewedArn = uniqueArn() - entitlementsByLicense.set(renewedArn, [ - currentEntitlement({ CustomerAWSAccountId: account, Value: { IntegerValue: 60 } }), + await insertLicense({ organizationId, customerAwsAccountId: account }) + const newArn = uniqueArn() + entitlementsByLicense.set(newArn, [ + currentEntitlement({ CustomerAWSAccountId: undefined, Value: { IntegerValue: 60 } }), + ]) + deliverOnce([ + { + ...licenseUpdated(newArn), + detail: { + license: { arn: newArn }, + product: { code: PRODUCT }, + acceptor: { accountId: account }, + }, + }, ]) - let delivered = false - queueSend.mockImplementation(async (command: { kind: string }) => { - if (command.kind !== 'receive') return {} - if (delivered) return { Messages: [] } - delivered = true - return { - Messages: [ - { - MessageId: 'renewal', - ReceiptHandle: 'r-renewal', - Body: JSON.stringify({ - source: 'aws.agreement-marketplace', - 'detail-type': 'License Updated - Manufacturer', - detail: { license: { arn: renewedArn }, product: { code: PRODUCT } }, - }), - }, - ], - } - }) await reconcileAwsMarketplaceLicenses() - expect(await readLicense(renewedArn)).toMatchObject({ + expect(await readLicense(newArn)).toMatchObject({ customerAwsAccountId: account, organizationId: null, status: 'active', quantity: 60, }) - expect(sentEmails(TEAM)).toEqual([ + expect(await emailsTo(TEAM)).toEqual([ expect.objectContaining({ subject: expect.stringMatching(/New license/), text: expect.stringContaining(`Marketplace organization (${organizationId})`), }), ]) }) + it('stops receiving batches once the run budget cannot fit another', async () => { vi.useFakeTimers({ toFake: ['Date'] }) try { @@ -947,7 +1151,7 @@ describe('reconcileAwsMarketplaceLicenses', () => { it('keeps licenses AWS keeps refusing from starving the rest of the re-verify', async () => { const failing = Array.from({ length: 50 }, () => uniqueArn()) const healthyArn = uniqueArn() - const longAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000) + const longAgo = (days: number) => new Date(Date.now() - days * DAY_MS) await db.insert(awsMarketplaceLicense).values([ ...failing.map((licenseArn) => ({ id: generateId(), @@ -956,6 +1160,7 @@ describe('reconcileAwsMarketplaceLicenses', () => { customerAwsAccountId: ACCOUNT, status: 'active' as const, lastVerifiedAt: longAgo(3), + lastVerificationAttemptAt: longAgo(3), })), { id: generateId(), @@ -964,6 +1169,7 @@ describe('reconcileAwsMarketplaceLicenses', () => { customerAwsAccountId: ACCOUNT, status: 'active' as const, lastVerifiedAt: longAgo(2), + lastVerificationAttemptAt: longAgo(2), }, ]) const rejected = Object.assign(new Error('Invalid license'), { @@ -983,35 +1189,13 @@ describe('reconcileAwsMarketplaceLicenses', () => { Date.now() - 60_000 ) }) +}) - /** One batch of `events`, then an empty queue; returns the receipt handles deleted. */ - function deliverOnce(events: Array>) { - const deleted: string[] = [] - let delivered = false - queueSend.mockImplementation( - async (command: { kind: string; input: { ReceiptHandle: string } }) => { - if (command.kind === 'delete') { - deleted.push(command.input.ReceiptHandle) - return {} - } - if (delivered) return { Messages: [] } - delivered = true - return { - Messages: events.map((event, index) => ({ - MessageId: `m-${index}`, - ReceiptHandle: `r-${index}`, - Body: JSON.stringify({ source: 'aws.agreement-marketplace', ...event }), - })), - } - } - ) - return deleted - } - +describe('agreement events', () => { it('re-verifies the license of an ended agreement, from an event that names no product', async () => { - const agreementId = `agmt-${generateId()}` - const license = await createClaimedLicense({ agreementId, claimTokenHash: null }) - entitlementsByLicense.set(license.licenseArn, []) + const agreementId = uniqueAgreement() + const { licenseArn } = await insertLicense({ agreementId }) + entitlementsByLicense.set(licenseArn, []) const deleted = deliverOnce([ { 'detail-type': 'Purchase Agreement Ended - Proposer', @@ -1019,112 +1203,270 @@ describe('reconcileAwsMarketplaceLicenses', () => { }, ]) - const summary = await reconcileAwsMarketplaceLicenses() - - expect(summary).toMatchObject({ applied: 1, discarded: 0 }) + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ applied: 1, discarded: 0 }) expect(deleted).toEqual(['r-0']) - expect((await readLicense(license.licenseArn))?.status).toBe('inactive') - expect(sentEmails(TEAM)).toEqual([ + expect((await readLicense(licenseArn))?.status).toBe('inactive') + expect(await emailsTo(TEAM)).toEqual([ expect.objectContaining({ subject: expect.stringMatching(/active → inactive/) }), ]) }) - it('discards an ended agreement it holds no license for without asking AWS', async () => { + it('reads every license of an ended agreement even when one fails, and keeps the message', async () => { + const agreementId = uniqueAgreement() + const failing = await insertLicense({ agreementId, lastVerificationAttemptAt: new Date(0) }) + const healthy = await insertLicense({ agreementId }) + entitlementsByLicense.set(failing.licenseArn, throttled()) + entitlementsByLicense.set(healthy.licenseArn, []) const deleted = deliverOnce([ { 'detail-type': 'Purchase Agreement Ended - Manufacturer', - detail: { agreement: { id: `agmt-${generateId()}` }, product: { code: 'other-product' } }, + detail: { agreement: { id: agreementId } }, }, ]) - const summary = await reconcileAwsMarketplaceLicenses() + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ retained: 1 }) + expect(deleted).toEqual([]) + expect((await readLicense(failing.licenseArn))?.status).toBe('active') + expect((await readLicense(healthy.licenseArn))?.status).toBe('inactive') + }) + + it('discards an ended agreement it holds no license for without asking AWS', async () => { + const deleted = deliverOnce([ + { + 'detail-type': 'Purchase Agreement Ended - Manufacturer', + detail: { agreement: { id: uniqueAgreement() }, product: { code: 'other-product' } }, + }, + ]) - expect(summary).toMatchObject({ applied: 0, discarded: 1 }) + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ applied: 0, discarded: 1 }) expect(deleted).toEqual(['r-0']) expect(entitlementSend).not.toHaveBeenCalled() - expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() + expect(await emailsTo(/./)).toEqual([]) }) - it('tells the team once of a renewal delivered to both seller roles, linking nothing', async () => { - const ownerId = await createUser() - const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) - const previousAgreementId = `agmt-${generateId()}` - const renewedAgreementId = `agmt-${generateId()}` - const license = await createClaimedLicense({ - organizationId, - agreementId: previousAgreementId, - claimTokenHash: null, - }) - const renewal = { - detail: { - agreement: { id: renewedAgreementId, intent: 'RENEW', previousAgreementId }, - }, - } + it('discards a created agreement that replaces nothing it holds', async () => { + const stored = uniqueAgreement() + const { licenseArn } = await insertLicense({ agreementId: stored }) const deleted = deliverOnce([ - { 'detail-type': 'Purchase Agreement Created - Proposer', ...renewal }, - { 'detail-type': 'Purchase Agreement Created - Manufacturer', ...renewal }, + agreementCreated(uniqueAgreement()), + agreementCreated(uniqueAgreement(), uniqueAgreement()), ]) - await reconcileAwsMarketplaceLicenses() + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ applied: 0, discarded: 2 }) + expect(deleted.sort()).toEqual(['r-0', 'r-1']) + expect(await emailsTo(/./)).toEqual([]) + expect((await readLicense(licenseArn))?.renewedAgreementId).toBeNull() + }) + it('tells the team of an advisory on an agreement it holds, and ignores others', async () => { + const agreementId = uniqueAgreement() + await insertLicense({ agreementId }) + const deleted = deliverOnce([ + { + 'detail-type': 'Purchase Agreement Advisory Issued - Manufacturer', + detail: { agreement: { id: agreementId }, advisory: 'payment' }, + }, + { + 'detail-type': 'Purchase Agreement Advisory Resolved - Manufacturer', + detail: { agreement: { id: uniqueAgreement() } }, + }, + ]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ applied: 1, discarded: 1 }) expect(deleted.sort()).toEqual(['r-0', 'r-1']) - expect(sentEmails(TEAM)).toEqual([ + expect(await emailsTo(TEAM)).toEqual([ expect.objectContaining({ - subject: expect.stringMatching(/Agreement renewed/), - text: expect.stringContaining(`Marketplace organization (${organizationId})`), + subject: expect.stringMatching(/advisory issued/), + text: expect.stringMatching(/payment/), }), ]) - expect(await readLicense(license.licenseArn)).toMatchObject({ + }) +}) + +describe('a replaced agreement', () => { + async function linkedPredecessor() { + const account = uniqueAccount() + const ownerId = await createUser() + const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) + const previousAgreementId = uniqueAgreement() + const old = await insertLicense({ organizationId, + linkedByUserId: ownerId, + contactEmail: CONTACT.contactEmail, + companyName: CONTACT.companyName, + customerAwsAccountId: account, agreementId: previousAgreementId, - renewedAgreementId, }) + return { ...old, account, ownerId, organizationId, previousAgreementId } + } - emailMailerMockFns.mockSendEmail.mockClear() - entitlementsByLicense.set(license.licenseArn, []) + it('hands the owner on when the Created event arrives first, telling the team once', async () => { + const old = await linkedPredecessor() + const agreementId = uniqueAgreement() + const newArn = uniqueArn() + entitlementsByLicense.set(newArn, [currentEntitlement({ CustomerAWSAccountId: old.account })]) deliverOnce([ - { - 'detail-type': 'Purchase Agreement Ended - Manufacturer', - detail: { agreement: { id: renewedAgreementId } }, - }, + agreementCreated(agreementId, old.previousAgreementId), + agreementCreated(agreementId, old.previousAgreementId), + licenseUpdated(newArn, agreementId), ]) + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ retained: 0 }) + + expect(await readLicense(old.licenseArn)).toMatchObject({ renewedAgreementId: agreementId }) + expect(await readLicense(newArn)).toMatchObject({ + organizationId: old.organizationId, + linkedByUserId: old.ownerId, + contactEmail: CONTACT.contactEmail, + agreementId, + }) + const notices = await emailsTo(TEAM) + expect(notices.filter((email) => /Agreement replaced/.test(email.subject))).toHaveLength(1) + }) + + it('hands the owner on when the license event arrives first', async () => { + const old = await linkedPredecessor() + const agreementId = uniqueAgreement() + const newArn = uniqueArn() + entitlementsByLicense.set(newArn, [currentEntitlement({ CustomerAWSAccountId: old.account })]) + deliverOnce([licenseUpdated(newArn, agreementId)]) await reconcileAwsMarketplaceLicenses() + expect((await readLicense(newArn))?.organizationId).toBeNull() - expect((await readLicense(license.licenseArn))?.status).toBe('inactive') + deliverOnce([agreementCreated(agreementId, old.previousAgreementId)]) + await reconcileAwsMarketplaceLicenses() + + expect(await readLicense(newArn)).toMatchObject({ + organizationId: old.organizationId, + linkedByUserId: old.ownerId, + }) + const notices = await emailsTo(TEAM) + expect(notices.filter((email) => /Agreement replaced/.test(email.subject))).toHaveLength(1) }) - it('discards a created agreement that renews nothing it holds', async () => { - const stored = `agmt-${generateId()}` - const license = await createClaimedLicense({ agreementId: stored, claimTokenHash: null }) - const deleted = deliverOnce([ - { - 'detail-type': 'Purchase Agreement Created - Proposer', - detail: { agreement: { id: `agmt-${generateId()}`, intent: 'NEW' } }, - }, - { - 'detail-type': 'Purchase Agreement Created - Manufacturer', - detail: { - agreement: { id: `agmt-${generateId()}`, intent: 'NEW', previousAgreementId: stored }, - }, - }, + it('still tells the team when a license event already moved the row to the new agreement', async () => { + const old = await linkedPredecessor() + const agreementId = uniqueAgreement() + entitlementsByLicense.set(old.licenseArn, [ + currentEntitlement({ CustomerAWSAccountId: old.account }), + ]) + deliverOnce([ + licenseUpdated(old.licenseArn, agreementId, new Date()), + agreementCreated(agreementId, old.previousAgreementId), + ]) + + await reconcileAwsMarketplaceLicenses() + + expect((await readLicense(old.licenseArn))?.agreementId).toBe(agreementId) + const notices = await emailsTo(TEAM) + expect(notices.filter((email) => /Agreement replaced/.test(email.subject))).toHaveLength(1) + }) + + it('does not let a late A→B overwrite B→C', async () => { + const old = await linkedPredecessor() + const middle = uniqueAgreement() + const latest = uniqueAgreement() + deliverOnce([ + agreementCreated(middle, old.previousAgreementId), + agreementCreated(latest, middle), + agreementCreated(middle, old.previousAgreementId), + ]) + + await reconcileAwsMarketplaceLicenses() + + expect((await readLicense(old.licenseArn))?.renewedAgreementId).toBe(latest) + }) + + it('does not let a redelivered old license event roll the agreement back', async () => { + const newer = uniqueAgreement() + const older = uniqueAgreement() + const { licenseArn } = await insertLicense() + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + deliverOnce([ + licenseUpdated(licenseArn, newer, new Date(Date.now() - 1000)), + licenseUpdated(licenseArn, older, new Date(Date.now() - 60_000)), + ]) + + await reconcileAwsMarketplaceLicenses() + + expect((await readLicense(licenseArn))?.agreementId).toBe(newer) + }) + + it('tells the team not to cancel when a superseded license ends', async () => { + const old = await linkedPredecessor() + await db + .update(awsMarketplaceLicense) + .set({ + renewedAgreementId: uniqueAgreement(), + lastVerificationAttemptAt: new Date(Date.now() - 2 * DAY_MS), + }) + .where(eq(awsMarketplaceLicense.id, old.id)) + entitlementsByLicense.set(old.licenseArn, []) + deliverOnce([]) + + await reconcileAwsMarketplaceLicenses() + + const [notice] = await emailsTo(TEAM) + expect(notice.text).toMatch(/Do not cancel/) + expect(notice.text).not.toMatch(/DELETE \/api\/v1\/admin\/subscriptions/) + }) +}) + +describe('the admin license route', () => { + function adminRequest( + body?: Record, + key: string | null = 'admin-key-placeholder' + ) { + return new NextRequest( + 'http://localhost:3000/api/v1/admin/dashboard/aws-marketplace-licenses?limit=250&offset=0', { - 'detail-type': 'Purchase Agreement Created - Manufacturer', - detail: { - agreement: { - id: `agmt-${generateId()}`, - intent: 'RENEW', - previousAgreementId: `agmt-${generateId()}`, - }, + method: body ? 'POST' : 'GET', + headers: { + 'content-type': 'application/json', + ...(key && { 'x-admin-key': key }), }, - }, + ...(body && { body: JSON.stringify(body) }), + } + ) + } + + it('refuses a caller without the admin key', async () => { + const { id, licenseArn } = await insertLicense() + const organizationId = await createOrganization([{ userId: await createUser(), role: 'owner' }]) + + const response = await adminLink(adminRequest({ licenseId: id, organizationId }, null), {}) + + expect(response.status).toBe(401) + expect((await readLicense(licenseArn))?.organizationId).toBeNull() + }) + + it('lists a license while unlinked, links it, and refuses to move it', async () => { + const { id, licenseArn } = await insertLicense() + const organizationId = await createOrganization([{ userId: await createUser(), role: 'owner' }]) + const otherOrganizationId = await createOrganization([ + { userId: await createUser(), role: 'owner' }, ]) + const listed = await (await adminList(adminRequest(), {})).json() + expect(listed).toMatchObject({ + data: expect.arrayContaining([expect.objectContaining({ id })]), + }) - const summary = await reconcileAwsMarketplaceLicenses() + const linked = await adminLink(adminRequest({ licenseId: id, organizationId }), {}) + const moved = await adminLink( + adminRequest({ licenseId: id, organizationId: otherOrganizationId }), + {} + ) - expect(summary).toMatchObject({ applied: 0, discarded: 3 }) - expect(deleted.sort()).toEqual(['r-0', 'r-1', 'r-2']) - expect(emailMailerMockFns.mockSendEmail).not.toHaveBeenCalled() - expect((await readLicense(license.licenseArn))?.renewedAgreementId).toBeNull() + expect(linked.status).toBe(200) + expect(moved.status).toBe(409) + expect((await readLicense(licenseArn))?.organizationId).toBe(organizationId) + await vi.waitFor(async () => { + const entries = await db + .select({ action: auditLog.action }) + .from(auditLog) + .where(eq(auditLog.resourceId, id)) + expect(entries).toEqual([{ action: 'aws_marketplace_license.linked' }]) + }) + await db.delete(auditLog).where(eq(auditLog.resourceId, id)) }) }) diff --git a/apps/sim/lib/billing/aws-marketplace/notice-events.ts b/apps/sim/lib/billing/aws-marketplace/notice-events.ts new file mode 100644 index 00000000000..b0589e548c1 --- /dev/null +++ b/apps/sim/lib/billing/aws-marketplace/notice-events.ts @@ -0,0 +1,33 @@ +export const AWS_MARKETPLACE_TEAM_NOTICE_EVENT = 'aws-marketplace.team-notice' +export const AWS_MARKETPLACE_BUYER_NOTICE_EVENT = 'aws-marketplace.buyer-notice' + +export interface AwsMarketplaceNoticeTerms { + status: 'pending' | 'active' | 'inactive' + dimension: string | null + quantity: number | null + expiresAt: string | null +} + +/** What the team is told; a retry renders it again from the license's current row. */ +export type AwsMarketplaceTeamNotice = + | { kind: 'new-license'; licenseId: string; source: 'registration' | 'event' } + | { kind: 'taken'; licenseId: string; userId: string } + | { kind: 'contact'; licenseId: string } + | { + kind: 'terms-changed' + licenseId: string + before: AwsMarketplaceNoticeTerms + after: AwsMarketplaceNoticeTerms + } + | { kind: 'renewed'; licenseIds: string[]; previousAgreementId: string; agreementId: string } + | { + kind: 'advisory' + licenseIds: string[] + agreementId: string + state: 'issued' | 'resolved' + detail: string + } + +export interface AwsMarketplaceBuyerNotice { + licenseId: string +} diff --git a/apps/sim/lib/billing/aws-marketplace/notifications.ts b/apps/sim/lib/billing/aws-marketplace/notifications.ts index b64c8b64461..b57bc8bf23e 100644 --- a/apps/sim/lib/billing/aws-marketplace/notifications.ts +++ b/apps/sim/lib/billing/aws-marketplace/notifications.ts @@ -1,14 +1,20 @@ import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' +import { + AWS_MARKETPLACE_BUYER_NOTICE_EVENT, + AWS_MARKETPLACE_TEAM_NOTICE_EVENT, + type AwsMarketplaceBuyerNotice, + type AwsMarketplaceNoticeTerms, + type AwsMarketplaceTeamNotice, +} from '@/lib/billing/aws-marketplace/notice-events' import { AWS_MARKETPLACE_SETUP_TURNAROUND } from '@/lib/billing/aws-marketplace/onboarding' import { type AwsMarketplaceLicenseRecord, + getLicensesByIds, getLinkParties, - type LicenseTerms, - type LicenseVerificationOutcome, listOrganizationsLinkedToAccount, } from '@/lib/billing/aws-marketplace/repository' import { env } from '@/lib/core/config/env' +import type { OutboxHandler, OutboxHandlerRegistry } from '@/lib/core/outbox/service' import { getEmailDomain } from '@/lib/core/utils/urls' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress } from '@/lib/messaging/email/utils' @@ -16,249 +22,220 @@ import { getBrandConfig } from '@/ee/whitelabeling' const logger = createLogger('AwsMarketplaceNotifications') +/** + * Enterprise provisioning otherwise sends Stripe invoices, and AWS requires a listed product to be + * billed entirely through its dimensions, so paused collection is the one safe way to provision. + */ +const PROVISION = + 'Provision through the admin API (POST /api/v1/admin/dashboard/enterprise-provisioning, or enterprise-owner-claims for a new owner) with pausePaymentCollection: true, sized from the dimension, quantity, and expiry above. Never send a Stripe invoice for a Marketplace customer: AWS bills them.' +const LINK = + 'Then link the license with POST /api/v1/admin/dashboard/aws-marketplace-licenses (licenseId, organizationId).' +const CANCEL = + 'Cancel the Enterprise subscription with POST /api/v1/admin/dashboard/organizations//cancellation (timing: immediate), unless a replacement notice for this account follows. The organization goes dormant and is not deleted.' +const UNCONFIRMED = + 'Unconfirmed: AWS has returned no entitlement for this license yet. Do not provision it until its status is active.' + +interface RenderedEmail { + to: string + subject: string + text: string + replyTo?: string | null +} + function teamInbox(): string { return `enterprise@${env.EMAIL_DOMAIN || getEmailDomain()}` } -function formatTerms(terms: LicenseTerms): string { - return `${terms.status}, quantity ${terms.quantity ?? 'not provided'}, expires ${ - terms.expiresAt?.toISOString() ?? 'never' - }` +function formatTerms(terms: AwsMarketplaceNoticeTerms): string { + return `${terms.status}, dimension ${terms.dimension ?? 'not provided'}, quantity ${ + terms.quantity ?? 'not provided' + }, expires ${terms.expiresAt ?? 'never'}` } -function licenseLines(license: AwsMarketplaceLicenseRecord): string { - return `License: ${license.licenseArn} +async function describeLicense(license: AwsMarketplaceLicenseRecord): Promise { + const { organizationName } = await getLinkParties(license.organizationId, null) + return `Organization: ${ + license.organizationId ? `${organizationName ?? 'Unknown'} (${license.organizationId})` : 'None' + } +License ID: ${license.id} +License: ${license.licenseArn} AWS account: ${license.customerAwsAccountId} Agreement: ${license.agreementId ?? 'Not provided'} +Status: ${license.status} Dimension: ${license.dimension ?? 'Not provided'} -Terms: ${formatTerms(license)}` +Quantity: ${license.quantity ?? 'Not provided'} +Expires: ${license.expiresAt?.toISOString() ?? 'Never'} +Contact email: ${license.contactEmail ?? 'Not provided'} +Company: ${license.companyName ?? 'Not provided'} +License Updated event: ${ + license.licenseEventReceivedAt + ? `received ${license.licenseEventReceivedAt.toISOString()}` + : 'not received yet; AWS says not to activate a subscription until it arrives' + }` } -/** Provisioning is manual, so a failed send is logged and never undoes the write it reports. */ -async function send( - notice: string, - licenseId: string, - email: { to: string; subject: string; text: string; replyTo?: string | null } -): Promise { - try { - const result = await sendEmail({ - to: [email.to], - subject: email.subject, - text: email.text, - from: getFromEmailAddress(), - ...(email.replyTo && { replyTo: email.replyTo }), - emailType: 'transactional', - }) - if (result.success) { - logger.info('Sent an AWS Marketplace notice', { notice, licenseId }) - return - } - logger.warn('Could not send an AWS Marketplace notice', { - notice, - licenseId, - message: result.message, - }) - } catch (error) { - logger.warn('Could not send an AWS Marketplace notice', { - notice, - licenseId, - message: getErrorMessage(error, 'Unknown error'), - }) +/** What is left for the team to do about one license AWS has just changed. */ +function nextStepForTerms( + license: AwsMarketplaceLicenseRecord, + after: AwsMarketplaceNoticeTerms +): string { + if (after.status === 'inactive' && license.renewedAgreementId) { + return `Agreement ${license.renewedAgreementId} replaced this license's agreement. Do not cancel on this notice: AWS grants the replacement its own license, so check that one.` } + if (after.status === 'inactive') return CANCEL + if (after.status === 'active' && !license.organizationId) return `${PROVISION} ${LINK}` + return 'Update the Enterprise provisioning to match. Never send a Stripe invoice for a Marketplace customer: AWS bills them.' } -/** - * Every notice runs after its write committed, with the claim already spent, so nothing in it - * (the lookups included) may fail the request; the warning is the fallback trail. - */ -async function bestEffort( - notice: string, - licenseId: string | null, - notify: () => Promise -): Promise { - try { - await notify() - } catch (error) { - logger.warn('Could not prepare an AWS Marketplace notice', { - notice, - licenseId, - message: getErrorMessage(error, 'Unknown error'), - }) +/** Each notice's subject, opening, and next step; the license details between them are shared. */ +async function teamNoticeCopy( + notice: AwsMarketplaceTeamNotice, + license: AwsMarketplaceLicenseRecord +): Promise<{ subject: string; opening: string; next: string; replyTo?: string | null }> { + const account = `AWS account ${license.customerAwsAccountId}` + switch (notice.kind) { + case 'new-license': { + const linked = await listOrganizationsLinkedToAccount({ + customerAwsAccountId: license.customerAwsAccountId, + productCode: license.productCode, + excludingLicenseArn: license.licenseArn, + }) + const others = linked.length + ? linked.map((org) => `${org.name} (${org.id})`).join(', ') + : 'None' + return { + subject: `New license for ${account}`, + opening: `${ + notice.source === 'registration' + ? 'A buyer started AWS Marketplace setup. They may not finish it.' + : 'An AWS Marketplace license appeared that no buyer has registered.' + }${license.status === 'pending' ? `\n${UNCONFIRMED}` : ''} +Organizations linked to other licenses of this account: ${others}`, + next: license.organizationId + ? 'It carries on the organization of the license it replaces. Keep that organization provisioned to the terms above.' + : `${PROVISION} ${LINK}`, + } + } + case 'taken': { + const { userName, userEmail } = await getLinkParties(null, notice.userId) + return { + subject: license.organizationId + ? `Linked to an organization: ${account}` + : `${license.contactEmail ?? userEmail ?? notice.userId} (no organization)`, + opening: `AWS Marketplace license ${ + license.organizationId ? 'linked' : 'registered to an account, no organization yet' + }. Taken by ${userName ?? 'Unknown'} <${userEmail ?? 'unknown'}> (${notice.userId}). The buyer was told to wait for you to contact them.`, + next: license.organizationId + ? PROVISION + : `Create the Enterprise organization for this buyer. ${PROVISION} ${LINK}`, + replyTo: license.contactEmail ?? userEmail, + } + } + case 'contact': + return { + subject: `Buyer contact: ${account}`, + opening: `A buyer left contact details while AWS confirms their subscription.${ + license.status === 'pending' ? `\n${UNCONFIRMED}` : '' + }`, + next: 'Nothing to provision on this notice; a separate one follows when AWS confirms it.', + replyTo: license.contactEmail, + } + case 'terms-changed': + return { + subject: `License ${notice.before.status} → ${notice.after.status}: ${account}`, + opening: `AWS Marketplace changed what this license grants. +Before: ${formatTerms(notice.before)} +After: ${formatTerms(notice.after)}`, + next: nextStepForTerms(license, notice.after), + } + case 'renewed': + return { + subject: `Agreement replaced: ${account}`, + opening: `An AWS Marketplace agreement was replaced. +Previous agreement: ${notice.previousAgreementId} +New agreement: ${notice.agreementId}`, + next: 'AWS grants a new license with every new agreement. It inherits the organization below when it arrives, and a notice follows. Do not cancel because the previous license ends.', + } + case 'advisory': + return { + subject: `Agreement advisory ${notice.state}: ${account}`, + opening: `AWS Marketplace ${notice.state} an advisory on agreement ${notice.agreementId}. +Event detail: ${notice.detail}`, + next: 'Review the agreement in the AWS Marketplace Management Portal.', + } } } -/** - * Tells the team a buyer took a license, for an organization or for their account alone, so - * they provision it the way an Enterprise deal is provisioned. No subscription is changed here. - */ -export function notifyTeamOfTakenLicense( - license: AwsMarketplaceLicenseRecord, - organizationId: string | null, - userId: string -): Promise { - return bestEffort('team notice of a taken AWS Marketplace license', license.id, () => - sendTakenLicenseNotice(license, organizationId, userId) +async function renderTeamNotice(notice: AwsMarketplaceTeamNotice): Promise { + const licenses = await getLicensesByIds( + 'licenseIds' in notice ? notice.licenseIds : [notice.licenseId] ) -} - -async function sendTakenLicenseNotice( - license: AwsMarketplaceLicenseRecord, - organizationId: string | null, - userId: string -): Promise { - const parties = await getLinkParties(organizationId, userId) - const organization = organizationId - ? `${parties.organizationName ?? 'Unknown'} (${organizationId})` - : 'None yet' - const instructions = organizationId - ? 'Provision this organization the way Enterprise deals are provisioned. No subscription was changed.' - : 'Registered to an account, no organization yet. Create the Enterprise organization for this buyer and link it. No subscription was changed.' - await send('team notice of a taken AWS Marketplace license', license.id, { + const license = licenses[0] + if (!license) return null + const copy = await teamNoticeCopy(notice, license) + const described = await Promise.all(licenses.map(describeLicense)) + return { to: teamInbox(), - subject: `[AWS MARKETPLACE] ${ - organizationId - ? (parties.organizationName ?? organizationId) - : `${license.contactEmail ?? parties.userEmail ?? userId} (no organization)` - }`, - replyTo: license.contactEmail ?? parties.userEmail, - text: `AWS Marketplace license ${organizationId ? 'linked' : 'registered to an account'} -Organization: ${organization} -Taken by: ${parties.userName ?? 'Unknown'} <${parties.userEmail ?? 'unknown'}> (${userId}) -Contact email: ${license.contactEmail ?? 'Not provided'} -Company: ${license.companyName ?? 'Not provided'} -${licenseLines(license)} + subject: `[AWS MARKETPLACE] ${copy.subject}`, + replyTo: copy.replyTo, + text: `${copy.opening} -${instructions} -`, - }) -} +${described.join('\n\n')} -/** Confirms the registration to the buyer and says what happens next. */ -export function notifyBuyerOfTakenLicense(license: AwsMarketplaceLicenseRecord): Promise { - return bestEffort('buyer confirmation of an AWS Marketplace license', license.id, () => - sendBuyerConfirmation(license) - ) +${copy.next} +`, + } } -async function sendBuyerConfirmation(license: AwsMarketplaceLicenseRecord): Promise { - if (!license.contactEmail) return +/** AWS's manual-setup guidance, in its own order: being set up, wait, turnaround, contact. */ +async function renderBuyerNotice(notice: AwsMarketplaceBuyerNotice): Promise { + const [license] = await getLicensesByIds([notice.licenseId]) + if (!license?.contactEmail) return null const brand = getBrandConfig() - await send('buyer confirmation of an AWS Marketplace license', license.id, { + return { to: license.contactEmail, - subject: `Your ${brand.name} AWS Marketplace subscription is registered`, + subject: `Your ${brand.name} account is being set up`, replyTo: brand.supportEmail, - text: `Your ${brand.name} subscription from AWS Marketplace is registered. + text: `Thanks for subscribing to ${brand.name} through AWS Marketplace. -Next steps: our team will set up your Enterprise workspace ${AWS_MARKETPLACE_SETUP_TURNAROUND} and email you when it is ready. +Your account is being set up. Please wait for us to contact you. We expect to finish ${AWS_MARKETPLACE_SETUP_TURNAROUND}. Questions? Contact ${brand.supportEmail}. `, - }) + } } /** - * Tells the team when AWS changed what a license grants, or described a license nobody has - * registered, so a lapsed buyer does not keep Enterprise and a renewal is not missed. An - * unchanged re-verify sends nothing. A new license is never linked automatically. + * Sends one rendered notice. A failure throws, so the outbox retries it with backoff; the + * checkpoint keeps a crash between the send and the completion from sending it twice. */ -export function notifyTeamOfVerification(outcome: LicenseVerificationOutcome): Promise { - if (outcome.kind === 'skipped' || (outcome.kind === 'updated' && !outcome.changed)) { - return Promise.resolve() - } - return bestEffort('team notice of an AWS Marketplace license change', outcome.license.id, () => - sendVerificationNotice(outcome) - ) -} - -async function sendVerificationNotice( - outcome: Exclude -): Promise { - const { license } = outcome - if (outcome.kind === 'created') { - const linked = await listOrganizationsLinkedToAccount({ - customerAwsAccountId: license.customerAwsAccountId, - productCode: license.productCode, - excludingLicenseArn: license.licenseArn, - }) - await send('team notice of a new AWS Marketplace license', license.id, { - to: teamInbox(), - subject: `[AWS MARKETPLACE] New license for AWS account ${license.customerAwsAccountId}`, - text: `A new AWS Marketplace license appeared that no buyer has registered. -${licenseLines(license)} -Organizations linked to other licenses of this account: ${ - linked.length ? linked.map((org) => `${org.name} (${org.id})`).join(', ') : 'None' - } - -It is not linked to any organization. Link it if it renews an existing customer. -`, +function noticeHandler( + render: (notice: T) => Promise +): OutboxHandler { + return async (rawPayload, context) => { + const payload = rawPayload as T & { sentAt?: string } + if (payload.sentAt) return + const email = await render(payload) + if (!email) { + logger.warn('Skipping an AWS Marketplace notice with nothing left to say', { + eventId: context.eventId, + }) + return + } + const result = await sendEmail({ + to: [email.to], + subject: email.subject, + text: email.text, + from: getFromEmailAddress(), + ...(email.replyTo && { replyTo: email.replyTo }), + emailType: 'transactional', }) - return + if (!result.success) throw new Error(result.message || 'Failed to send the notice') + await context.checkpointPayload({ sentAt: new Date().toISOString() }) } - const parties = license.organizationId - ? await getLinkParties(license.organizationId, license.linkedByUserId) - : null - await send('team notice of a changed AWS Marketplace license', license.id, { - to: teamInbox(), - subject: `[AWS MARKETPLACE] License ${outcome.before.status} → ${license.status}: ${ - parties?.organizationName ?? `AWS account ${license.customerAwsAccountId}` - }`, - text: `AWS Marketplace changed what this license grants. -Organization: ${ - license.organizationId - ? `${parties?.organizationName ?? 'Unknown'} (${license.organizationId})` - : 'None' - } -${licenseLines(license)} -Before: ${formatTerms(outcome.before)} -After: ${formatTerms(license)} - -Update the organization's Enterprise provisioning to match. -`, - }) } -/** - * Tells the team an agreement renewed, naming the organizations its licenses belong to. The - * renewal may issue a new license ARN (AWS does not say), so nothing is linked automatically. - */ -export function notifyTeamOfAgreementRenewal( - licenses: AwsMarketplaceLicenseRecord[], - renewal: { previousAgreementId: string; agreementId: string } -): Promise { - return bestEffort('team notice of a renewed AWS Marketplace agreement', null, () => - sendRenewalNotice(licenses, renewal) - ) -} - -async function sendRenewalNotice( - licenses: AwsMarketplaceLicenseRecord[], - renewal: { previousAgreementId: string; agreementId: string } -): Promise { - const described = await Promise.all( - licenses.map(async (license) => { - const { organizationName } = await getLinkParties(license.organizationId, null) - const organization = license.organizationId - ? `${organizationName ?? 'Unknown'} (${license.organizationId})` - : 'None' - return { license, organization, organizationName } - }) - ) - const first = described[0] - if (!first) return - await send('team notice of a renewed AWS Marketplace agreement', first.license.id, { - to: teamInbox(), - subject: `[AWS MARKETPLACE] Agreement renewed: ${ - first.organizationName ?? `AWS account ${first.license.customerAwsAccountId}` - }`, - text: `An AWS Marketplace agreement renewed. -Previous agreement: ${renewal.previousAgreementId} -New agreement: ${renewal.agreementId} - -${described - .map(({ license, organization }) => `Organization: ${organization}\n${licenseLines(license)}`) - .join('\n\n')} - -The renewal may issue a new license. If one appears, link it to the same organization. Nothing was linked automatically. -`, - }) -} +export const awsMarketplaceOutboxHandlers = { + [AWS_MARKETPLACE_TEAM_NOTICE_EVENT]: noticeHandler(renderTeamNotice), + [AWS_MARKETPLACE_BUYER_NOTICE_EVENT]: noticeHandler(renderBuyerNotice), +} as const satisfies OutboxHandlerRegistry diff --git a/apps/sim/lib/billing/aws-marketplace/reconcile.ts b/apps/sim/lib/billing/aws-marketplace/reconcile.ts index 0cc5e3271a6..61dd0c147f9 100644 --- a/apps/sim/lib/billing/aws-marketplace/reconcile.ts +++ b/apps/sim/lib/billing/aws-marketplace/reconcile.ts @@ -1,8 +1,14 @@ -import { DeleteMessageCommand, type Message, ReceiveMessageCommand } from '@aws-sdk/client-sqs' +import { + ChangeMessageVisibilityCommand, + DeleteMessageCommand, + type Message, + ReceiveMessageCommand, +} from '@aws-sdk/client-sqs' import { createLogger } from '@sim/logger' import { toStringOrNull } from '@sim/utils/coerce' import { getErrorMessage } from '@sim/utils/errors' import { toRecord } from '@sim/utils/object' +import { truncate } from '@sim/utils/string' import { getEventsQueueClient } from '@/lib/billing/aws-marketplace/client' import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' import { @@ -10,13 +16,10 @@ import { verifyLicense, } from '@/lib/billing/aws-marketplace/entitlements' import { - notifyTeamOfAgreementRenewal, - notifyTeamOfVerification, -} from '@/lib/billing/aws-marketplace/notifications' -import { + applyAgreementRenewal, + enqueueAgreementAdvisory, listLicensesDueForReverify, listLicensesForAgreement, - markLicensesRenewed, markVerificationAttempted, recordLicenseVerification, } from '@/lib/billing/aws-marketplace/repository' @@ -25,10 +28,8 @@ import { env } from '@/lib/core/config/env' const logger = createLogger('AwsMarketplaceReconcile') const EVENT_SOURCE = 'aws.agreement-marketplace' -const LICENSE_EVENT_TYPES = new Set([ - 'License Updated - Manufacturer', - 'License Deprovisioned - Manufacturer', -]) +const LICENSE_UPDATED_EVENT = 'License Updated - Manufacturer' +const LICENSE_DEPROVISIONED_EVENT = 'License Deprovisioned - Manufacturer' const AGREEMENT_ENDED_EVENT_TYPES = new Set([ 'Purchase Agreement Ended - Proposer', 'Purchase Agreement Ended - Manufacturer', @@ -37,20 +38,25 @@ const AGREEMENT_CREATED_EVENT_TYPES = new Set([ 'Purchase Agreement Created - Proposer', 'Purchase Agreement Created - Manufacturer', ]) +const ADVISORY_STATE_BY_EVENT_TYPE = new Map([ + ['Purchase Agreement Advisory Issued - Manufacturer', 'issued'], + ['Purchase Agreement Advisory Resolved - Manufacturer', 'resolved'], +]) +const MAX_ADVISORY_DETAIL_LENGTH = 2_000 /** SQS caps one receive at 10 messages; a run drains at most this many batches. */ const RECEIVE_BATCH_SIZE = 10 const MAX_RECEIVE_BATCHES = 10 const RECEIVE_WAIT_SECONDS = 2 -/** One message's or stale license's entitlement reads, retries included, are aborted past this. */ -const MESSAGE_BUDGET_MS = 15_000 -/** Messages in a batch are handled one after another. */ -const BATCH_BUDGET_MS = RECEIVE_BATCH_SIZE * MESSAGE_BUDGET_MS +/** One license's read is aborted here, whatever the SDK's three attempts have reached. */ +const LICENSE_READ_BUDGET_MS = 15_000 /** - * 10 messages × 15 s = 150 s per batch, so each receive hides its batch for 180 s (above the work, - * independent of the queue's 5-minute default), and a run starts no batch or re-verify that could - * end past 6 minutes, well inside the cron's 10-minute lock. + * A batch of 10 single-license messages reads for at most 10 × 15 s = 150 s, and notices are only + * enqueued, so each receive hides its batch for 180 s; a multi-license message extends its own + * visibility per license, and if that lets a sibling reappear, handling it again is idempotent. A + * run starts no batch or re-verify that could read past 6 minutes, inside the cron's 10-minute lock. */ +const BATCH_BUDGET_MS = RECEIVE_BATCH_SIZE * LICENSE_READ_BUDGET_MS const VISIBILITY_TIMEOUT_SECONDS = 180 const RUN_BUDGET_MS = 6 * 60 * 1000 @@ -61,6 +67,8 @@ interface AwsMarketplaceReconcileSummary { received: number applied: number discarded: number + /** Named a license that has no row and that AWS described nothing to create one from. */ + skipped: number retained: number reverified: number reverifyFailed: number @@ -71,15 +79,32 @@ interface AwsMarketplaceReconcileSummary { /** Whether work that may take up to `workMs` still ends inside the run's budget. */ type BudgetCheck = (workMs: number) => boolean +interface DrainContext { + productCode: string + queueUrl: string + summary: AwsMarketplaceReconcileSummary + fits: BudgetCheck +} + type MarketplaceEvent = - | { kind: 'license'; licenseArn: string; productCode: string; agreementId: string | null } - | { kind: 'agreement-ended'; agreementId: string } | { - kind: 'agreement-created' - agreementId: string - intent: string | null - previousAgreementId: string | null + kind: 'license' + deprovisioned: boolean + licenseArn: string | null + productCode: string | null + agreementId: string | null + acceptorAccountId: string | null + occurredAt: Date | null } + | { kind: 'agreement-ended'; agreementId: string } + | { kind: 'agreement-created'; agreementId: string; previousAgreementId: string | null } + | { kind: 'advisory'; agreementId: string; state: 'issued' | 'resolved'; detail: string } + +function parseEventTime(value: unknown): Date | null { + const text = toStringOrNull(value) + const time = text ? new Date(text) : null + return time && Number.isFinite(time.getTime()) ? time : null +} /** * The event a queue message carries, or null for a body that is not one. Agreement events name no @@ -98,12 +123,16 @@ function parseMarketplaceEvent(body: string | undefined): MarketplaceEvent | nul const detail = toRecord(event.detail) const agreement = toRecord(detail.agreement) const agreementId = toStringOrNull(agreement.id) - if (LICENSE_EVENT_TYPES.has(detailType)) { - const licenseArn = toStringOrNull(toRecord(detail.license).arn) - const productCode = toStringOrNull(toRecord(detail.product).code) - return licenseArn && productCode - ? { kind: 'license', licenseArn, productCode, agreementId } - : null + if (detailType === LICENSE_UPDATED_EVENT || detailType === LICENSE_DEPROVISIONED_EVENT) { + return { + kind: 'license', + deprovisioned: detailType === LICENSE_DEPROVISIONED_EVENT, + licenseArn: toStringOrNull(toRecord(detail.license).arn), + productCode: toStringOrNull(toRecord(detail.product).code), + agreementId, + acceptorAccountId: toStringOrNull(toRecord(detail.acceptor).accountId), + occurredAt: parseEventTime(event.time), + } } if (!agreementId) return null if (AGREEMENT_ENDED_EVENT_TYPES.has(detailType)) return { kind: 'agreement-ended', agreementId } @@ -111,103 +140,202 @@ function parseMarketplaceEvent(body: string | undefined): MarketplaceEvent | nul return { kind: 'agreement-created', agreementId, - intent: toStringOrNull(agreement.intent), previousAgreementId: toStringOrNull(agreement.previousAgreementId), } } + const advisoryState = ADVISORY_STATE_BY_EVENT_TYPE.get(detailType) + if (advisoryState) { + return { + kind: 'advisory', + agreementId, + state: advisoryState, + detail: truncate(JSON.stringify(detail), MAX_ADVISORY_DETAIL_LENGTH), + } + } return null } -type MessageOutcome = 'applied' | 'discarded' | 'retained' +type MessageOutcome = 'applied' | 'discarded' | 'skipped' | 'retained' -/** Stamps the attempt, asks the Entitlement Service, records its answer, and tells the team. */ +/** Stamps the attempt, asks the Entitlement Service, and records its answer. */ async function reverifyLicense( productCode: string, licenseArn: string, - signal: AbortSignal, - agreementId?: string | null -): Promise { + options: { + ended?: boolean + agreement?: { agreementId: string | null; seenAt: Date | null } + fallbackAccountId?: string | null + licenseEventReceivedAt?: Date + } = {} +) { const verifiedAt = new Date() await markVerificationAttempted(licenseArn, verifiedAt) - const summary = await verifyLicense(productCode, licenseArn, signal) + const summary = await verifyLicense( + productCode, + licenseArn, + AbortSignal.timeout(LICENSE_READ_BUDGET_MS) + ) const outcome = await recordLicenseVerification({ licenseArn, productCode, - agreementId, - summary, - verifiedAt, + verification: { summary, verifiedAt, ended: options.ended }, + agreement: options.agreement, + fallbackAccountId: options.fallbackAccountId, + licenseEventReceivedAt: options.licenseEventReceivedAt, }) - await notifyTeamOfVerification(outcome) + return { summary, outcome } } -/** An agreement ID we hold no license for is normal: the seller account has other products. */ +async function extendVisibility( + context: DrainContext, + message: Message, + messageId: string | undefined +) { + if (!message.ReceiptHandle) return + try { + await getEventsQueueClient().send( + new ChangeMessageVisibilityCommand({ + QueueUrl: context.queueUrl, + ReceiptHandle: message.ReceiptHandle, + VisibilityTimeout: VISIBILITY_TIMEOUT_SECONDS, + }) + ) + } catch (error) { + logger.warn('Could not extend an AWS Marketplace event; it may be redelivered', { + messageId, + message: getErrorMessage(error, 'Unknown error'), + }) + } +} + +/** + * Each license gets its own read bound and commits on its own, so one that fails or runs out of + * time costs no other's progress. The message stays queued until all of them have been read, and + * a redelivery starts with those least recently attempted. An agreement ID with no license is + * normal: the seller account has other products. + */ async function applyAgreementEnded( - productCode: string, + context: DrainContext, agreementId: string, - messageId: string | undefined, - signal: AbortSignal + message: Message ): Promise { - const licenseArns = await listLicensesForAgreement(productCode, agreementId) + const messageId = message.MessageId + const licenseArns = await listLicensesForAgreement(context.productCode, agreementId) if (licenseArns.length === 0) { logger.info('Ignoring an AWS Marketplace agreement event for no stored license', { messageId }) return 'discarded' } - for (const licenseArn of licenseArns) { - await reverifyLicense(productCode, licenseArn, signal) + let incomplete = false + for (const [index, licenseArn] of licenseArns.entries()) { + if (!context.fits(LICENSE_READ_BUDGET_MS)) { + context.summary.outOfBudget = true + return 'retained' + } + if (index > 0) await extendVisibility(context, message, messageId) + try { + await reverifyLicense(context.productCode, licenseArn, { ended: true }) + } catch (error) { + if (isInvalidEntitlementRequest(error)) { + logger.warn('The Entitlement Service rejected a license of an ended agreement', { + messageId, + message: getErrorMessage(error, 'Unknown error'), + }) + continue + } + incomplete = true + logger.warn('Could not re-verify a license of an ended agreement', { + messageId, + message: getErrorMessage(error, 'Unknown error'), + }) + } } - return 'applied' + return incomplete ? 'retained' : 'applied' } -async function applyAgreementCreated( - productCode: string, - event: Extract, +async function applyLicenseEvent( + context: DrainContext, + event: Extract, messageId: string | undefined ): Promise { - const previousAgreementId = event.intent === 'RENEW' ? event.previousAgreementId : null - const renewed = previousAgreementId - ? await markLicensesRenewed({ - productCode, - previousAgreementId, - agreementId: event.agreementId, - }) - : [] - if (!previousAgreementId || renewed.length === 0) { - logger.info('Ignoring an AWS Marketplace agreement event for no stored license', { messageId }) + const { productCode } = context + if (event.productCode !== productCode) { + logger.warn('Discarding an AWS Marketplace event for another product', { messageId }) return 'discarded' } - await notifyTeamOfAgreementRenewal(renewed, { - previousAgreementId, - agreementId: event.agreementId, + if (!event.licenseArn) { + logger.warn('Leaving an AWS Marketplace license event with no ARN for the dead-letter queue', { + messageId, + }) + return 'retained' + } + const { summary, outcome } = await reverifyLicense(productCode, event.licenseArn, { + ended: event.deprovisioned, + agreement: { agreementId: event.agreementId, seenAt: event.occurredAt }, + fallbackAccountId: event.acceptorAccountId, + licenseEventReceivedAt: event.deprovisioned ? undefined : new Date(), }) + if (outcome.kind === 'skipped') { + logger.warn('Skipping an AWS Marketplace event for a license AWS does not describe', { + messageId, + }) + return 'skipped' + } + if (event.deprovisioned && summary.active) { + logger.warn('AWS deprovisioned a license its Entitlement Service still reports as current', { + messageId, + licenseId: outcome.license.id, + }) + return 'retained' + } return 'applied' } +async function applyEvent( + context: DrainContext, + event: MarketplaceEvent, + message: Message +): Promise { + const { productCode } = context + const messageId = message.MessageId + if (event.kind === 'license') return applyLicenseEvent(context, event, messageId) + if (event.kind === 'agreement-ended') { + return applyAgreementEnded(context, event.agreementId, message) + } + const involved = + event.kind === 'advisory' + ? await enqueueAgreementAdvisory({ + productCode, + agreementId: event.agreementId, + state: event.state, + detail: event.detail, + messageId: messageId ?? event.agreementId, + }) + : event.previousAgreementId + ? await applyAgreementRenewal({ + productCode, + previousAgreementId: event.previousAgreementId, + agreementId: event.agreementId, + }) + : false + if (involved) return 'applied' + logger.info('Ignoring an AWS Marketplace agreement event for no stored license', { messageId }) + return 'discarded' +} + /** * Re-reads what an event concerns rather than trusting the event's own fields. A message that can * never apply is discarded; one that failed for a reason that may pass is retained, so SQS * redelivers it and the dead-letter queue catches one that never succeeds. */ -async function handleMessage(productCode: string, message: Message): Promise { +async function handleMessage(context: DrainContext, message: Message): Promise { const messageId = message.MessageId const event = parseMarketplaceEvent(message.Body) if (!event) { logger.warn('Discarding an unrecognized AWS Marketplace event', { messageId }) return 'discarded' } - if (event.kind === 'license' && event.productCode !== productCode) { - logger.warn('Discarding an AWS Marketplace event for another product', { messageId }) - return 'discarded' - } try { - const signal = AbortSignal.timeout(MESSAGE_BUDGET_MS) - if (event.kind === 'agreement-ended') { - return await applyAgreementEnded(productCode, event.agreementId, messageId, signal) - } - if (event.kind === 'agreement-created') { - return await applyAgreementCreated(productCode, event, messageId) - } - await reverifyLicense(productCode, event.licenseArn, signal, event.agreementId) - return 'applied' + return await applyEvent(context, event, message) } catch (error) { if (isInvalidEntitlementRequest(error)) { logger.warn('Discarding an AWS Marketplace event the Entitlement Service rejected', { @@ -224,12 +352,8 @@ async function handleMessage(productCode: string, message: Message): Promise { +async function drainEvents(context: DrainContext): Promise { + const { queueUrl, summary, fits } = context const client = getEventsQueueClient() for (let batch = 0; batch < MAX_RECEIVE_BATCHES; batch++) { if (!fits(BATCH_BUDGET_MS)) { @@ -257,7 +381,7 @@ async function drainEvents( for (const message of messages) { summary.received++ - const outcome = await handleMessage(productCode, message) + const outcome = await handleMessage(context, message) summary[outcome]++ if (outcome === 'retained' || !message.ReceiptHandle) continue try { @@ -282,12 +406,12 @@ async function reverifyStaleLicenses( const cutoff = new Date(Date.now() - REVERIFY_AFTER_MS) const licenseArns = await listLicensesDueForReverify(cutoff, REVERIFY_BATCH_SIZE) for (const licenseArn of licenseArns) { - if (!fits(MESSAGE_BUDGET_MS)) { + if (!fits(LICENSE_READ_BUDGET_MS)) { summary.outOfBudget = true return } try { - await reverifyLicense(productCode, licenseArn, AbortSignal.timeout(MESSAGE_BUDGET_MS)) + await reverifyLicense(productCode, licenseArn) summary.reverified++ } catch (error) { summary.reverifyFailed++ @@ -311,6 +435,7 @@ export async function reconcileAwsMarketplaceLicenses(): Promise Date.now() - startedAt + workMs <= RUN_BUDGET_MS const queueUrl = env.AWS_MARKETPLACE_EVENTS_QUEUE_URL - if (queueUrl) await drainEvents(productCode, queueUrl, summary, fits) + if (queueUrl) await drainEvents({ productCode, queueUrl, summary, fits }) await reverifyStaleLicenses(productCode, summary, fits) return summary } diff --git a/apps/sim/lib/billing/aws-marketplace/registration.ts b/apps/sim/lib/billing/aws-marketplace/registration.ts index ce68be54f01..0bbce158a0b 100644 --- a/apps/sim/lib/billing/aws-marketplace/registration.ts +++ b/apps/sim/lib/billing/aws-marketplace/registration.ts @@ -4,6 +4,7 @@ import { } from '@aws-sdk/client-marketplace-metering' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' import { AWS_MARKETPLACE_CLAIM_TTL_MS, issueClaimToken } from '@/lib/billing/aws-marketplace/claim' import { getMeteringClient } from '@/lib/billing/aws-marketplace/client' import { type EntitlementSummary, verifyLicense } from '@/lib/billing/aws-marketplace/entitlements' @@ -15,6 +16,9 @@ const logger = createLogger('AwsMarketplaceRegistration') /** Token failures the buyer can fix only by starting again from AWS Marketplace. */ const REJECTED_TOKEN_ERRORS = new Set(['InvalidTokenException', 'ExpiredTokenException']) +/** The buyer's browser is waiting, so AWS gets only a moment to finish after a purchase. */ +const NO_ENTITLEMENT_RETRY_DELAYS_MS = [500, 1_000] + type AwsMarketplaceRegistration = | { kind: 'claimed'; claimToken: string } | { kind: AwsMarketplaceOnboardingError } @@ -23,10 +27,27 @@ function errorName(error: unknown): string | undefined { return error instanceof Error ? error.name : undefined } +/** AWS may not have finished right after a purchase, so an empty answer is asked again briefly. */ +async function readEntitlements( + productCode: string, + licenseArn: string +): Promise<{ summary: EntitlementSummary; verifiedAt: Date }> { + let verifiedAt = new Date() + let summary = await verifyLicense(productCode, licenseArn) + for (const delayMs of NO_ENTITLEMENT_RETRY_DELAYS_MS) { + if (summary.entitlementCount > 0) break + await sleep(delayMs) + verifiedAt = new Date() + summary = await verifyLicense(productCode, licenseArn) + } + return { summary, verifiedAt } +} + /** * Turns the registration token AWS posts to the fulfillment URL into a license row. Every - * registration reads the license's entitlements, active or not, and only an active license - * gets a claim the buyer can link to an organization. The token itself is never logged. + * registration reads the license's entitlements. A license AWS has not confirmed yet still gets a + * claim, so the buyer can leave contact details; only a lapsed one does not. The token itself is + * never logged. */ export async function registerAwsMarketplaceBuyer( productCode: string, @@ -43,10 +64,7 @@ export async function registerAwsMarketplaceBuyer( logger.warn('AWS Marketplace rejected a registration token', { reason: name }) return { kind: 'invalid' } } - logger.error('Could not resolve an AWS Marketplace customer', { - reason: name, - message: getErrorMessage(error, 'Unknown error'), - }) + logger.error('Could not resolve an AWS Marketplace customer', { reason: name }) return { kind: 'unavailable' } } @@ -54,50 +72,40 @@ export async function registerAwsMarketplaceBuyer( logger.warn('AWS Marketplace registration is for another product') return { kind: 'invalid' } } - /** New SaaS integrations get no `CustomerIdentifier`; the account ID and license ARN name the buyer. */ + // New SaaS integrations get no CustomerIdentifier; the account ID and license ARN name the buyer. if (!customer.CustomerAWSAccountId) { logger.warn('AWS Marketplace registration resolved without an account') return { kind: 'invalid' } } - /** - * A future-dated agreement gets its license ARN on its start date. Nothing is saved, since the - * ARN is the row's key; the License Updated event for it records the row then. - */ + // A future-dated agreement gets its ARN on its start date; the License Updated event records it. if (!customer.LicenseArn) { logger.info('AWS Marketplace registration is for an agreement that has not started') return { kind: 'not-started' } } - const verifiedAt = new Date() - let summary: EntitlementSummary try { - summary = await verifyLicense(productCode, customer.LicenseArn) + const verification = await readEntitlements(productCode, customer.LicenseArn) + const claim = issueClaimToken() + const saved = await saveRegisteredLicense({ + licenseArn: customer.LicenseArn, + productCode, + customerAwsAccountId: customer.CustomerAWSAccountId, + agreementId: customer.Metadata?.AgreementId ?? null, + verification, + claim: { hash: claim.hash, expiresAt: new Date(Date.now() + AWS_MARKETPLACE_CLAIM_TTL_MS) }, + }) + logger.info('Registered an AWS Marketplace license', { + licenseId: saved.id, + status: saved.status, + }) + return saved.status === 'inactive' + ? { kind: 'no-subscription' } + : { kind: 'claimed', claimToken: claim.token } } catch (error) { - logger.error('Could not read entitlements for an AWS Marketplace registration', { + logger.error('Could not record an AWS Marketplace registration', { reason: errorName(error), message: getErrorMessage(error, 'Unknown error'), }) return { kind: 'unavailable' } } - - const claim = summary.active ? issueClaimToken() : null - const saved = await saveRegisteredLicense({ - licenseArn: customer.LicenseArn, - productCode, - customerAwsAccountId: customer.CustomerAWSAccountId, - customerIdentifier: customer.CustomerIdentifier ?? null, - agreementId: customer.Metadata?.AgreementId ?? null, - summary, - verifiedAt, - claim: claim - ? { hash: claim.hash, expiresAt: new Date(Date.now() + AWS_MARKETPLACE_CLAIM_TTL_MS) } - : null, - }) - logger.info('Registered an AWS Marketplace license', { - licenseId: saved.id, - active: saved.active, - }) - return claim && saved.active - ? { kind: 'claimed', claimToken: claim.token } - : { kind: 'no-subscription' } } diff --git a/apps/sim/lib/billing/aws-marketplace/repository.ts b/apps/sim/lib/billing/aws-marketplace/repository.ts index e708cca642a..bdfe87b74f1 100644 --- a/apps/sim/lib/billing/aws-marketplace/repository.ts +++ b/apps/sim/lib/billing/aws-marketplace/repository.ts @@ -1,158 +1,278 @@ import { db } from '@sim/db' -import { awsMarketplaceLicense, member, organization, user } from '@sim/db/schema' +import { awsMarketplaceLicense, member, organization, outboxEvent, user } from '@sim/db/schema' import { generateId } from '@sim/utils/id' -import { and, asc, eq, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm' +import { and, asc, count, desc, eq, inArray, isNotNull, isNull, lt, ne, or } from 'drizzle-orm' import type { EntitlementSummary } from '@/lib/billing/aws-marketplace/entitlements' +import { + AWS_MARKETPLACE_BUYER_NOTICE_EVENT, + AWS_MARKETPLACE_TEAM_NOTICE_EVENT, + type AwsMarketplaceBuyerNotice, + type AwsMarketplaceNoticeTerms, + type AwsMarketplaceTeamNotice, +} from '@/lib/billing/aws-marketplace/notice-events' +import { enqueueOutboxEvent } from '@/lib/core/outbox/service' import type { DbTransaction } from '@/lib/db/types' export type AwsMarketplaceLicenseRecord = typeof awsMarketplaceLicense.$inferSelect +type AwsMarketplaceLicenseStatus = AwsMarketplaceLicenseRecord['status'] -interface AdministeredOrganization { - id: string - name: string - role: 'owner' | 'admin' +const MAX_LICENSES_PER_AGREEMENT = 20 + +/** One verification read, as a writer applies it under the license's row lock. */ +export interface LicenseVerification { + summary: EntitlementSummary + /** When the read started; a read older than the row's last one changes nothing. */ + verifiedAt: Date + /** + * AWS said this license ended (Deprovisioned or Agreement Ended), so no entitlements means it + * lapsed rather than that AWS has not finished setting it up. + */ + ended?: boolean +} + +/** Where an agreement ID came from, and when that source was true. */ +interface AgreementObservation { + agreementId: string | null + seenAt: Date | null +} + +function enqueueTeamNotice(tx: DbTransaction, notice: AwsMarketplaceTeamNotice, id?: string) { + return enqueueOutboxEvent(tx, AWS_MARKETPLACE_TEAM_NOTICE_EVENT, notice, { id }) +} + +function enqueueBuyerNotice(tx: DbTransaction, notice: AwsMarketplaceBuyerNotice) { + return enqueueOutboxEvent(tx, AWS_MARKETPLACE_BUYER_NOTICE_EVENT, notice) +} + +/** Whether a read that started at `verifiedAt` may overwrite what the row last recorded. */ +export function isNewerRead(license: AwsMarketplaceLicenseRecord, verifiedAt: Date): boolean { + return license.lastVerifiedAt.getTime() <= verifiedAt.getTime() } /** - * The verification columns a read writes. A deprovisioned license returns no entitlements, so its - * last-known dimension, quantity, and expiry stay as they were and only the status changes. + * No entitlements after AWS has said the license ended, or on a license that was already confirmed + * once, is a lapse. Otherwise AWS may simply not have finished, so the license stays pending. */ -function verificationFields(summary: EntitlementSummary, verifiedAt: Date) { +function statusFromRead( + prior: AwsMarketplaceLicenseStatus | null, + { summary, ended }: LicenseVerification +): AwsMarketplaceLicenseStatus { + if (summary.active) return 'active' + if (summary.entitlementCount > 0 || ended) return 'inactive' + return prior === null || prior === 'pending' ? 'pending' : 'inactive' +} + +/** + * The columns a read writes. With no entitlements the last-known dimension, quantity, and expiry + * stay as they were, and only the status can change. An inactive license has nothing to claim. + */ +function verificationFields( + prior: AwsMarketplaceLicenseStatus | null, + verification: LicenseVerification +) { + const status = statusFromRead(prior, verification) + const { summary, verifiedAt } = verification return { - status: summary.active ? ('active' as const) : ('inactive' as const), + status, lastVerifiedAt: verifiedAt, + lastVerificationAttemptAt: verifiedAt, updatedAt: new Date(), ...(summary.entitlementCount > 0 && { dimension: summary.dimension, quantity: summary.quantity, expiresAt: summary.expiresAt, }), + ...(status === 'inactive' && { claimTokenHash: null, claimExpiresAt: null }), } } -/** Whether a read that started at `verifiedAt` may overwrite what the row last recorded. */ -function isNewerRead(license: AwsMarketplaceLicenseRecord, verifiedAt: Date): boolean { - return license.lastVerifiedAt.getTime() <= verifiedAt.getTime() +function termsOf(license: AwsMarketplaceLicenseRecord): AwsMarketplaceNoticeTerms { + return { + status: license.status, + dimension: license.dimension, + quantity: license.quantity, + expiresAt: license.expiresAt?.toISOString() ?? null, + } +} + +function termsDiffer(before: AwsMarketplaceNoticeTerms, after: AwsMarketplaceNoticeTerms): boolean { + return ( + before.status !== after.status || + before.dimension !== after.dimension || + before.quantity !== after.quantity || + before.expiresAt !== after.expiresAt + ) +} + +/** + * The one place a read reaches a stored license, for every writer: under the caller's row lock it + * applies the read unless a newer one is stored, and when that moves the terms, enqueues the team's + * notice in the same transaction. `alongside` is written either way. + */ +async function applyVerification( + tx: DbTransaction, + locked: AwsMarketplaceLicenseRecord, + verification: LicenseVerification, + alongside: Partial = {} +): Promise { + const fresh = isNewerRead(locked, verification.verifiedAt) + const [license] = await tx + .update(awsMarketplaceLicense) + .set({ + ...(fresh ? verificationFields(locked.status, verification) : { updatedAt: new Date() }), + ...alongside, + }) + .where(eq(awsMarketplaceLicense.id, locked.id)) + .returning() + const before = termsOf(locked) + const after = termsOf(license) + if (fresh && termsDiffer(before, after)) { + await enqueueTeamNotice(tx, { kind: 'terms-changed', licenseId: license.id, before, after }) + } + return license +} + +/** The agreement columns an observation may write: never one older than what is stored. */ +function agreementFields(locked: AwsMarketplaceLicenseRecord | null, seen: AgreementObservation) { + if (!seen.agreementId) return {} + const stored = locked?.agreementSeenAt + const older = stored ? !seen.seenAt || seen.seenAt.getTime() < stored.getTime() : false + if (older || (locked?.agreementId && !seen.seenAt)) return {} + return { agreementId: seen.agreementId, agreementSeenAt: seen.seenAt } +} + +async function lockByArn(tx: DbTransaction, licenseArn: string) { + const [locked] = await tx + .select() + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.licenseArn, licenseArn)) + .for('update') + .limit(1) + return locked ?? null +} + +/** + * AWS grants a new license with every new agreement. When a license with no owner names an + * agreement a linked license was renewed to, it carries on that license's organization, linker, + * and contact. Runs from whichever arrives second, the Created event or the license itself. + */ +async function inheritFromRenewedLicense( + tx: DbTransaction, + license: AwsMarketplaceLicenseRecord +): Promise { + if (!license.agreementId || license.organizationId || license.linkedByUserId) return license + const [predecessor] = await tx + .select() + .from(awsMarketplaceLicense) + .where( + and( + eq(awsMarketplaceLicense.renewedAgreementId, license.agreementId), + eq(awsMarketplaceLicense.productCode, license.productCode), + eq(awsMarketplaceLicense.customerAwsAccountId, license.customerAwsAccountId), + ne(awsMarketplaceLicense.id, license.id), + or( + isNotNull(awsMarketplaceLicense.organizationId), + isNotNull(awsMarketplaceLicense.linkedByUserId) + ) + ) + ) + .orderBy(asc(awsMarketplaceLicense.organizationId)) + .limit(1) + if (!predecessor) return license + const [inherited] = await tx + .update(awsMarketplaceLicense) + .set({ + organizationId: predecessor.organizationId, + linkedByUserId: predecessor.linkedByUserId, + contactEmail: predecessor.contactEmail, + companyName: predecessor.companyName, + updatedAt: new Date(), + }) + .where(eq(awsMarketplaceLicense.id, license.id)) + .returning() + return inherited } /** - * Records a registration: the license row keyed by its ARN, as ResolveCustomer and the - * Entitlement Service described it, with the claim when the license is active. Every writer of - * entitlement fields follows one rule: a read older than the row's last one changes none of them, - * so a slow registration cannot restore `active` after the reconcile recorded `inactive`. The - * claim is stored only while the newest answer is active, and an inactive one clears any claim - * outstanding from an earlier registration. + * Records a registration: the license row keyed by its ARN, with `claim` unless the newest answer + * is inactive. A new row tells the team at once, so a buyer who pays and abandons setup is seen. */ export async function saveRegisteredLicense(input: { licenseArn: string productCode: string customerAwsAccountId: string - customerIdentifier: string | null agreementId: string | null - summary: EntitlementSummary - verifiedAt: Date - claim: { hash: string; expiresAt: Date } | null -}): Promise<{ id: string; active: boolean }> { - const identity = { - productCode: input.productCode, - customerAwsAccountId: input.customerAwsAccountId, - ...(input.customerIdentifier && { customerIdentifier: input.customerIdentifier }), - ...(input.agreementId && { agreementId: input.agreementId }), - } - const claimFields = (active: boolean) => ({ - claimTokenHash: active ? (input.claim?.hash ?? null) : null, - claimExpiresAt: active ? (input.claim?.expiresAt ?? null) : null, - }) + verification: LicenseVerification + claim: { hash: string; expiresAt: Date } +}): Promise<{ id: string; status: AwsMarketplaceLicenseStatus }> { + const { verification, claim } = input + const claimFields = { claimTokenHash: claim.hash, claimExpiresAt: claim.expiresAt } + const seen = { agreementId: input.agreementId, seenAt: verification.verifiedAt } return db.transaction(async (tx) => { + const status = statusFromRead(null, verification) const [created] = await tx .insert(awsMarketplaceLicense) .values({ id: generateId(), licenseArn: input.licenseArn, - ...identity, - ...verificationFields(input.summary, input.verifiedAt), - ...claimFields(input.summary.active), + productCode: input.productCode, + customerAwsAccountId: input.customerAwsAccountId, + ...agreementFields(null, seen), + ...(status === 'inactive' ? {} : claimFields), + ...verificationFields(null, verification), }) .onConflictDoNothing({ target: awsMarketplaceLicense.licenseArn }) - .returning({ id: awsMarketplaceLicense.id }) - if (created) return { id: created.id, active: input.summary.active } - - const [locked] = await tx - .select() - .from(awsMarketplaceLicense) - .where(eq(awsMarketplaceLicense.licenseArn, input.licenseArn)) - .for('update') - .limit(1) - const fresh = isNewerRead(locked, input.verifiedAt) - const active = fresh ? input.summary.active : locked.status === 'active' - await tx - .update(awsMarketplaceLicense) - .set({ - ...identity, - ...(fresh - ? verificationFields(input.summary, input.verifiedAt) - : { updatedAt: new Date() }), - ...claimFields(active && input.summary.active), + .returning() + if (created) { + const license = await inheritFromRenewedLicense(tx, created) + await enqueueTeamNotice(tx, { + kind: 'new-license', + licenseId: license.id, + source: 'registration', }) - .where(eq(awsMarketplaceLicense.id, locked.id)) - return { id: locked.id, active: active && input.summary.active } - }) -} - -/** The terms the team provisions from; a change to any of them needs their attention. */ -export interface LicenseTerms { - status: 'active' | 'inactive' - quantity: number | null - expiresAt: Date | null -} - -function termsOf(license: AwsMarketplaceLicenseRecord): LicenseTerms { - return { status: license.status, quantity: license.quantity, expiresAt: license.expiresAt } -} + return { id: license.id, status: license.status } + } -function termsDiffer(before: LicenseTerms, after: LicenseTerms): boolean { - return ( - before.status !== after.status || - before.quantity !== after.quantity || - (before.expiresAt?.getTime() ?? null) !== (after.expiresAt?.getTime() ?? null) - ) + const locked = await lockByArn(tx, input.licenseArn) + if (!locked) throw new Error('AWS Marketplace license vanished during registration') + const effective = isNewerRead(locked, verification.verifiedAt) + ? statusFromRead(locked.status, verification) + : locked.status + const written = await applyVerification(tx, locked, verification, { + ...agreementFields(locked, seen), + ...(effective === 'inactive' ? {} : claimFields), + }) + const license = await inheritFromRenewedLicense(tx, written) + return { id: license.id, status: license.status } + }) } export type LicenseVerificationOutcome = - /** AWS described a license no row held yet; it is recorded with no organization. */ - | { kind: 'created'; license: AwsMarketplaceLicenseRecord } - | { - kind: 'updated' - before: LicenseTerms - license: AwsMarketplaceLicenseRecord - /** Status, quantity, or expiry moved; an unchanged re-verify is false. */ - changed: boolean - } - /** No row to update, or a read older than the row's last one. */ + | { kind: 'created' | 'updated'; license: AwsMarketplaceLicenseRecord } + /** No row, and AWS described nothing to create one from. */ | { kind: 'skipped' } /** - * Applies a verification read that started at `verifiedAt`, comparing the terms before and after - * under one row lock. A read older than the row's last one changes nothing, so overlapping - * reconcile runs and registrations cannot roll a license back. A license AWS describes with an - * entitlement but nobody registered (a renewal may issue a new ARN) is recorded with no - * organization; one AWS no longer describes is only updated if it exists. + * Applies a verification read. A license AWS describes with an entitlement but nobody registered is + * recorded (a renewal gets a new ARN), carrying on a renewed license's owner when there is one; one + * AWS no longer describes is only updated if it exists. */ export async function recordLicenseVerification(input: { licenseArn: string productCode: string - /** The agreement a license event names; a buyer who never registered has no other source. */ - agreementId?: string | null - summary: EntitlementSummary - verifiedAt: Date + verification: LicenseVerification + /** The account from the event, for an entitlement that omits it. */ + fallbackAccountId?: string | null + agreement?: AgreementObservation + /** When a License Updated event was applied, which AWS asks to wait for before activating. */ + licenseEventReceivedAt?: Date }): Promise { - const fields = { - ...verificationFields(input.summary, input.verifiedAt), - ...(input.agreementId && { agreementId: input.agreementId }), - } + const { verification, licenseEventReceivedAt } = input + const seen = input.agreement ?? { agreementId: null, seenAt: null } + const stamp = licenseEventReceivedAt ? { licenseEventReceivedAt } : {} return db.transaction(async (tx) => { - const accountId = input.summary.customerAwsAccountId - if (accountId && input.summary.entitlementCount > 0) { + const accountId = verification.summary.customerAwsAccountId ?? input.fallbackAccountId + if (accountId && verification.summary.entitlementCount > 0) { const [created] = await tx .insert(awsMarketplaceLicense) .values({ @@ -160,52 +280,137 @@ export async function recordLicenseVerification(input: { licenseArn: input.licenseArn, productCode: input.productCode, customerAwsAccountId: accountId, - customerIdentifier: input.summary.customerIdentifier, - ...fields, + ...agreementFields(null, seen), + ...stamp, + ...verificationFields(null, verification), }) .onConflictDoNothing({ target: awsMarketplaceLicense.licenseArn }) .returning() - if (created) return { kind: 'created', license: created } + if (created) { + const license = await inheritFromRenewedLicense(tx, created) + await enqueueTeamNotice(tx, { kind: 'new-license', licenseId: license.id, source: 'event' }) + return { kind: 'created', license } + } } - const [locked] = await tx - .select() - .from(awsMarketplaceLicense) - .where(eq(awsMarketplaceLicense.licenseArn, input.licenseArn)) - .for('update') - .limit(1) - if (!locked || !isNewerRead(locked, input.verifiedAt)) return { kind: 'skipped' } - const [license] = await tx - .update(awsMarketplaceLicense) - .set(fields) - .where(eq(awsMarketplaceLicense.id, locked.id)) - .returning() - const before = termsOf(locked) - return { kind: 'updated', before, license, changed: termsDiffer(before, termsOf(license)) } + const locked = await lockByArn(tx, input.licenseArn) + if (!locked) return { kind: 'skipped' } + const written = await applyVerification(tx, locked, verification, { + ...agreementFields(locked, seen), + ...stamp, + }) + return { kind: 'updated', license: await inheritFromRenewedLicense(tx, written) } }) } /** - * The licenses longest since their last re-verify attempt, up to `limit`, none attempted after - * `cutoff`. Ordering by the attempt rather than the success keeps a license AWS keeps refusing - * from holding the front of every batch. + * Records that `agreementId` replaced `previousAgreementId`, from either arrival order, and tells + * the team. A renewal is accepted only on a row with none recorded or one naming the previous + * agreement, so a late A→B cannot overwrite B→C. Rows already on the new agreement count too, so + * the notice survives a license event that got here first, and SQS redelivery cannot send it twice. + * Returns whether any stored license was involved. */ -export async function listLicensesDueForReverify(cutoff: Date, limit: number): Promise { - const { lastVerificationAttemptAt, lastVerifiedAt } = awsMarketplaceLicense - const rows = await db - .select({ licenseArn: awsMarketplaceLicense.licenseArn }) - .from(awsMarketplaceLicense) - .where( - or( - lt(lastVerificationAttemptAt, cutoff), - and(isNull(lastVerificationAttemptAt), lt(lastVerifiedAt, cutoff)) +export async function applyAgreementRenewal(input: { + productCode: string + previousAgreementId: string + agreementId: string +}): Promise { + const { productCode, previousAgreementId, agreementId } = input + return db.transaction(async (tx) => { + const predecessors = await tx + .select() + .from(awsMarketplaceLicense) + .where( + and( + eq(awsMarketplaceLicense.productCode, productCode), + or( + eq(awsMarketplaceLicense.agreementId, previousAgreementId), + eq(awsMarketplaceLicense.renewedAgreementId, previousAgreementId) + ) + ) ) + .for('update') + const accepted = predecessors.filter( + (row) => row.renewedAgreementId === null || row.renewedAgreementId === previousAgreementId ) - .orderBy(asc(sql`coalesce(${lastVerificationAttemptAt}, ${lastVerifiedAt})`)) - .limit(limit) - return rows.map((row) => row.licenseArn) + if (accepted.length > 0) { + await tx + .update(awsMarketplaceLicense) + .set({ renewedAgreementId: agreementId, updatedAt: new Date() }) + .where( + inArray( + awsMarketplaceLicense.id, + accepted.map((row) => row.id) + ) + ) + } + const successors = await tx + .select() + .from(awsMarketplaceLicense) + .where( + and( + eq(awsMarketplaceLicense.productCode, productCode), + eq(awsMarketplaceLicense.agreementId, agreementId) + ) + ) + .for('update') + for (const successor of successors) await inheritFromRenewedLicense(tx, successor) + + const licenseIds = [...accepted, ...successors].map((row) => row.id) + if (licenseIds.length === 0) return false + const noticeId = `aws-marketplace-renewal-${agreementId}` + const [sent] = await tx + .select({ id: outboxEvent.id }) + .from(outboxEvent) + .where(eq(outboxEvent.id, noticeId)) + .limit(1) + if (!sent) { + await enqueueTeamNotice( + tx, + { kind: 'renewed', licenseIds, previousAgreementId, agreementId }, + noticeId + ) + } + return true + }) } -const MAX_LICENSES_PER_AGREEMENT = 20 +/** Tells the team of an AWS advisory on an agreement we hold; redelivery cannot send it twice. */ +export async function enqueueAgreementAdvisory(input: { + productCode: string + agreementId: string + state: 'issued' | 'resolved' + detail: string + messageId: string +}): Promise { + return db.transaction(async (tx) => { + const rows = await tx + .select({ id: awsMarketplaceLicense.id }) + .from(awsMarketplaceLicense) + .where(belongsToAgreement(input.productCode, input.agreementId)) + .limit(MAX_LICENSES_PER_AGREEMENT) + if (rows.length === 0) return false + const noticeId = `aws-marketplace-advisory-${input.messageId}` + const [sent] = await tx + .select({ id: outboxEvent.id }) + .from(outboxEvent) + .where(eq(outboxEvent.id, noticeId)) + .limit(1) + if (!sent) { + await enqueueTeamNotice( + tx, + { + kind: 'advisory', + licenseIds: rows.map((row) => row.id), + agreementId: input.agreementId, + state: input.state, + detail: input.detail, + }, + noticeId + ) + } + return true + }) +} function belongsToAgreement(productCode: string, agreementId: string) { return and( @@ -217,7 +422,10 @@ function belongsToAgreement(productCode: string, agreementId: string) { ) } -/** The licenses an agreement covers, by the ID they registered with or last renewed to. */ +/** + * The licenses an agreement covers, least recently attempted first, so a redelivered message moves + * on to the ones an earlier delivery did not reach. + */ export async function listLicensesForAgreement( productCode: string, agreementId: string @@ -226,34 +434,20 @@ export async function listLicensesForAgreement( .select({ licenseArn: awsMarketplaceLicense.licenseArn }) .from(awsMarketplaceLicense) .where(belongsToAgreement(productCode, agreementId)) - .orderBy(asc(awsMarketplaceLicense.createdAt)) + .orderBy(asc(awsMarketplaceLicense.lastVerificationAttemptAt)) .limit(MAX_LICENSES_PER_AGREEMENT) return rows.map((row) => row.licenseArn) } -/** - * Moves the licenses of the previous agreement to the renewal and returns those this call moved. - * The seller receives one renewal as both a Proposer and a Manufacturer event, and SQS may - * redeliver either; only the first finds rows to move, so the team hears of it once. - */ -export async function markLicensesRenewed(input: { - productCode: string - previousAgreementId: string - agreementId: string -}): Promise { - return db - .update(awsMarketplaceLicense) - .set({ renewedAgreementId: input.agreementId, updatedAt: new Date() }) - .where( - and( - belongsToAgreement(input.productCode, input.previousAgreementId), - or( - isNull(awsMarketplaceLicense.renewedAgreementId), - ne(awsMarketplaceLicense.renewedAgreementId, input.agreementId) - ) - ) - ) - .returning() +/** The licenses longest since a verification attempt, up to `limit`, none attempted after `cutoff`. */ +export async function listLicensesDueForReverify(cutoff: Date, limit: number): Promise { + const rows = await db + .select({ licenseArn: awsMarketplaceLicense.licenseArn }) + .from(awsMarketplaceLicense) + .where(lt(awsMarketplaceLicense.lastVerificationAttemptAt, cutoff)) + .orderBy(asc(awsMarketplaceLicense.lastVerificationAttemptAt)) + .limit(limit) + return rows.map((row) => row.licenseArn) } /** Stamps an attempt to verify a license, before its outcome is known. */ @@ -289,54 +483,143 @@ export async function lockLicense( } /** - * Consumes the claim and records the verification it relied on unless a newer one is stored, - * taking the license when `link` is given: for an organization, or for an account alone when its - * organization is null. A license already taken by the same party keeps its original link. + * Consumes the claim under the caller's row lock, taking the license when `link` is given: for an + * organization, or for an account alone when its organization is null. A take tells the team and + * confirms to the buyer in the same transaction. A license the same party already holds keeps its + * link and announces nothing. */ export async function consumeLicenseClaim( tx: DbTransaction, + locked: AwsMarketplaceLicenseRecord, input: { - licenseId: string link: { organizationId: string | null; userId: string } | null contact: { email: string; companyName: string | null } - /** Null when the row already holds a newer read, which this one must not overwrite. */ - verification: { summary: EntitlementSummary; verifiedAt: Date } | null + verification: LicenseVerification } ): Promise { - const [row] = await tx - .update(awsMarketplaceLicense) - .set({ - ...(input.verification - ? verificationFields(input.verification.summary, input.verification.verifiedAt) - : { updatedAt: new Date() }), - contactEmail: input.contact.email, - companyName: input.contact.companyName, - ...(input.link && { - organizationId: input.link.organizationId, - linkedByUserId: input.link.userId, - linkedAt: new Date(), - }), - claimTokenHash: null, - claimExpiresAt: null, - }) - .where(eq(awsMarketplaceLicense.id, input.licenseId)) - .returning() - return row + const license = await applyVerification(tx, locked, input.verification, { + contactEmail: input.contact.email, + companyName: input.contact.companyName, + ...(input.link && { + organizationId: input.link.organizationId, + linkedByUserId: input.link.userId, + }), + claimTokenHash: null, + claimExpiresAt: null, + }) + if (input.link) { + await enqueueTeamNotice(tx, { kind: 'taken', licenseId: license.id, userId: input.link.userId }) + await enqueueBuyerNotice(tx, { licenseId: license.id }) + } + return license +} + +/** + * Applies a pending buyer's fresh read and their contact details, keeping the claim open. New + * details tell the team whom to reach while AWS finishes confirming. + */ +export async function saveLicenseContact( + tx: DbTransaction, + locked: AwsMarketplaceLicenseRecord, + input: { + contact: { email: string; companyName: string | null } + verification: LicenseVerification + } +): Promise { + const changed = + locked.contactEmail !== input.contact.email || locked.companyName !== input.contact.companyName + const license = await applyVerification(tx, locked, input.verification, { + contactEmail: input.contact.email, + companyName: input.contact.companyName, + }) + if (changed) await enqueueTeamNotice(tx, { kind: 'contact', licenseId: license.id }) + return license } /** Organizations the user owns or administers, which are the ones a license may be linked to. */ export async function listAdministeredOrganizations( userId: string -): Promise { - const rows = await db - .select({ id: organization.id, name: organization.name, role: member.role }) +): Promise> { + return db + .select({ id: organization.id, name: organization.name }) .from(member) .innerJoin(organization, eq(member.organizationId, organization.id)) .where(and(eq(member.userId, userId), inArray(member.role, ['owner', 'admin']))) .orderBy(asc(organization.name)) - return rows.flatMap((row) => - row.role === 'owner' || row.role === 'admin' ? [{ ...row, role: row.role }] : [] - ) +} + +const STATUS_COLUMNS = { + id: awsMarketplaceLicense.id, + status: awsMarketplaceLicense.status, + dimension: awsMarketplaceLicense.dimension, + quantity: awsMarketplaceLicense.quantity, + expiresAt: awsMarketplaceLicense.expiresAt, + lastVerifiedAt: awsMarketplaceLicense.lastVerifiedAt, +} + +/** What an organization's members see of the Marketplace licenses linked to it. */ +export async function listOrganizationLicenses(organizationId: string) { + return db + .select(STATUS_COLUMNS) + .from(awsMarketplaceLicense) + .where(eq(awsMarketplaceLicense.organizationId, organizationId)) + .orderBy(asc(awsMarketplaceLicense.createdAt)) +} + +/** What a buyer sees of the licenses registered to their account, before an organization exists. */ +export async function listAccountLicenses(userId: string) { + return db + .select(STATUS_COLUMNS) + .from(awsMarketplaceLicense) + .where( + and( + eq(awsMarketplaceLicense.linkedByUserId, userId), + isNull(awsMarketplaceLicense.organizationId) + ) + ) + .orderBy(asc(awsMarketplaceLicense.createdAt)) +} + +/** The licenses waiting for the team to link them, newest first, for the admin dashboard. */ +export async function listUnlinkedLicenses(page: { limit: number; offset: number }) { + return db + .select() + .from(awsMarketplaceLicense) + .where(isNull(awsMarketplaceLicense.organizationId)) + .orderBy(desc(awsMarketplaceLicense.createdAt)) + .limit(page.limit) + .offset(page.offset) +} + +export async function countUnlinkedLicenses(): Promise { + const [row] = await db + .select({ total: count() }) + .from(awsMarketplaceLicense) + .where(isNull(awsMarketplaceLicense.organizationId)) + return row?.total ?? 0 +} + +/** Sets a license's organization under its row lock; any outstanding claim has no further use. */ +export async function linkLicenseForTeam( + tx: DbTransaction, + licenseId: string, + organizationId: string +): Promise { + const [license] = await tx + .update(awsMarketplaceLicense) + .set({ organizationId, claimTokenHash: null, claimExpiresAt: null, updatedAt: new Date() }) + .where(eq(awsMarketplaceLicense.id, licenseId)) + .returning() + return license +} + +export async function organizationExists(tx: DbTransaction, organizationId: string) { + const [row] = await tx + .select({ id: organization.id }) + .from(organization) + .where(eq(organization.id, organizationId)) + .limit(1) + return Boolean(row) } /** Who linked a license to which organization, for the team's provisioning notice. */ @@ -367,20 +650,12 @@ export async function getLinkParties( } } -/** What an organization's members see of the Marketplace licenses linked to it. */ -export async function listOrganizationLicenses(organizationId: string) { +export async function getLicensesByIds(licenseIds: readonly string[]) { + if (licenseIds.length === 0) return [] return db - .select({ - id: awsMarketplaceLicense.id, - status: awsMarketplaceLicense.status, - dimension: awsMarketplaceLicense.dimension, - quantity: awsMarketplaceLicense.quantity, - expiresAt: awsMarketplaceLicense.expiresAt, - lastVerifiedAt: awsMarketplaceLicense.lastVerifiedAt, - }) + .select() .from(awsMarketplaceLicense) - .where(eq(awsMarketplaceLicense.organizationId, organizationId)) - .orderBy(asc(awsMarketplaceLicense.createdAt)) + .where(inArray(awsMarketplaceLicense.id, [...licenseIds])) } /** Organizations already linked to another license of this AWS account, for the team to match. */ diff --git a/apps/sim/lib/core/outbox/handlers.ts b/apps/sim/lib/core/outbox/handlers.ts index 62f9094fe03..5cf607449a9 100644 --- a/apps/sim/lib/core/outbox/handlers.ts +++ b/apps/sim/lib/core/outbox/handlers.ts @@ -1,5 +1,9 @@ import { ADMIN_INVITATION_OPERATION_EVENT_TYPE } from '@/lib/admin/invitation-operation-event' import { ADMIN_MEMBER_OPERATION_EVENT_TYPE } from '@/lib/admin/member-operation-event' +import { + AWS_MARKETPLACE_BUYER_NOTICE_EVENT, + AWS_MARKETPLACE_TEAM_NOTICE_EVENT, +} from '@/lib/billing/aws-marketplace/notice-events' import { ENTERPRISE_INVITE_PEOPLE_EVENT_TYPE, ENTERPRISE_MEMBER_RECONCILIATION_EVENT_TYPE, @@ -152,6 +156,15 @@ export const OUTBOX_HANDLER_GROUPS: readonly LazyOutboxHandlerGroup[] = [ return enterpriseOwnerClaimOutboxHandlers }, }, + { + events: [AWS_MARKETPLACE_TEAM_NOTICE_EVENT, AWS_MARKETPLACE_BUYER_NOTICE_EVENT], + load: async () => { + const { awsMarketplaceOutboxHandlers } = await import( + '@/lib/billing/aws-marketplace/notifications' + ) + return awsMarketplaceOutboxHandlers + }, + }, { events: [MIGRATED_INVITATION_EMAIL_EVENT_TYPE], load: async () => { diff --git a/apps/sim/lib/navigation/auth-surfaces.ts b/apps/sim/lib/navigation/auth-surfaces.ts index ca886bbde0b..8b8353ff653 100644 --- a/apps/sim/lib/navigation/auth-surfaces.ts +++ b/apps/sim/lib/navigation/auth-surfaces.ts @@ -10,6 +10,7 @@ const AUTH_SURFACE_ROOTS = [ '/oauth-error', '/invite', '/enterprise/claim', + '/marketplace', '/unsubscribe', '/desktop/auth', '/desktop/done', diff --git a/apps/sim/lib/navigation/paths.ts b/apps/sim/lib/navigation/paths.ts index 18f18ae0afd..fe511534675 100644 --- a/apps/sim/lib/navigation/paths.ts +++ b/apps/sim/lib/navigation/paths.ts @@ -25,6 +25,9 @@ export const WORKSPACES_PATH = '/workspace' /** Opens full settings in the viewer's most recent accessible workspace. */ export const WORKSPACE_SETTINGS_PATH = `${WORKSPACES_PATH}?redirect=settings` +/** Opens billing settings in the viewer's most recent accessible workspace. */ +export const WORKSPACE_BILLING_PATH = `${WORKSPACES_PATH}?redirect=billing` + /** Root of the organization surface; `/o` alone resolves like {@link APP_ENTRY_PATH}. */ const ORGANIZATIONS_PATH = '/o' diff --git a/apps/sim/package.json b/apps/sim/package.json index dd41c896f58..877f854df70 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -92,7 +92,7 @@ "@aws-sdk/client-sso-admin": "3.1150.0", "@aws-sdk/client-sts": "3.1150.0", "@aws-sdk/client-textract": "3.1150.0", - "@aws-sdk/credential-provider-http": "3.972.76", + "@aws-sdk/credential-providers": "3.1150.0", "@aws-sdk/lib-dynamodb": "3.1150.0", "@aws-sdk/s3-request-presigner": "3.1150.0", "@azure/communication-email": "1.1.0", diff --git a/bun.lock b/bun.lock index 9c4cc947ee9..c7b71214eee 100644 --- a/bun.lock +++ b/bun.lock @@ -246,7 +246,7 @@ "@aws-sdk/client-sso-admin": "3.1150.0", "@aws-sdk/client-sts": "3.1150.0", "@aws-sdk/client-textract": "3.1150.0", - "@aws-sdk/credential-provider-http": "3.972.76", + "@aws-sdk/credential-providers": "3.1150.0", "@aws-sdk/lib-dynamodb": "3.1150.0", "@aws-sdk/s3-request-presigner": "3.1150.0", "@azure/communication-email": "1.1.0", @@ -1096,6 +1096,8 @@ "@aws-sdk/core": ["@aws-sdk/core@3.978.3", "", { "dependencies": { "@aws-sdk/types": "^3.974.6", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.35.2", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-nslpzUCTI9b2IQMWSd6mSAXldBexPc8rr0d3dXUutXYFLFaTdsHa2GD7sJdsn3PhyjK0O5K8ypd6zi+RXXPVlg=="], + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.78", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.53", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.37.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oFab6FAl8EHhMkUR4VOOgVMYCSq0QSLTY3UpYXXAyoIYMinm0vYqTtfDWQuvWN2VjohTppeDPht1L7sCr7MnFg=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-rsRJcW2iFUhCgclyD6OSbMEaPl8Eu0qSeRZb2uuCK1ViZ66hl5wOe/p6Vr6MO3b+ydyKgFeqza2p6tZThvILlw=="], "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-pnKdh0Br7OJPj+7qKjzgTfDTFW0RhEMKXNDR2jv9CAM4oHuG/4z3hRh8KtutLfpmItuRtDbKBmwWkDqHwwAWXw=="], @@ -1112,6 +1114,8 @@ "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.80", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-TJF25Ndwy2HxcVj6oxMgp8qVlJAawlExHYBXqhuC6Zt2m+A7WpdkjBD6QglNbiArlteYljZHEY3R7KKmno6ISA=="], + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-cognito-identity": "^3.972.73", "@aws-sdk/credential-provider-env": "^3.972.74", "@aws-sdk/credential-provider-http": "^3.972.76", "@aws-sdk/credential-provider-ini": "^3.973.19", "@aws-sdk/credential-provider-login": "^3.972.81", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/credential-provider-process": "^3.972.74", "@aws-sdk/credential-provider-sso": "^3.973.18", "@aws-sdk/credential-provider-web-identity": "^3.972.80", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-wqJ2MjeDMTQhwuelUjc60mpKvDGWtOMjVJ1ZNxD7AapNIBFy5RVYPSys54RYeD8Gm3SwIKS1TwF934lXQ/CIeA=="], + "@aws-sdk/dynamodb-codec": ["@aws-sdk/dynamodb-codec@3.973.48", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-LlTEc0u7gs2Qhr09vnRbdI8pY8O6/hP7wcsnlwrWQGEEheLEm3SB5Aqv9z7LiTXXEb2elBdSUyQ5OPuprELD6A=="], "@aws-sdk/endpoint-cache": ["@aws-sdk/endpoint-cache@3.972.11", "", { "dependencies": { "mnemonist": "0.38.3", "tslib": "^2.6.2" } }, "sha512-8q1ICxcDjHId3bBryuu/j+1L9y5/3uQnwzLDt5j2ElcjZSoWmFtymdJy7OjLrluSMe0Z4mq5bcH4fxBXvlEHfw=="], @@ -4806,6 +4810,14 @@ "@aws-sdk/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.53", "", { "dependencies": { "@aws-sdk/core": "^3.979.0", "@aws-sdk/signature-v4-multi-region": "^3.996.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.37.0", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.13.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-JRcs7wwU/cMw62hbLUxlh/wyrHaL/4KOnJs/Hny0ZIo4GY8biBMEfLJrYDEBMF8wDIewtDZsBWT9Rna4uaRemw=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-cognito-identity/@smithy/core": ["@smithy/core@3.38.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-aLyyCnK2p4yvNA2TaM+5owXqb+rZGSiUKEsNP329AzeYnERtAz7NrlTbRsaxqmZ0ooxwoID8LMm959UGQQDIsg=="], + + "@aws-sdk/credential-provider-cognito-identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-sdk/credential-provider-env/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@aws-sdk/credential-provider-http/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4822,6 +4834,8 @@ "@aws-sdk/credential-provider-web-identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-sdk/credential-providers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-sdk/dynamodb-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@aws-sdk/endpoint-cache/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -5482,6 +5496,12 @@ "@aws-crypto/util/@aws-sdk/types/@smithy/types": ["@smithy/types@4.15.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/core@3.979.0", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.42", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.37.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-E3eeLhIOxlIc9NPsZbtpHuk8W/gEZ3YvFauNPrrrtNtF7Pq1cwSrFwA51Z1EIoUY6VFvTyltyLRNUhaa0aSKSA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.50", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-Ob9GCZ3YnGPws696zTYW/rLFYHa9QLh6DgTCnkHb+0jhHNBgHWM8Ui2b+yrIjkfDsRVsglyrOey6tOOG/VWxMA=="], + + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.14.0", "", { "dependencies": { "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-j24RS5V9J52u0z81RpgF7H55p4bS6UCEIxF4ZYzTYh6M9hzXvjAIAYwgLobqf7vJq3xib5VXc7nX3YesV8Rx4A=="], + "@better-auth/sso/tldts/tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], "@browserbasehq/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -5808,6 +5828,8 @@ "word-extractor/yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.42", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-smX8/gNP8Kjq+CWTBMZQLQ912vOQjiWExzwzvMO+eujPB5RY8IbtF0AGdUUXHKN7KoDWK9dIcuW0WG269kiP6w=="], + "@browserbasehq/stagehand/@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "@browserbasehq/stagehand/openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], diff --git a/packages/db/migrations/0454_aws_marketplace_license.sql b/packages/db/migrations/0454_aws_marketplace_license.sql index 54281adf49a..dd2d1a66d69 100644 --- a/packages/db/migrations/0454_aws_marketplace_license.sql +++ b/packages/db/migrations/0454_aws_marketplace_license.sql @@ -1,23 +1,23 @@ -CREATE TYPE "public"."aws_marketplace_license_status" AS ENUM('active', 'inactive');--> statement-breakpoint +CREATE TYPE "public"."aws_marketplace_license_status" AS ENUM('pending', 'active', 'inactive');--> statement-breakpoint CREATE TABLE "aws_marketplace_license" ( "id" text PRIMARY KEY NOT NULL, "license_arn" text NOT NULL, "product_code" text NOT NULL, - "customer_identifier" text, "customer_aws_account_id" text NOT NULL, "agreement_id" text, + "agreement_seen_at" timestamp, "renewed_agreement_id" text, "organization_id" text, - "status" "aws_marketplace_license_status" DEFAULT 'inactive' NOT NULL, + "status" "aws_marketplace_license_status" DEFAULT 'pending' NOT NULL, "dimension" text, "quantity" integer, "expires_at" timestamp, "last_verified_at" timestamp NOT NULL, - "last_verification_attempt_at" timestamp, + "last_verification_attempt_at" timestamp NOT NULL, + "license_event_received_at" timestamp, "claim_token_hash" text, "claim_expires_at" timestamp, "linked_by_user_id" text, - "linked_at" timestamp, "contact_email" text, "company_name" text, "created_at" timestamp DEFAULT now() NOT NULL, @@ -27,9 +27,10 @@ CREATE TABLE "aws_marketplace_license" ( ALTER TABLE "aws_marketplace_license" ADD CONSTRAINT "aws_marketplace_license_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "aws_marketplace_license" ADD CONSTRAINT "aws_marketplace_license_linked_by_user_id_user_id_fk" FOREIGN KEY ("linked_by_user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint CREATE UNIQUE INDEX "aws_marketplace_license_arn_unique" ON "aws_marketplace_license" USING btree ("license_arn");--> statement-breakpoint +CREATE UNIQUE INDEX "aws_marketplace_license_claim_token_hash_unique" ON "aws_marketplace_license" USING btree ("claim_token_hash");--> statement-breakpoint CREATE INDEX "aws_marketplace_license_agreement_id_idx" ON "aws_marketplace_license" USING btree ("agreement_id");--> statement-breakpoint CREATE INDEX "aws_marketplace_license_renewed_agreement_id_idx" ON "aws_marketplace_license" USING btree ("renewed_agreement_id");--> statement-breakpoint CREATE INDEX "aws_marketplace_license_account_product_idx" ON "aws_marketplace_license" USING btree ("customer_aws_account_id","product_code");--> statement-breakpoint -CREATE UNIQUE INDEX "aws_marketplace_license_claim_token_hash_unique" ON "aws_marketplace_license" USING btree ("claim_token_hash");--> statement-breakpoint -CREATE INDEX "aws_marketplace_license_reverify_due_idx" ON "aws_marketplace_license" USING btree (coalesce("last_verification_attempt_at", "last_verified_at"));--> statement-breakpoint -CREATE INDEX "aws_marketplace_license_organization_id_idx" ON "aws_marketplace_license" USING btree ("organization_id"); \ No newline at end of file +CREATE INDEX "aws_marketplace_license_last_attempt_idx" ON "aws_marketplace_license" USING btree ("last_verification_attempt_at");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_organization_id_idx" ON "aws_marketplace_license" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "aws_marketplace_license_linked_by_user_id_idx" ON "aws_marketplace_license" USING btree ("linked_by_user_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0454_snapshot.json b/packages/db/migrations/meta/0454_snapshot.json index 1922aeb3161..bd224a03291 100644 --- a/packages/db/migrations/meta/0454_snapshot.json +++ b/packages/db/migrations/meta/0454_snapshot.json @@ -1,5 +1,5 @@ { - "id": "54cc9081-fd36-49c9-ac52-0d35d68f54a1", + "id": "4265b908-fbe8-481a-9c67-67f8a79c2ee9", "prevId": "ae1c3abd-b4a0-4658-8dd6-4858c603d078", "version": "7", "dialect": "postgresql", @@ -1150,12 +1150,6 @@ "primaryKey": false, "notNull": true }, - "customer_identifier": { - "name": "customer_identifier", - "type": "text", - "primaryKey": false, - "notNull": false - }, "customer_aws_account_id": { "name": "customer_aws_account_id", "type": "text", @@ -1168,6 +1162,12 @@ "primaryKey": false, "notNull": false }, + "agreement_seen_at": { + "name": "agreement_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, "renewed_agreement_id": { "name": "renewed_agreement_id", "type": "text", @@ -1186,7 +1186,7 @@ "typeSchema": "public", "primaryKey": false, "notNull": true, - "default": "'inactive'" + "default": "'pending'" }, "dimension": { "name": "dimension", @@ -1216,6 +1216,12 @@ "name": "last_verification_attempt_at", "type": "timestamp", "primaryKey": false, + "notNull": true + }, + "license_event_received_at": { + "name": "license_event_received_at", + "type": "timestamp", + "primaryKey": false, "notNull": false }, "claim_token_hash": { @@ -1236,12 +1242,6 @@ "primaryKey": false, "notNull": false }, - "linked_at": { - "name": "linked_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, "contact_email": { "name": "contact_email", "type": "text", @@ -1285,6 +1285,21 @@ "method": "btree", "with": {} }, + "aws_marketplace_license_claim_token_hash_unique": { + "name": "aws_marketplace_license_claim_token_hash_unique", + "columns": [ + { + "expression": "claim_token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, "aws_marketplace_license_agreement_id_idx": { "name": "aws_marketplace_license_agreement_id_idx", "columns": [ @@ -1336,28 +1351,28 @@ "method": "btree", "with": {} }, - "aws_marketplace_license_claim_token_hash_unique": { - "name": "aws_marketplace_license_claim_token_hash_unique", + "aws_marketplace_license_last_attempt_idx": { + "name": "aws_marketplace_license_last_attempt_idx", "columns": [ { - "expression": "claim_token_hash", + "expression": "last_verification_attempt_at", "isExpression": false, "asc": true, "nulls": "last" } ], - "isUnique": true, + "isUnique": false, "concurrently": false, "method": "btree", "with": {} }, - "aws_marketplace_license_reverify_due_idx": { - "name": "aws_marketplace_license_reverify_due_idx", + "aws_marketplace_license_organization_id_idx": { + "name": "aws_marketplace_license_organization_id_idx", "columns": [ { - "expression": "coalesce(\"last_verification_attempt_at\", \"last_verified_at\")", + "expression": "organization_id", + "isExpression": false, "asc": true, - "isExpression": true, "nulls": "last" } ], @@ -1366,11 +1381,11 @@ "method": "btree", "with": {} }, - "aws_marketplace_license_organization_id_idx": { - "name": "aws_marketplace_license_organization_id_idx", + "aws_marketplace_license_linked_by_user_id_idx": { + "name": "aws_marketplace_license_linked_by_user_id_idx", "columns": [ { - "expression": "organization_id", + "expression": "linked_by_user_id", "isExpression": false, "asc": true, "nulls": "last" @@ -32439,7 +32454,7 @@ "public.aws_marketplace_license_status": { "name": "aws_marketplace_license_status", "schema": "public", - "values": ["active", "inactive"] + "values": ["pending", "active", "inactive"] }, "public.background_work_kind": { "name": "background_work_kind", diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 664d506ad24..1489fe74be8 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -3176,7 +3176,7 @@ { "idx": 454, "version": "7", - "when": 1793935763803, + "when": 1793937981518, "tag": "0454_aws_marketplace_license", "breakpoints": true } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 9ce1c74a8eb..979adaf251e 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1928,15 +1928,18 @@ export const organization = pgTable('organization', { }) export const awsMarketplaceLicenseStatusEnum = pgEnum('aws_marketplace_license_status', [ + 'pending', 'active', 'inactive', ]) /** - * An AWS Marketplace SaaS contract license, one row per license ARN: one AWS account can hold - * several concurrent agreements, so the account ID identifies the buyer but not the license. `status` mirrors the last Entitlement Service read, never a - * contract amount. The buyer's browser holds a one-time claim token whose hash and expiry live - * here until an organization owner or admin links the license to their organization. + * An AWS Marketplace SaaS contract license, one row per license ARN: AWS grants a new license with + * every agreement, so one account can hold several, and the account ID names the buyer but not the + * license. `status` is the newest Entitlement Service read, never a contract amount; `pending` + * means AWS has returned no entitlement yet, which `inactive` (lapsed or deprovisioned) is not. + * While a buyer's setup is open, their browser holds a one-time claim whose hash and expiry live + * here; a verification recording `inactive` clears it. */ export const awsMarketplaceLicense = pgTable( 'aws_marketplace_license', @@ -1944,28 +1947,28 @@ export const awsMarketplaceLicense = pgTable( id: text('id').primaryKey(), licenseArn: text('license_arn').notNull(), productCode: text('product_code').notNull(), - /** Legacy SaaS integrations only; ResolveCustomer leaves it empty for new products. */ - customerIdentifier: text('customer_identifier'), - /** Not unique: one AWS account can hold several concurrent licenses. */ customerAwsAccountId: text('customer_aws_account_id').notNull(), agreementId: text('agreement_id'), - /** The agreement a renewal event last moved this license to, so each renewal is told once. */ + /** When the source of `agreement_id` was true, so a redelivered old event cannot roll it back. */ + agreementSeenAt: timestamp('agreement_seen_at'), + /** The agreement that replaced this license's, from a Purchase Agreement Created event. */ renewedAgreementId: text('renewed_agreement_id'), organizationId: text('organization_id').references(() => organization.id, { onDelete: 'set null', }), - status: awsMarketplaceLicenseStatusEnum('status').notNull().default('inactive'), + status: awsMarketplaceLicenseStatusEnum('status').notNull().default('pending'), dimension: text('dimension'), quantity: integer('quantity'), expiresAt: timestamp('expires_at'), lastVerifiedAt: timestamp('last_verified_at').notNull(), - /** Every re-verify attempt, failed ones too, so a license AWS keeps refusing cannot starve the rest. */ - lastVerificationAttemptAt: timestamp('last_verification_attempt_at'), + /** Every verification attempt, failed ones too, so a failing license cannot starve the rest. */ + lastVerificationAttemptAt: timestamp('last_verification_attempt_at').notNull(), + /** AWS says not to activate a subscription until this event has arrived, so staff can see it. */ + licenseEventReceivedAt: timestamp('license_event_received_at'), claimTokenHash: text('claim_token_hash'), claimExpiresAt: timestamp('claim_expires_at'), linkedByUserId: text('linked_by_user_id').references(() => user.id, { onDelete: 'set null' }), - linkedAt: timestamp('linked_at'), - /** Whom to contact about provisioning, as the buyer entered it when taking the license. */ + /** Whom to contact about provisioning, as the buyer entered it during setup. */ contactEmail: text('contact_email'), companyName: text('company_name'), createdAt: timestamp('created_at').notNull().defaultNow(), @@ -1973,17 +1976,16 @@ export const awsMarketplaceLicense = pgTable( }, (table) => [ uniqueIndex('aws_marketplace_license_arn_unique').on(table.licenseArn), + uniqueIndex('aws_marketplace_license_claim_token_hash_unique').on(table.claimTokenHash), index('aws_marketplace_license_agreement_id_idx').on(table.agreementId), index('aws_marketplace_license_renewed_agreement_id_idx').on(table.renewedAgreementId), index('aws_marketplace_license_account_product_idx').on( table.customerAwsAccountId, table.productCode ), - uniqueIndex('aws_marketplace_license_claim_token_hash_unique').on(table.claimTokenHash), - index('aws_marketplace_license_reverify_due_idx').on( - sql`coalesce(${table.lastVerificationAttemptAt}, ${table.lastVerifiedAt})` - ), + index('aws_marketplace_license_last_attempt_idx').on(table.lastVerificationAttemptAt), index('aws_marketplace_license_organization_id_idx').on(table.organizationId), + index('aws_marketplace_license_linked_by_user_id_idx').on(table.linkedByUserId), ] ) diff --git a/packages/testing/src/mocks/schema-tables.generated.ts b/packages/testing/src/mocks/schema-tables.generated.ts index 45a7f8a0ec1..24e91762785 100644 --- a/packages/testing/src/mocks/schema-tables.generated.ts +++ b/packages/testing/src/mocks/schema-tables.generated.ts @@ -453,9 +453,9 @@ export const GENERATED_SCHEMA_TABLES = { 'id', 'licenseArn', 'productCode', - 'customerIdentifier', 'customerAwsAccountId', 'agreementId', + 'agreementSeenAt', 'renewedAgreementId', 'organizationId', 'status', @@ -464,10 +464,10 @@ export const GENERATED_SCHEMA_TABLES = { 'expiresAt', 'lastVerifiedAt', 'lastVerificationAttemptAt', + 'licenseEventReceivedAt', 'claimTokenHash', 'claimExpiresAt', 'linkedByUserId', - 'linkedAt', 'contactEmail', 'companyName', 'createdAt', @@ -2274,7 +2274,7 @@ export const GENERATED_SCHEMA_ENUMS = { secretUsageSourceEnum: ['workflow', 'copilot', 'mcp'], billingBlockedReasonEnum: ['payment_failed', 'dispute'], billingEntityTypeEnum: ['user', 'organization'], - awsMarketplaceLicenseStatusEnum: ['active', 'inactive'], + awsMarketplaceLicenseStatusEnum: ['pending', 'active', 'inactive'], invitationKindEnum: ['organization', 'workspace'], invitationMembershipIntentEnum: ['internal', 'external'], invitationStatusEnum: ['pending', 'accepted', 'rejected', 'cancelled', 'expired'], From 2807b8297b00e246b3decc4a8f08aadbf574680e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 20:49:57 -0800 Subject: [PATCH 5/7] fix(billing): apply AWS Marketplace review fixes Keep License Updated events for unknown licenses queued until AWS describes them, so a renewal cannot be lost. Tell the team when the event reaches a license already taken, and when a repeat take changes the contact details. Give the cancel instruction its required operationId, and only when an organization holds the license. Show account-registered licenses on organization billing, and stop offering plans on the upgrade page to anyone billed through AWS. Delete the expired and inactive claim states, which no reachable row can produce. --- .../aws/aws-marketplace-onboarding.tsx | 15 +-- .../settings/components/billing/billing.tsx | 17 ++- .../aws-marketplace-subscription-section.tsx | 6 +- .../upgrade/hooks/use-upgrade-state.ts | 7 +- .../[workspaceId]/upgrade/upgrade.tsx | 10 +- apps/sim/hooks/queries/aws-marketplace.ts | 16 +++ apps/sim/lib/api/contracts/aws-marketplace.ts | 4 +- .../aws-marketplace/application/use-cases.ts | 16 +-- .../aws-marketplace/lifecycle.integration.ts | 125 ++++++++++++++++-- .../billing/aws-marketplace/notice-events.ts | 1 + .../billing/aws-marketplace/notifications.ts | 32 ++++- .../lib/billing/aws-marketplace/reconcile.ts | 12 +- .../lib/billing/aws-marketplace/repository.ts | 42 ++++-- 13 files changed, 237 insertions(+), 66 deletions(-) diff --git a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx index ed555942d81..2cbc0e8468d 100644 --- a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx +++ b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx @@ -134,7 +134,7 @@ export function AwsMarketplaceOnboarding({ diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx index 5f06bcf2c99..7514ebec4ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx @@ -53,7 +53,7 @@ import { SettingsQueryErrorState } from '@/app/workspace/[workspaceId]/settings/ import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel' import { RESOURCE_ROW_ARROW_CLASSES } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' -import { useAwsMarketplaceLicenses } from '@/hooks/queries/aws-marketplace' +import { useAwsMarketplaceBilling } from '@/hooks/queries/aws-marketplace' import { useBillingUsageNotifications, useUpdateGeneralSetting, @@ -135,10 +135,7 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps enabled: !isOrganizationScope, }) const billingOrganizationId = isOrganizationScope ? (organizationId ?? null) : null - const { data: marketplaceLicenses } = useAwsMarketplaceLicenses(billingOrganizationId) - const billedThroughMarketplace = marketplaceLicenses?.some( - (license) => license.status !== 'inactive' - ) + const { billedThroughMarketplace } = useAwsMarketplaceBilling(billingOrganizationId) const { data: organizationBillingData, @@ -459,7 +456,9 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps : `Organization ${planName} plan` : `Personal ${planName} plan` const priceText = billedThroughMarketplace - ? 'Billed through AWS Marketplace' + ? subscription.isFree + ? 'AWS Marketplace setup in progress' + : 'Billed through AWS Marketplace' : organizationSubscriptionState === 'free' ? 'No active organization subscription' : organizationSubscriptionState === 'lapsed' @@ -669,6 +668,12 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps )} + {isOrganizationScope && ( + + )} {!subscription.isFree && invoices.length > 0 && ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx index 0fcacb762bd..f4a2250c043 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/components/aws-marketplace-subscription-section/aws-marketplace-subscription-section.tsx @@ -15,16 +15,18 @@ const STATUS_BADGES = { interface AwsMarketplaceSubscriptionSectionProps { /** Null reads the licenses registered to the viewer's own account. */ organizationId: string | null + label?: string } /** Read-only: AWS requires buyers to see their subscription status in the product. */ export function AwsMarketplaceSubscriptionSection({ organizationId, + label = 'AWS Marketplace subscription', }: AwsMarketplaceSubscriptionSectionProps) { const { data: licenses, error, isFetching, refetch } = useAwsMarketplaceLicenses(organizationId) if (error && !licenses) { return ( - + +
{licenses.map((license) => { const badge = STATUS_BADGES[license.status] diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts b/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts index 15152b4a57b..b612e8b6a3c 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/hooks/use-upgrade-state.ts @@ -9,6 +9,7 @@ import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces' import { useSubscriptionUpgrade } from '@/lib/billing/client/upgrade' import { CREDIT_TIERS } from '@/lib/billing/constants' import { getPlanTierCredits, isEnterprise, isFree, isPro, isTeam } from '@/lib/billing/plan-helpers' +import { useAwsMarketplaceBilling } from '@/hooks/queries/aws-marketplace' import { invalidateWorkspaceUsage } from '@/hooks/queries/utils/invalidate-usage' import { subscriptionKeys } from '@/hooks/queries/utils/subscription-keys' import { workspaceHostKeys } from '@/hooks/queries/workspace-host' @@ -38,6 +39,7 @@ export interface UpgradeState { status: string } showUpgradePlans: boolean + billedThroughMarketplace: boolean proTier: { credits: number; dollars: number; name: string } maxTier: { credits: number; dollars: number; name: string } isOnPro: boolean @@ -65,6 +67,7 @@ export function useUpgradeState({ const { handleUpgrade } = useSubscriptionUpgrade() const queryClient = useQueryClient() const { ownerBilling } = hostContext + const marketplace = useAwsMarketplaceBilling(hostContext.hostOrganizationId ?? null) const [isAnnual, setIsAnnual] = useState( !ownerBilling.isPaid || ownerBilling.billingInterval === 'year' ) @@ -197,7 +200,9 @@ export function useUpgradeState({ isAnnual, setIsAnnual, subscription, - showUpgradePlans: !subscription.isEnterprise, + showUpgradePlans: + !subscription.isEnterprise && !marketplace.isPending && !marketplace.billedThroughMarketplace, + billedThroughMarketplace: marketplace.billedThroughMarketplace, proTier: PRO_TIER, maxTier: MAX_TIER, isOnPro, diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx b/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx index 1bad0a5626b..8f4945ab453 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx @@ -85,10 +85,12 @@ export function Upgrade({ workspaceId }: UpgradeProps) { return null } - if (!canManageBilling) { - const description = hostContext.hostOrganizationId - ? `Plans for ${hostContext.workspace.name} are managed by its organization administrators. Contact an organization admin to change this workspace’s plan.` - : `Only the owner of ${hostContext.workspace.name} can change this workspace’s plan.` + if (!canManageBilling || state.billedThroughMarketplace) { + const description = canManageBilling + ? 'Your subscription is billed through AWS Marketplace, so plans can’t be changed here.' + : hostContext.hostOrganizationId + ? `Plans for ${hostContext.workspace.name} are managed by its organization administrators. Contact an organization admin to change this workspace’s plan.` + : `Only the owner of ${hostContext.workspace.name} can change this workspace’s plan.` return (
diff --git a/apps/sim/hooks/queries/aws-marketplace.ts b/apps/sim/hooks/queries/aws-marketplace.ts index 0845b12ecfd..a5312a48d03 100644 --- a/apps/sim/hooks/queries/aws-marketplace.ts +++ b/apps/sim/hooks/queries/aws-marketplace.ts @@ -67,6 +67,22 @@ export function useSaveAwsMarketplaceClaimContact() { }) } +/** + * Whether AWS Marketplace bills this organization or the viewer's own account: a pending or active + * license on either. Pending until both reads settle. + */ +export function useAwsMarketplaceBilling(organizationId: string | null) { + const organizationLicenses = useAwsMarketplaceLicenses(organizationId) + const accountLicenses = useAwsMarketplaceLicenses(null) + return { + isPending: organizationLicenses.isPending || accountLicenses.isPending, + billedThroughMarketplace: [ + ...(organizationLicenses.data ?? []), + ...(accountLicenses.data ?? []), + ].some((license) => license.status !== 'inactive'), + } +} + /** The Marketplace subscriptions an organization holds, or with null the viewer's own account. */ export function useAwsMarketplaceLicenses(organizationId: string | null) { return useQuery({ diff --git a/apps/sim/lib/api/contracts/aws-marketplace.ts b/apps/sim/lib/api/contracts/aws-marketplace.ts index a1c11862b7e..b778985a10b 100644 --- a/apps/sim/lib/api/contracts/aws-marketplace.ts +++ b/apps/sim/lib/api/contracts/aws-marketplace.ts @@ -46,10 +46,8 @@ const claimedLicenseSchema = awsMarketplaceLicenseSchema.extend({ }) const awsMarketplaceClaimSchema = z.discriminatedUnion('status', [ - /** This browser holds no claim, or one already used. */ + /** No open claim: none in this browser, or it was used, expired, or cleared by a lapse. */ z.object({ status: z.literal('missing') }), - z.object({ status: z.literal('expired') }), - z.object({ status: z.literal('inactive') }), /** AWS has returned no entitlement yet; the buyer can leave contact details meanwhile. */ z.object({ status: z.literal('pending'), license: claimedLicenseSchema }), z.object({ diff --git a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts index 2ec11396e28..4e1d3ec5383 100644 --- a/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts +++ b/apps/sim/lib/billing/aws-marketplace/application/use-cases.ts @@ -42,15 +42,13 @@ const SUBSCRIPTION_UNCONFIRMED = 'AWS is still confirming your subscription. Try again in a few minutes.' /** - * Under the row lock: a newer verification than this request's wins, so a license a newer read - * says is not active cannot be taken. + * Under the row lock, after the claim is rechecked: a newer read that still says pending wins over + * this request's. A newer read that says inactive already cleared the claim. */ function requireNotOverruled(locked: AwsMarketplaceLicenseRecord, verifiedAt: Date): void { - if (isNewerRead(locked, verifiedAt) || locked.status === 'active') return - throw new OrchestrationError( - 'conflict', - locked.status === 'pending' ? SUBSCRIPTION_UNCONFIRMED : NO_ACTIVE_SUBSCRIPTION - ) + if (locked.status === 'pending' && !isNewerRead(locked, verifiedAt)) { + throw new OrchestrationError('conflict', SUBSCRIPTION_UNCONFIRMED) + } } const CLAIM_NOT_FOUND = @@ -120,9 +118,7 @@ export const readAwsMarketplaceClaim: OperationUseCase< requireProductCode() if (!input.claimToken) return { status: 'missing' } const license = await findLicenseByClaimHash(hashClaimToken(input.claimToken)) - if (!license) return { status: 'missing' } - if (!isClaimOpen(license, new Date())) return { status: 'expired' } - if (license.status === 'inactive') return { status: 'inactive' } + if (!license || !isClaimOpen(license, new Date())) return { status: 'missing' } if (license.status === 'pending') { return { status: 'pending', license: claimedLicenseView(license) } } diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts index c809040a2cf..3704d38dc16 100644 --- a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -92,6 +92,7 @@ const NOTICE_EVENTS = [AWS_MARKETPLACE_TEAM_NOTICE_EVENT, AWS_MARKETPLACE_BUYER_ const DAY_MS = 24 * 60 * 60 * 1000 const TEAM = /^enterprise@/ const BUYER = /^buyer@example\.test$/ +const CANCEL_CALL = /POST \/api\/v1\/admin\/dashboard\/organizations\/\/cancellation/ /** What GetEntitlements answers for each license ARN; an Error is thrown instead. */ const entitlementsByLicense = new Map() @@ -607,7 +608,7 @@ describe('reading a claim', () => { const organizationId = await createOrganization([{ userId: adminId, role: 'admin' }]) const open = await createClaimedLicense({ customerAwsAccountId: '123456781234' }) const linked = await createClaimedLicense({ organizationId }) - const lapsed = await createClaimedLicense({ status: 'inactive' }) + const expired = await createClaimedLicense({ claimExpiresAt: new Date(Date.now() - 1000) }) const read = (userId: string, claimToken: string) => readAwsMarketplaceClaim.execute({ principal: createSessionPrincipal({ userId }), @@ -620,7 +621,7 @@ describe('reading a claim', () => { }) expect(await read(adminId, linked.token)).toEqual({ status: 'linked', organizationId }) expect(await read(strangerId, linked.token)).toEqual({ status: 'linked', organizationId: null }) - expect(await read(strangerId, lapsed.token)).toEqual({ status: 'inactive' }) + expect(await read(strangerId, expired.token)).toEqual({ status: 'missing' }) }) }) @@ -848,19 +849,19 @@ describe.each([ ]) }) - it('a claim whose read predates a newer inactive verification', async () => { + it('an active read older than a newer read that still says pending', async () => { const license = await createClaimedLicense({ - status: 'inactive', + status: 'pending', lastVerifiedAt: new Date(Date.now() + 5 * 60 * 1000), }) entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) const error = await rejection(run({ licenseId: license.id, claimToken: license.token })) - expect(error).toMatchObject({ code: 'conflict' }) + expect(error).toMatchObject({ code: 'conflict', message: expect.stringMatching(/confirming/) }) expect(await readLicense(license.licenseArn)).toMatchObject({ linkedByUserId: null, - status: 'inactive', + status: 'pending', claimTokenHash: hashClaimToken(license.token), }) expect(await emailsTo(/./)).toEqual([]) @@ -874,7 +875,11 @@ describe('registering a claimed license to an account', () => { buyerId = await createUser() }) - function registerToAccount(input: { licenseId: string; claimToken: string | null }) { + function registerToAccount(input: { + licenseId: string + claimToken: string | null + contactEmail?: string + }) { return registerAwsMarketplaceLicenseToAccount.execute({ principal: createSessionPrincipal({ userId: buyerId }), input: { ...CONTACT, ...input }, @@ -910,6 +915,32 @@ describe('registering a claimed license to an account', () => { }) }) + it('tells the team when a re-registration changes the contact details, and only then', async () => { + const license = await createClaimedLicense() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + await registerToAccount({ licenseId: license.id, claimToken: license.token }) + const contactNotices = async () => + (await emailsTo(TEAM)).filter((email) => /Buyer contact/.test(email.subject)) + const reclaim = async (token: string) => { + resolvesTo(license.licenseArn) + return claimTokenOf(await registration(`x-amzn-marketplace-token=${token}`)) + } + + await registerToAccount({ licenseId: license.id, claimToken: await reclaim('same-contact') }) + expect(await contactNotices()).toEqual([]) + + await registerToAccount({ + licenseId: license.id, + claimToken: await reclaim('new-contact'), + contactEmail: 'new-contact@example.test', + }) + + expect(await contactNotices()).toEqual([ + expect.objectContaining({ text: expect.stringMatching(/new-contact@example\.test/) }), + ]) + expect((await readLicense(license.licenseArn))?.contactEmail).toBe('new-contact@example.test') + }) + it('refuses a license already linked to an organization', async () => { const ownerId = await createUser() const organizationId = await createOrganization([{ userId: ownerId, role: 'owner' }]) @@ -1006,7 +1037,32 @@ describe('reconcileAwsMarketplaceLicenses', () => { expect.stringMatching(/active → inactive/), expect.stringMatching(/active → inactive/), ]) - expect(notices[0].text).toMatch(/DELETE \/api\/v1\/admin\/subscriptions/) + }) + + it('gives the cancel instruction and its required body only when an organization holds the lapsed license', async () => { + const organizationId = await createOrganization([{ userId: await createUser(), role: 'owner' }]) + const linked = await insertLicense({ organizationId }) + const unlinked = await insertLicense() + entitlementsByLicense.set(linked.licenseArn, [lapsedEntitlement()]) + entitlementsByLicense.set(unlinked.licenseArn, [lapsedEntitlement()]) + deliverOnce( + [linked, unlinked].map(({ licenseArn }) => ({ + 'detail-type': 'License Deprovisioned - Manufacturer', + detail: { license: { arn: licenseArn }, product: { code: PRODUCT } }, + })) + ) + + await reconcileAwsMarketplaceLicenses() + + const notices = await emailsTo(TEAM) + const noticeOf = ({ id }: { id: string }) => + notices.find((email) => email.text.includes(`License ID: ${id}`))?.text + expect(noticeOf(linked)).toMatch(CANCEL_CALL) + expect(noticeOf(linked)).toMatch(/\(operationId: a new UUID, timing: immediate\)/) + expect(noticeOf(unlinked)).not.toMatch(CANCEL_CALL) + expect(noticeOf(unlinked)).toMatch( + /No organization holds this license\. If you provisioned one for this buyer, cancel it there\./ + ) }) it('sends the team nothing when a re-verify finds the same terms', async () => { @@ -1057,6 +1113,34 @@ describe('reconcileAwsMarketplaceLicenses', () => { expect(row?.id).toBe(id) }) + it('tells the team once when the License Updated event reaches a license already taken', async () => { + const organizationId = await createOrganization([{ userId: await createUser(), role: 'owner' }]) + const linked = await insertLicense({ organizationId }) + const registered = await insertLicense({ linkedByUserId: await createUser() }) + const unowned = await insertLicense() + for (const { licenseArn } of [linked, registered, unowned]) { + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + } + deliverOnce([ + licenseUpdated(linked.licenseArn), + licenseUpdated(registered.licenseArn), + licenseUpdated(unowned.licenseArn), + licenseUpdated(linked.licenseArn), + ]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ applied: 4, retained: 0 }) + + const received = (await emailsTo(TEAM)).filter((email) => + /License Updated event received/.test(email.subject) + ) + const announces = ({ id }: { id: string }) => + received.filter((email) => email.text.includes(`License ID: ${id}`)).length + expect(received).toHaveLength(2) + expect(announces(linked)).toBe(1) + expect(announces(registered)).toBe(1) + expect(announces(unowned)).toBe(0) + }) + it('keeps a Deprovisioned event for redelivery while AWS still reports the license current', async () => { const { licenseArn } = await insertLicense() entitlementsByLicense.set(licenseArn, [currentEntitlement()]) @@ -1080,12 +1164,31 @@ describe('reconcileAwsMarketplaceLicenses', () => { expect(deleted).toEqual([]) }) - it('skips an unknown license AWS describes nothing for, and warns rather than applying', async () => { + it('keeps a License Updated event for redelivery while AWS describes nothing for an unknown license', async () => { const licenseArn = uniqueArn() entitlementsByLicense.set(licenseArn, []) const deleted = deliverOnce([licenseUpdated(licenseArn)]) - expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ skipped: 1, applied: 0 }) + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ + retained: 1, + skipped: 0, + applied: 0, + }) + expect(deleted).toEqual([]) + expect(await readLicense(licenseArn)).toBeUndefined() + }) + + it('skips and deletes a Deprovisioned event for a license it never recorded', async () => { + const licenseArn = uniqueArn() + entitlementsByLicense.set(licenseArn, []) + const deleted = deliverOnce([ + { + 'detail-type': 'License Deprovisioned - Manufacturer', + detail: { license: { arn: licenseArn }, product: { code: PRODUCT } }, + }, + ]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ skipped: 1, retained: 0 }) expect(deleted).toEqual(['r-0']) expect(await readLicense(licenseArn)).toBeUndefined() }) @@ -1408,7 +1511,7 @@ describe('a replaced agreement', () => { const [notice] = await emailsTo(TEAM) expect(notice.text).toMatch(/Do not cancel/) - expect(notice.text).not.toMatch(/DELETE \/api\/v1\/admin\/subscriptions/) + expect(notice.text).not.toMatch(CANCEL_CALL) }) }) diff --git a/apps/sim/lib/billing/aws-marketplace/notice-events.ts b/apps/sim/lib/billing/aws-marketplace/notice-events.ts index b0589e548c1..789d7461a31 100644 --- a/apps/sim/lib/billing/aws-marketplace/notice-events.ts +++ b/apps/sim/lib/billing/aws-marketplace/notice-events.ts @@ -13,6 +13,7 @@ export type AwsMarketplaceTeamNotice = | { kind: 'new-license'; licenseId: string; source: 'registration' | 'event' } | { kind: 'taken'; licenseId: string; userId: string } | { kind: 'contact'; licenseId: string } + | { kind: 'license-event-received'; licenseId: string } | { kind: 'terms-changed' licenseId: string diff --git a/apps/sim/lib/billing/aws-marketplace/notifications.ts b/apps/sim/lib/billing/aws-marketplace/notifications.ts index b57bc8bf23e..e026f430960 100644 --- a/apps/sim/lib/billing/aws-marketplace/notifications.ts +++ b/apps/sim/lib/billing/aws-marketplace/notifications.ts @@ -31,7 +31,9 @@ const PROVISION = const LINK = 'Then link the license with POST /api/v1/admin/dashboard/aws-marketplace-licenses (licenseId, organizationId).' const CANCEL = - 'Cancel the Enterprise subscription with POST /api/v1/admin/dashboard/organizations//cancellation (timing: immediate), unless a replacement notice for this account follows. The organization goes dormant and is not deleted.' + 'Cancel the Enterprise subscription with POST /api/v1/admin/dashboard/organizations//cancellation (operationId: a new UUID, timing: immediate), unless a replacement notice for this account follows. The organization goes dormant and is not deleted.' +const NO_ORGANIZATION_TO_CANCEL = + 'No organization holds this license. If you provisioned one for this buyer, cancel it there.' const UNCONFIRMED = 'Unconfirmed: AWS has returned no entitlement for this license yet. Do not provision it until its status is active.' @@ -82,7 +84,9 @@ function nextStepForTerms( if (after.status === 'inactive' && license.renewedAgreementId) { return `Agreement ${license.renewedAgreementId} replaced this license's agreement. Do not cancel on this notice: AWS grants the replacement its own license, so check that one.` } - if (after.status === 'inactive') return CANCEL + if (after.status === 'inactive') { + return license.organizationId ? CANCEL : NO_ORGANIZATION_TO_CANCEL + } if (after.status === 'active' && !license.organizationId) return `${PROVISION} ${LINK}` return 'Update the Enterprise provisioning to match. Never send a Stripe invoice for a Marketplace customer: AWS bills them.' } @@ -131,15 +135,29 @@ Organizations linked to other licenses of this account: ${others}`, replyTo: license.contactEmail ?? userEmail, } } - case 'contact': + case 'contact': { + const confirming = license.status === 'pending' return { subject: `Buyer contact: ${account}`, - opening: `A buyer left contact details while AWS confirms their subscription.${ - license.status === 'pending' ? `\n${UNCONFIRMED}` : '' - }`, - next: 'Nothing to provision on this notice; a separate one follows when AWS confirms it.', + opening: confirming + ? `A buyer left contact details while AWS confirms their subscription.\n${UNCONFIRMED}` + : 'A buyer changed their contact details.', + next: confirming + ? 'Nothing to provision on this notice; a separate one follows when AWS confirms it.' + : 'Nothing to provision on this notice. Use these contact details from now on.', replyTo: license.contactEmail, } + } + case 'license-event-received': + return { + subject: `License Updated event received: ${account}`, + opening: + 'AWS sent the License Updated event for a license already taken. AWS says not to activate a subscription until it arrives.', + next: + license.status === 'active' + ? 'Provisioning that was waiting for this event can go ahead, following the earlier notice.' + : 'Do not provision it until its status is active.', + } case 'terms-changed': return { subject: `License ${notice.before.status} → ${notice.after.status}: ${account}`, diff --git a/apps/sim/lib/billing/aws-marketplace/reconcile.ts b/apps/sim/lib/billing/aws-marketplace/reconcile.ts index 61dd0c147f9..fcfd1700537 100644 --- a/apps/sim/lib/billing/aws-marketplace/reconcile.ts +++ b/apps/sim/lib/billing/aws-marketplace/reconcile.ts @@ -67,7 +67,7 @@ interface AwsMarketplaceReconcileSummary { received: number applied: number discarded: number - /** Named a license that has no row and that AWS described nothing to create one from. */ + /** A Deprovisioned event for a license that has no row, so nothing is left to record. */ skipped: number retained: number reverified: number @@ -275,9 +275,13 @@ async function applyLicenseEvent( licenseEventReceivedAt: event.deprovisioned ? undefined : new Date(), }) if (outcome.kind === 'skipped') { - logger.warn('Skipping an AWS Marketplace event for a license AWS does not describe', { - messageId, - }) + if (!event.deprovisioned) { + logger.warn('Leaving a License Updated event for a license AWS does not describe yet', { + messageId, + }) + return 'retained' + } + logger.warn('Skipping a License Deprovisioned event for a license with no row', { messageId }) return 'skipped' } if (event.deprovisioned && summary.active) { diff --git a/apps/sim/lib/billing/aws-marketplace/repository.ts b/apps/sim/lib/billing/aws-marketplace/repository.ts index bdfe87b74f1..10e8b56d8f8 100644 --- a/apps/sim/lib/billing/aws-marketplace/repository.ts +++ b/apps/sim/lib/billing/aws-marketplace/repository.ts @@ -255,7 +255,8 @@ export type LicenseVerificationOutcome = /** * Applies a verification read. A license AWS describes with an entitlement but nobody registered is * recorded (a renewal gets a new ARN), carrying on a renewed license's owner when there is one; one - * AWS no longer describes is only updated if it exists. + * AWS no longer describes is only updated if it exists. The first License Updated event for a + * license already taken tells the team, whose earlier notice said to wait for it. */ export async function recordLicenseVerification(input: { licenseArn: string @@ -298,7 +299,19 @@ export async function recordLicenseVerification(input: { ...agreementFields(locked, seen), ...stamp, }) - return { kind: 'updated', license: await inheritFromRenewedLicense(tx, written) } + const license = await inheritFromRenewedLicense(tx, written) + if ( + licenseEventReceivedAt && + !locked.licenseEventReceivedAt && + (license.organizationId || license.linkedByUserId) + ) { + await enqueueTeamNotice( + tx, + { kind: 'license-event-received', licenseId: license.id }, + `aws-marketplace-license-event-${license.id}` + ) + } + return { kind: 'updated', license } }) } @@ -482,18 +495,27 @@ export async function lockLicense( return row ?? null } +interface BuyerContact { + email: string + companyName: string | null +} + +function contactChanged(locked: AwsMarketplaceLicenseRecord, contact: BuyerContact): boolean { + return locked.contactEmail !== contact.email || locked.companyName !== contact.companyName +} + /** * Consumes the claim under the caller's row lock, taking the license when `link` is given: for an * organization, or for an account alone when its organization is null. A take tells the team and * confirms to the buyer in the same transaction. A license the same party already holds keeps its - * link and announces nothing. + * link and announces only new contact details. */ export async function consumeLicenseClaim( tx: DbTransaction, locked: AwsMarketplaceLicenseRecord, input: { link: { organizationId: string | null; userId: string } | null - contact: { email: string; companyName: string | null } + contact: BuyerContact verification: LicenseVerification } ): Promise { @@ -510,6 +532,8 @@ export async function consumeLicenseClaim( if (input.link) { await enqueueTeamNotice(tx, { kind: 'taken', licenseId: license.id, userId: input.link.userId }) await enqueueBuyerNotice(tx, { licenseId: license.id }) + } else if (contactChanged(locked, input.contact)) { + await enqueueTeamNotice(tx, { kind: 'contact', licenseId: license.id }) } return license } @@ -522,12 +546,11 @@ export async function saveLicenseContact( tx: DbTransaction, locked: AwsMarketplaceLicenseRecord, input: { - contact: { email: string; companyName: string | null } + contact: BuyerContact verification: LicenseVerification } ): Promise { - const changed = - locked.contactEmail !== input.contact.email || locked.companyName !== input.contact.companyName + const changed = contactChanged(locked, input.contact) const license = await applyVerification(tx, locked, input.verification, { contactEmail: input.contact.email, companyName: input.contact.companyName, @@ -580,7 +603,10 @@ export async function listAccountLicenses(userId: string) { .orderBy(asc(awsMarketplaceLicense.createdAt)) } -/** The licenses waiting for the team to link them, newest first, for the admin dashboard. */ +/** + * Licenses with no organization, newest first, for the admin dashboard: those waiting for the team + * to link them, and inactive ones that never will be. + */ export async function listUnlinkedLicenses(page: { limit: number; offset: number }) { return db .select() From 3277b99e67e15912631024beedc993df6e9b0774 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 21:02:10 -0800 Subject: [PATCH 6/7] fix(billing): base staff instructions on current license state and scope account status A retried notice could tell staff to cancel a license that had since recovered, and a notice for a lapsed license could tell them to provision it, so next steps now read the license as it stands. A viewer's account license no longer labels an organization's Stripe billing as Marketplace-billed, and upgrade entry points stay hidden until the lookup settles. A buyer registered to their account sees status on the onboarding page itself, since they may not be able to open any billing page. Account license reads are keyed by viewer, reverify is scoped to the configured product, lapsed dimension ties resolve deterministically, and the admin routes honor the feature switch. --- .../aws-marketplace-licenses/route.ts | 3 + .../aws/aws-marketplace-onboarding.tsx | 112 ++++++++++++++---- .../settings/components/billing/billing.tsx | 4 +- apps/sim/hooks/queries/aws-marketplace.ts | 27 +++-- .../aws-marketplace/entitlements.test.ts | 9 ++ .../billing/aws-marketplace/entitlements.ts | 4 +- .../aws-marketplace/lifecycle.integration.ts | 56 +++++++++ .../billing/aws-marketplace/notifications.ts | 27 +++-- .../lib/billing/aws-marketplace/reconcile.ts | 2 +- .../lib/billing/aws-marketplace/repository.ts | 15 ++- 10 files changed, 204 insertions(+), 55 deletions(-) diff --git a/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts b/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts index f7cf237fc78..fb549dc5e7b 100644 --- a/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts +++ b/apps/sim/app/api/v1/admin/dashboard/aws-marketplace-licenses/route.ts @@ -9,6 +9,7 @@ import { linkAwsMarketplaceLicenseAsAdmin, listUnlinkedAwsMarketplaceLicenses, } from '@/lib/billing/aws-marketplace/application/admin' +import { getAwsMarketplaceProductCode } from '@/lib/billing/aws-marketplace/config' import { asOrchestrationError } from '@/lib/core/orchestration/types' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getAdminAuditActor } from '@/app/api/v1/admin/dashboard/actor' @@ -28,6 +29,7 @@ const logger = createLogger('AdminAwsMarketplaceLicensesAPI') /** The Marketplace licenses no organization holds yet. */ export const GET = withRouteHandler( withAdminAuth(async (request) => { + if (!getAwsMarketplaceProductCode()) return adminNotFoundResponse('AWS Marketplace') const parsed = await parseRequest( adminDashboardListAwsMarketplaceLicensesContract, request, @@ -54,6 +56,7 @@ export const GET = withRouteHandler( /** Links a Marketplace license to an organization once the team has provisioned it. */ export const POST = withRouteHandler( withAdminAuth(async (request) => { + if (!getAwsMarketplaceProductCode()) return adminNotFoundResponse('AWS Marketplace') const parsed = await parseRequest( adminDashboardLinkAwsMarketplaceLicenseContract, request, diff --git a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx index 2cbc0e8468d..a1ac7d78420 100644 --- a/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx +++ b/apps/sim/app/marketplace/aws/aws-marketplace-onboarding.tsx @@ -18,12 +18,13 @@ import { AWS_MARKETPLACE_SETUP_TURNAROUND, type AwsMarketplaceOnboardingError, } from '@/lib/billing/aws-marketplace/onboarding' -import { organizationRoutes, WORKSPACE_BILLING_PATH } from '@/lib/navigation/paths' +import { organizationRoutes } from '@/lib/navigation/paths' import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect' import { InviteLayout, InviteStatusCard } from '@/app/invite/components' import { useBrandConfig } from '@/ee/whitelabeling' import { useAwsMarketplaceClaim, + useAwsMarketplaceLicenses, useLinkAwsMarketplaceLicense, useRegisterAwsMarketplaceLicenseToAccount, useSaveAwsMarketplaceClaimContact, @@ -59,6 +60,12 @@ const REGISTRATION_ERROR_COPY: Record< /** AWS's manual-setup guidance: being set up, wait to be contacted, turnaround, contact. */ const BEING_SET_UP = `Your account is being set up. Please wait for us to contact you. We expect to finish ${AWS_MARKETPLACE_SETUP_TURNAROUND}.` +const LICENSE_STATUS_LABELS = { + active: 'Active', + pending: 'Confirming with AWS', + inactive: 'Inactive', +} as const + function authLink(path: '/login' | '/signup'): string { return buildAuthCrossLink(path, { callbackUrl: AWS_MARKETPLACE_ONBOARDING_PATH, @@ -80,6 +87,69 @@ function SupportContact({ supportEmail }: SupportContactProps) { ) } +/** Status for a buyer with no organization, who may not be able to open any billing page. */ +function AccountSubscriptionStatus() { + const { data: licenses } = useAwsMarketplaceLicenses(null) + if (!licenses?.length) return null + return ( +
    + {licenses.map((license) => ( +
  • + {LICENSE_STATUS_LABELS[license.status]} + {license.expiresAt && `, expires ${formatDate(new Date(license.expiresAt))}`} +
  • + ))} +
+ ) +} + +interface NoSetupInProgressProps { + supportContact: ReactNode + actions: Array<{ label: string; onClick: () => void }> +} + +/** A finished buyer who returns sees their account's status; anyone else, how to start. */ +function NoSetupInProgress({ supportContact, actions }: NoSetupInProgressProps) { + const { data: licenses, isPending } = useAwsMarketplaceLicenses(null) + if (isPending) { + return ( + + + + ) + } + if (licenses?.length) { + const underway = licenses.some((license) => license.status !== 'inactive') + return ( + + + + {supportContact} +
+ } + actions={actions} + /> + + ) + } + return ( + + + + ) +} + interface AwsMarketplaceOnboardingProps { registrationError: AwsMarketplaceOnboardingError | null registrationDisabled: boolean @@ -134,7 +204,11 @@ export function AwsMarketplaceOnboarding({ -

- - View subscription status - -

+ {organizationId ? ( +

+ + View subscription status + +

+ ) : ( + + )} {supportContact}
} @@ -200,17 +274,7 @@ export function AwsMarketplaceOnboarding({ const claim = claimQuery.data if (claim.status === 'missing') { - return ( - - - - ) + return } if (claim.status === 'linked') { diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx index 7514ebec4ed..d0fbd308b03 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/billing/billing.tsx @@ -135,7 +135,8 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps enabled: !isOrganizationScope, }) const billingOrganizationId = isOrganizationScope ? (organizationId ?? null) : null - const { billedThroughMarketplace } = useAwsMarketplaceBilling(billingOrganizationId) + const marketplaceBilling = useAwsMarketplaceBilling(billingOrganizationId) + const { billedThroughMarketplace } = marketplaceBilling const { data: organizationBillingData, @@ -521,6 +522,7 @@ export function Billing({ scope, organizationId, creditUsageHref }: BillingProps
{!subscription.isEnterprise && !billedThroughMarketplace && + !marketplaceBilling.isPending && (canExplorePlans && upgradeHref ? ( [...awsMarketplaceKeys.claims(), viewerId] as const, licenses: () => [...awsMarketplaceKeys.all, 'licenses'] as const, - /** An organization's licenses, or with null the viewer's own account's. */ - licenseList: (organizationId: string | null) => - [...awsMarketplaceKeys.licenses(), organizationId ?? ''] as const, + /** An organization's licenses, or one viewer's own account's: the session scopes that read. */ + licenseList: (scope: { organizationId: string } | { viewerId: string }) => + [...awsMarketplaceKeys.licenses(), scope] as const, } export function useAwsMarketplaceClaim(viewerId: string | null) { @@ -68,25 +69,25 @@ export function useSaveAwsMarketplaceClaimContact() { } /** - * Whether AWS Marketplace bills this organization or the viewer's own account: a pending or active - * license on either. Pending until both reads settle. + * Whether AWS Marketplace bills this scope: the organization, or with null the viewer's account. + * A failed read reports false, so a lookup error never blocks a customer who pays through Stripe. */ export function useAwsMarketplaceBilling(organizationId: string | null) { - const organizationLicenses = useAwsMarketplaceLicenses(organizationId) - const accountLicenses = useAwsMarketplaceLicenses(null) + const licenses = useAwsMarketplaceLicenses(organizationId) return { - isPending: organizationLicenses.isPending || accountLicenses.isPending, - billedThroughMarketplace: [ - ...(organizationLicenses.data ?? []), - ...(accountLicenses.data ?? []), - ].some((license) => license.status !== 'inactive'), + isPending: licenses.isPending, + billedThroughMarketplace: + licenses.data?.some((license) => license.status !== 'inactive') ?? false, } } /** The Marketplace subscriptions an organization holds, or with null the viewer's own account. */ export function useAwsMarketplaceLicenses(organizationId: string | null) { + const { data: session } = useSession() + const viewerId = session?.user?.id ?? '' return useQuery({ - queryKey: awsMarketplaceKeys.licenseList(organizationId), + queryKey: awsMarketplaceKeys.licenseList(organizationId ? { organizationId } : { viewerId }), + enabled: Boolean(organizationId || viewerId), queryFn: ({ signal }) => organizationId ? requestJson(listOrganizationAwsMarketplaceLicensesContract, { diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts index a297689a73f..56088443a8c 100644 --- a/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.test.ts @@ -60,6 +60,15 @@ describe('verifyLicense', () => { expect((await read(entitlement({ ExpirationDate: NOW }))).active).toBe(false) }) + it('reads the same lapsed dimension whichever order AWS returns ties in', async () => { + const expired = new Date('2026-01-01T00:00:00.000Z') + const alpha = entitlement({ Dimension: 'alpha', ExpirationDate: expired }) + const zeta = entitlement({ Dimension: 'zeta', ExpirationDate: expired }) + + expect((await read(alpha, zeta)).dimension).toBe('alpha') + expect((await read(zeta, alpha)).dimension).toBe('alpha') + }) + it('describes the current entitlement when another one has lapsed', async () => { const summary = await read( entitlement({ diff --git a/apps/sim/lib/billing/aws-marketplace/entitlements.ts b/apps/sim/lib/billing/aws-marketplace/entitlements.ts index b5d96fd1f29..0b8ef4e454c 100644 --- a/apps/sim/lib/billing/aws-marketplace/entitlements.ts +++ b/apps/sim/lib/billing/aws-marketplace/entitlements.ts @@ -38,7 +38,9 @@ function summarizeEntitlements( .filter((entitlement) => isCurrent(entitlement, now)) .sort((left, right) => compareStrings(left.Dimension ?? '', right.Dimension ?? '')) const lapsed = [...entitlements].sort( - (left, right) => (right.ExpirationDate?.getTime() ?? 0) - (left.ExpirationDate?.getTime() ?? 0) + (left, right) => + (right.ExpirationDate?.getTime() ?? 0) - (left.ExpirationDate?.getTime() ?? 0) || + compareStrings(left.Dimension ?? '', right.Dimension ?? '') ) const chosen = current[0] ?? lapsed[0] return { diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts index 3704d38dc16..86831f2aeec 100644 --- a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -1065,6 +1065,62 @@ describe('reconcileAwsMarketplaceLicenses', () => { ) }) + it('does not tell the team to cancel when a retried notice arrives after the license recovered', async () => { + const organizationId = await createOrganization([{ userId: await createUser(), role: 'owner' }]) + const license = await insertLicense({ organizationId }) + const event = { + 'detail-type': 'License Updated - Manufacturer', + detail: { license: { arn: license.licenseArn }, product: { code: PRODUCT } }, + } + entitlementsByLicense.set(license.licenseArn, [lapsedEntitlement()]) + deliverOnce([event]) + await reconcileAwsMarketplaceLicenses() + entitlementsByLicense.set(license.licenseArn, [currentEntitlement()]) + deliverOnce([event]) + await reconcileAwsMarketplaceLicenses() + + const notices = await emailsTo(TEAM) + + expect(notices.length).toBeGreaterThan(1) + for (const notice of notices) expect(notice.text).not.toMatch(CANCEL_CALL) + }) + + it('does not ask the team to provision a license that lapsed before setup', async () => { + const licenseArn = uniqueArn() + entitlementsByLicense.set(licenseArn, [lapsedEntitlement()]) + deliverOnce([ + { + 'detail-type': 'License Updated - Manufacturer', + detail: { + license: { arn: licenseArn }, + product: { code: PRODUCT }, + acceptor: { accountId: ACCOUNT }, + }, + }, + ]) + + await reconcileAwsMarketplaceLicenses() + + const [notice] = await emailsTo(TEAM) + expect(notice.text).toMatch(/Do not provision this license/) + expect(notice.text).not.toMatch(/pausePaymentCollection/) + }) + + it('re-verifies only the configured product', async () => { + const longAgo = new Date(Date.now() - 3 * DAY_MS) + const other = await insertLicense({ + productCode: 'another-product', + lastVerifiedAt: longAgo, + lastVerificationAttemptAt: longAgo, + }) + entitlementsByLicense.set(other.licenseArn, [lapsedEntitlement()]) + queueSend.mockResolvedValue({ Messages: [] }) + + await reconcileAwsMarketplaceLicenses() + + expect((await readLicense(other.licenseArn))?.lastVerificationAttemptAt).toEqual(longAgo) + }) + it('sends the team nothing when a re-verify finds the same terms', async () => { const expiresAt = new Date(Date.now() + 30 * DAY_MS) const { licenseArn } = await insertLicense({ diff --git a/apps/sim/lib/billing/aws-marketplace/notifications.ts b/apps/sim/lib/billing/aws-marketplace/notifications.ts index e026f430960..81957e7dd3e 100644 --- a/apps/sim/lib/billing/aws-marketplace/notifications.ts +++ b/apps/sim/lib/billing/aws-marketplace/notifications.ts @@ -76,18 +76,18 @@ License Updated event: ${ }` } -/** What is left for the team to do about one license AWS has just changed. */ -function nextStepForTerms( - license: AwsMarketplaceLicenseRecord, - after: AwsMarketplaceNoticeTerms -): string { - if (after.status === 'inactive' && license.renewedAgreementId) { +/** + * What is left for the team to do about one license AWS has just changed. It reads the license as + * it stands now, because an outbox retry can deliver a notice after a newer change. + */ +function nextStepForTerms(license: AwsMarketplaceLicenseRecord): string { + if (license.status === 'inactive' && license.renewedAgreementId) { return `Agreement ${license.renewedAgreementId} replaced this license's agreement. Do not cancel on this notice: AWS grants the replacement its own license, so check that one.` } - if (after.status === 'inactive') { + if (license.status === 'inactive') { return license.organizationId ? CANCEL : NO_ORGANIZATION_TO_CANCEL } - if (after.status === 'active' && !license.organizationId) return `${PROVISION} ${LINK}` + if (license.status === 'active' && !license.organizationId) return `${PROVISION} ${LINK}` return 'Update the Enterprise provisioning to match. Never send a Stripe invoice for a Marketplace customer: AWS bills them.' } @@ -117,7 +117,9 @@ async function teamNoticeCopy( Organizations linked to other licenses of this account: ${others}`, next: license.organizationId ? 'It carries on the organization of the license it replaces. Keep that organization provisioned to the terms above.' - : `${PROVISION} ${LINK}`, + : license.status === 'active' + ? `${PROVISION} ${LINK}` + : 'Do not provision this license. A notice follows if AWS reports it active.', } } case 'taken': { @@ -164,7 +166,7 @@ Organizations linked to other licenses of this account: ${others}`, opening: `AWS Marketplace changed what this license grants. Before: ${formatTerms(notice.before)} After: ${formatTerms(notice.after)}`, - next: nextStepForTerms(license, notice.after), + next: nextStepForTerms(license), } case 'renewed': return { @@ -224,8 +226,9 @@ Questions? Contact ${brand.supportEmail}. } /** - * Sends one rendered notice. A failure throws, so the outbox retries it with backoff; the - * checkpoint keeps a crash between the send and the completion from sending it twice. + * Sends one rendered notice. A failure throws, so the outbox retries it with backoff. Delivery is + * at least once: the checkpoint narrows the window for a repeat but a crash right after the send + * can still repeat it, which a person reading these emails can absorb. */ function noticeHandler( render: (notice: T) => Promise diff --git a/apps/sim/lib/billing/aws-marketplace/reconcile.ts b/apps/sim/lib/billing/aws-marketplace/reconcile.ts index fcfd1700537..02d784ac0c3 100644 --- a/apps/sim/lib/billing/aws-marketplace/reconcile.ts +++ b/apps/sim/lib/billing/aws-marketplace/reconcile.ts @@ -408,7 +408,7 @@ async function reverifyStaleLicenses( fits: BudgetCheck ): Promise { const cutoff = new Date(Date.now() - REVERIFY_AFTER_MS) - const licenseArns = await listLicensesDueForReverify(cutoff, REVERIFY_BATCH_SIZE) + const licenseArns = await listLicensesDueForReverify(productCode, cutoff, REVERIFY_BATCH_SIZE) for (const licenseArn of licenseArns) { if (!fits(LICENSE_READ_BUDGET_MS)) { summary.outOfBudget = true diff --git a/apps/sim/lib/billing/aws-marketplace/repository.ts b/apps/sim/lib/billing/aws-marketplace/repository.ts index 10e8b56d8f8..1b5d1425c1c 100644 --- a/apps/sim/lib/billing/aws-marketplace/repository.ts +++ b/apps/sim/lib/billing/aws-marketplace/repository.ts @@ -452,12 +452,21 @@ export async function listLicensesForAgreement( return rows.map((row) => row.licenseArn) } -/** The licenses longest since a verification attempt, up to `limit`, none attempted after `cutoff`. */ -export async function listLicensesDueForReverify(cutoff: Date, limit: number): Promise { +/** This product's licenses longest since an attempt, up to `limit`, none attempted after `cutoff`. */ +export async function listLicensesDueForReverify( + productCode: string, + cutoff: Date, + limit: number +): Promise { const rows = await db .select({ licenseArn: awsMarketplaceLicense.licenseArn }) .from(awsMarketplaceLicense) - .where(lt(awsMarketplaceLicense.lastVerificationAttemptAt, cutoff)) + .where( + and( + eq(awsMarketplaceLicense.productCode, productCode), + lt(awsMarketplaceLicense.lastVerificationAttemptAt, cutoff) + ) + ) .orderBy(asc(awsMarketplaceLicense.lastVerificationAttemptAt)) .limit(limit) return rows.map((row) => row.licenseArn) From e2027bd692f9dc5a3fb6cefd2aaa25aa8bd4d542 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 5 Nov 2026 21:59:51 -0800 Subject: [PATCH 7/7] fix(billing): keep ended agreements queued until AWS agrees and bound each message An Agreement Ended event can arrive before the entitlement read reflects the end, and deleting it left the license current for up to a day, so the message now stays queued until AWS reports the license ended. The run budget is checked before each message, since one ended agreement can read many licenses, and messages not started stay queued for the next run. Removes a billing redirect nothing uses any more, and stops one test depending on the order of same-transaction notices. --- apps/sim/app/workspace/page.tsx | 10 +-- .../aws-marketplace/lifecycle.integration.ts | 61 ++++++++++++++++--- .../lib/billing/aws-marketplace/reconcile.ts | 17 ++++-- apps/sim/lib/navigation/paths.ts | 3 - 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/apps/sim/app/workspace/page.tsx b/apps/sim/app/workspace/page.tsx index fbc234eecfb..2013b453fcb 100644 --- a/apps/sim/app/workspace/page.tsx +++ b/apps/sim/app/workspace/page.tsx @@ -132,9 +132,7 @@ export default function WorkspacePage() { ? buildUpgradeHref(id, isUpgradeReason(rawReason) ? rawReason : undefined) : redirectTarget === 'settings' ? `/workspace/${id}/settings/general` - : redirectTarget === 'billing' - ? `/workspace/${id}/settings/billing` - : `/workspace/${id}` + : `/workspace/${id}` const { workspaces, lastActiveWorkspaceId, creationPolicy, forwardOrganizationId } = data @@ -143,11 +141,7 @@ export default function WorkspacePage() { hasRedirectedRef.current = true const routes = organizationRoutes(forwardOrganizationId) router.replace( - redirectTarget === 'settings' - ? routes.settingsSection('general') - : redirectTarget === 'billing' - ? routes.settingsSection('billing') - : routes.home + redirectTarget === 'settings' ? routes.settingsSection('general') : routes.home ) return } diff --git a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts index 86831f2aeec..db0d1d85e7c 100644 --- a/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts +++ b/apps/sim/lib/billing/aws-marketplace/lifecycle.integration.ts @@ -767,13 +767,16 @@ describe('linking a claimed license', () => { companyName: CONTACT.companyName, }) const team = await emailsTo(TEAM) - expect(team.map((email) => email.subject)).toEqual([ - expect.stringMatching(/active → active/), - expect.stringMatching(/Linked to an organization/), - ]) - expect(team[1].text).toMatch(/Company: Example Co/) - expect(team[1].text).toMatch(/pausePaymentCollection: true/) - expect(team[1].text).toMatch(/Never send a Stripe invoice/) + expect(team.map((email) => email.subject)).toEqual( + expect.arrayContaining([ + expect.stringMatching(/active → active/), + expect.stringMatching(/Linked to an organization/), + ]) + ) + const linkNotice = team.find((email) => /Linked to an organization/.test(email.subject)) + expect(linkNotice?.text).toMatch(/Company: Example Co/) + expect(linkNotice?.text).toMatch(/pausePaymentCollection: true/) + expect(linkNotice?.text).toMatch(/Never send a Stripe invoice/) expect(await emailsTo(BUYER)).toEqual([ expect.objectContaining({ text: expect.stringMatching( @@ -1307,6 +1310,35 @@ describe('reconcileAwsMarketplaceLicenses', () => { } }) + it('leaves messages it has not started queued once the run budget cannot fit another', async () => { + vi.useFakeTimers({ toFake: ['Date'] }) + try { + const licenses = await Promise.all([insertLicense(), insertLicense()]) + for (const { licenseArn } of licenses) { + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + } + const deleted = deliverOnce( + licenses.map(({ licenseArn }) => ({ + 'detail-type': 'License Updated - Manufacturer', + detail: { license: { arn: licenseArn }, product: { code: PRODUCT } }, + })) + ) + const receive = queueSend.getMockImplementation() + queueSend.mockImplementation(async (command: { kind: string }) => { + const answer = await receive?.(command) + if (command.kind === 'receive') vi.setSystemTime(Date.now() + 6 * 60_000) + return answer + }) + + const summary = await reconcileAwsMarketplaceLicenses() + + expect(summary).toMatchObject({ received: 0, outOfBudget: true }) + expect(deleted).toEqual([]) + } finally { + vi.useRealTimers() + } + }) + it('keeps licenses AWS keeps refusing from starving the rest of the re-verify', async () => { const failing = Array.from({ length: 50 }, () => uniqueArn()) const healthyArn = uniqueArn() @@ -1370,6 +1402,21 @@ describe('agreement events', () => { ]) }) + it('keeps an ended agreement queued while AWS still reports its license current', async () => { + const agreementId = uniqueAgreement() + const { licenseArn } = await insertLicense({ agreementId }) + entitlementsByLicense.set(licenseArn, [currentEntitlement()]) + const deleted = deliverOnce([ + { + 'detail-type': 'Purchase Agreement Ended - Proposer', + detail: { agreement: { id: agreementId } }, + }, + ]) + + expect(await reconcileAwsMarketplaceLicenses()).toMatchObject({ retained: 1, applied: 0 }) + expect(deleted).toEqual([]) + }) + it('reads every license of an ended agreement even when one fails, and keeps the message', async () => { const agreementId = uniqueAgreement() const failing = await insertLicense({ agreementId, lastVerificationAttemptAt: new Date(0) }) diff --git a/apps/sim/lib/billing/aws-marketplace/reconcile.ts b/apps/sim/lib/billing/aws-marketplace/reconcile.ts index 02d784ac0c3..5c9a8dc6fea 100644 --- a/apps/sim/lib/billing/aws-marketplace/reconcile.ts +++ b/apps/sim/lib/billing/aws-marketplace/reconcile.ts @@ -210,9 +210,9 @@ async function extendVisibility( /** * Each license gets its own read bound and commits on its own, so one that fails or runs out of - * time costs no other's progress. The message stays queued until all of them have been read, and - * a redelivery starts with those least recently attempted. An agreement ID with no license is - * normal: the seller account has other products. + * time costs no other's progress. The message stays queued until AWS reports all of them ended, + * since a read can lag the event, and a redelivery starts with those least recently attempted. An + * agreement ID with no license is normal: the seller account has other products. */ async function applyAgreementEnded( context: DrainContext, @@ -233,7 +233,11 @@ async function applyAgreementEnded( } if (index > 0) await extendVisibility(context, message, messageId) try { - await reverifyLicense(context.productCode, licenseArn, { ended: true }) + const { summary } = await reverifyLicense(context.productCode, licenseArn, { ended: true }) + if (summary.active) { + incomplete = true + logger.info('AWS still reports a license of an ended agreement current', { messageId }) + } } catch (error) { if (isInvalidEntitlementRequest(error)) { logger.warn('The Entitlement Service rejected a license of an ended agreement', { @@ -384,6 +388,11 @@ async function drainEvents(context: DrainContext): Promise { if (messages.length === 0) return for (const message of messages) { + // An ended agreement can read many licenses; what is not started stays queued for the next run. + if (!fits(LICENSE_READ_BUDGET_MS)) { + summary.outOfBudget = true + return + } summary.received++ const outcome = await handleMessage(context, message) summary[outcome]++ diff --git a/apps/sim/lib/navigation/paths.ts b/apps/sim/lib/navigation/paths.ts index fe511534675..18f18ae0afd 100644 --- a/apps/sim/lib/navigation/paths.ts +++ b/apps/sim/lib/navigation/paths.ts @@ -25,9 +25,6 @@ export const WORKSPACES_PATH = '/workspace' /** Opens full settings in the viewer's most recent accessible workspace. */ export const WORKSPACE_SETTINGS_PATH = `${WORKSPACES_PATH}?redirect=settings` -/** Opens billing settings in the viewer's most recent accessible workspace. */ -export const WORKSPACE_BILLING_PATH = `${WORKSPACES_PATH}?redirect=billing` - /** Root of the organization surface; `/o` alone resolves like {@link APP_ENTRY_PATH}. */ const ORGANIZATIONS_PATH = '/o'