From 0e2c31bf60df08fc0273255ad4ef7d7769752fab Mon Sep 17 00:00:00 2001 From: Matt Linkous Date: Fri, 19 Dec 2025 18:42:10 -0600 Subject: [PATCH] feat: Stripe Sync Engine integration (#41317) * add initial installation flow of stripe sync engine * update docs link * Add supabase_vault extension dep * Add stripe logo to sync engine integration * Move overview content to bottom of integration pages * Add sync state to stripe sync page * only check sync state if stripe integration is installed * Use proper stripe-sync package and setup flows * Improve sync engine installation ux * Remove unused hardcoded dep * Add alpha status to stripe sync engine integration * fix typo * run format * fix types * Rename the stripe-sync path to remove the 'integration". The path needs to have BASE_PATH to work on prod. * Design tidy up (#41337) UI tidy up * update to latest sync engine package * Add stripe key verification * Remove noop try/catch * Add integration isntallation telelemtry * Add basic settings page * Address coderabbit comments * remove unused dep * Remove state setting on render * s/description/comment * Cleanup settings screen UI * Improve settings screen design * update schema test snapshot * Use latest stripe-sync-engine package * Update repo url to new official location * revert marketing change * Update stripe sync engine package * Add link to table from overview page * Add feature flag and improve telemetry * Fix missing useMemo dep * add uninstall telemetry note --------- Co-authored-by: Ivan Vasilov Co-authored-by: Saxon Fletcher --- .../Integration/BuildBySection.tsx | 11 +- .../Integration/IntegrationOverviewTab.tsx | 105 +-- .../Integration/MarkdownContent.tsx | 10 +- .../Landing/Integrations.constants.tsx | 69 +- .../Landing/useInstalledIntegrations.tsx | 48 +- .../StripeSyncEngine/InstallationOverview.tsx | 403 +++++++++++ .../StripeSyncChangesCard.tsx | 72 ++ .../StripeSyncSettingsPage.tsx | 187 +++++ .../data/database-integrations/stripe/keys.ts | 6 + .../stripe/stripe-sync-install-mutation.ts | 91 +++ .../stripe/stripe-sync-uninstall-mutation.ts | 88 +++ .../stripe/sync-state-query.ts | 64 ++ apps/studio/package.json | 1 + .../pages/api/integrations/stripe-sync.ts | 141 ++++ .../integrations/[id]/[pageId]/index.tsx | 7 +- .../project/[ref]/integrations/index.tsx | 17 +- apps/studio/proxy.ts | 1 + .../stripe_sync_engine/overview.md | 3 + packages/common/telemetry-constants.ts | 97 +++ packages/pg-meta/src/pg-meta-schemas.ts | 1 + packages/pg-meta/src/sql/schemas.ts | 3 +- packages/pg-meta/test/schemas.test.ts | 15 +- pnpm-lock.yaml | 640 +++++++++++++++++- pnpm-workspace.yaml | 1 + 24 files changed, 2006 insertions(+), 75 deletions(-) create mode 100644 apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx create mode 100644 apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx create mode 100644 apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx create mode 100644 apps/studio/data/database-integrations/stripe/keys.ts create mode 100644 apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts create mode 100644 apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts create mode 100644 apps/studio/data/database-integrations/stripe/sync-state-query.ts create mode 100644 apps/studio/pages/api/integrations/stripe-sync.ts create mode 100644 apps/studio/static-data/integrations/stripe_sync_engine/overview.md diff --git a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx index f2e84a6f645..31cef5ce34a 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx @@ -1,16 +1,17 @@ import { Book } from 'lucide-react' import Link from 'next/link' -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' +import { ComponentPropsWithoutRef, ElementRef, forwardRef, ReactNode } from 'react' import { cn } from 'ui' import { IntegrationDefinition } from '../Landing/Integrations.constants' interface BuiltBySectionProps extends ComponentPropsWithoutRef<'div'> { integration: IntegrationDefinition + status?: string | ReactNode } export const BuiltBySection = forwardRef, BuiltBySectionProps>( - ({ integration, className, ...props }, ref) => { + ({ integration, status, className, ...props }, ref) => { const { docsUrl } = integration const { name, websiteUrl } = integration?.author ?? {} @@ -22,6 +23,12 @@ export const BuiltBySection = forwardRef, BuiltBySectionProps> className={cn('flex flex-wrap items-center gap-8 md:gap-10 px-4 md:px-10', className)} {...props} > + {status && ( +
+
STATUS
+
{status}
+
+ )} {name && (
BUILT BY
diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx index 88a3e09f160..78c379c1c82 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx @@ -1,11 +1,9 @@ -import { useRouter } from 'next/router' import { PropsWithChildren, ReactNode } from 'react' import { useParams } from 'common' -import { Markdown } from 'components/interfaces/Markdown' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Alert_Shadcn_, AlertDescription_Shadcn_, Badge, Separator } from 'ui' +import { Badge, Card, CardContent, cn, Separator } from 'ui' import { INTEGRATIONS } from '../Landing/Integrations.constants' import { BuiltBySection } from './BuildBySection' import { MarkdownContent } from './MarkdownContent' @@ -13,14 +11,17 @@ import { MissingExtensionAlert } from './MissingExtensionAlert' interface IntegrationOverviewTabProps { actions?: ReactNode + status?: string | ReactNode + alert?: ReactNode } export const IntegrationOverviewTab = ({ actions, + alert, + status, children, }: PropsWithChildren) => { const { id } = useParams() - const router = useRouter() const { data: project } = useSelectedProjectQuery() const integration = INTEGRATIONS.find((i) => i.id === id) @@ -47,43 +48,69 @@ export const IntegrationOverviewTab = ({ return (
- - {dependsOnExtension && ( -
- - - - Supabase - Postgres Module - - `\`${x}\``).join(', ')} - extension${integration.requiredExtensions.length > 1 ? 's' : ''} directly in your Postgres database. - ${hasToInstallExtensions && !hasMissingExtensions ? `Install ${integration.requiredExtensions.length > 1 ? 'these' : 'this'} database extension${integration.requiredExtensions.length > 1 ? 's' : ''} to use ${integration.name} in your project.` : ''} - `} - /> - - {hasMissingExtensions ? ( - integration.missingExtensionsAlert - ) : ( -
- {installableExtensions.map((extension) => ( - - ))} -
- )} -
-
-
- )} - {!!actions && !hasToInstallExtensions &&
{actions}
} + + {alert &&
{alert}
} + {dependsOnExtension && ( +
+

Required extensions

+ + +
    + {(integration.requiredExtensions ?? []).map((requiredExtension, idx) => { + const extension = (extensions ?? []).find((ext) => ext.name === requiredExtension) + const isInstalled = !!extension?.installed_version + const isLastRow = idx === (integration.requiredExtensions?.length ?? 0) - 1 + + return ( +
  • +
    + + {requiredExtension} + +
    + +
    + {extension ? ( + isInstalled ? ( + Installed + ) : ( + + ) + ) : ( + Unavailable + )} +
    +
  • + ) + })} +
+ + {hasMissingExtensions && ( +
{integration.missingExtensionsAlert}
+ )} +
+
+
+ )} + {!!actions && ( +
+ {actions} +
+ )} {children}
) diff --git a/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx b/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx index 24b4bec4d40..120d31fa4b7 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/MarkdownContent.tsx @@ -6,9 +6,15 @@ import { cn } from 'ui' const CHAR_LIMIT = 500 // Adjust this number as needed -export const MarkdownContent = ({ integrationId }: { integrationId: string }) => { +export const MarkdownContent = ({ + integrationId, + initiallyExpanded, +}: { + integrationId: string + initiallyExpanded?: boolean +}) => { const [content, setContent] = useState('') - const [isExpanded, setIsExpanded] = useState(false) + const [isExpanded, setIsExpanded] = useState(initiallyExpanded ?? false) useEffect(() => { import(`static-data/integrations/${integrationId}/overview.md`) diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index 71e865542a7..8dff7c8b059 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -1,4 +1,4 @@ -import { Clock5, Layers, Timer, Vault, Webhook } from 'lucide-react' +import { Clock5, Layers, Timer, Vault, Webhook, Receipt } from 'lucide-react' import dynamic from 'next/dynamic' import Image from 'next/image' import { ComponentType, ReactNode } from 'react' @@ -51,7 +51,7 @@ const authorSupabase = { websiteUrl: 'https://supabase.com', } -const supabaseIntegrations: IntegrationDefinition[] = [ +const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [ { id: 'queues', type: 'postgres_extension' as const, @@ -315,7 +315,7 @@ const supabaseIntegrations: IntegrationDefinition[] = [ }, ] as const -const wrapperIntegrations: IntegrationDefinition[] = WRAPPERS.map((w) => { +const WRAPPER_INTEGRATIONS: IntegrationDefinition[] = WRAPPERS.map((w) => { return { id: w.name, type: 'wrapper' as const, @@ -366,7 +366,66 @@ const wrapperIntegrations: IntegrationDefinition[] = WRAPPERS.map((w) => { } }) +const TEMPLATE_INTEGRATIONS: IntegrationDefinition[] = [ + { + id: 'stripe_sync_engine', + type: 'custom' as const, + requiredExtensions: ['pgmq', 'supabase_vault', 'pg_cron', 'pg_net'], + missingExtensionsAlert: , + name: `Stripe Sync Engine`, + status: 'alpha', + icon: ({ className, ...props } = {}) => ( + {'Stripe + ), + description: + 'Continuously sync your payments, customer, and other data from Stripe to your Postgres database', + docsUrl: 'https://github.com/stripe-experiments/sync-engine/', + author: { + name: 'Stripe', + websiteUrl: 'https://www.stripe.com', + }, + navigation: [ + { + route: 'overview', + label: 'Overview', + }, + { + route: 'settings', + label: 'Settings', + }, + ], + navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => { + switch (pageId) { + case 'overview': + return dynamic( + () => + import( + 'components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview' + ).then((mod) => mod.StripeSyncInstallationPage), + { loading: Loading } + ) + case 'settings': + return dynamic( + () => + import( + 'components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage' + ).then((mod) => mod.StripeSyncSettingsPage), + { loading: Loading } + ) + } + return null + }, + }, +] + export const INTEGRATIONS: IntegrationDefinition[] = [ - ...wrapperIntegrations, - ...supabaseIntegrations, + ...WRAPPER_INTEGRATIONS, + ...SUPABASE_INTEGRATIONS, + ...TEMPLATE_INTEGRATIONS, ] diff --git a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx index 1c1b2d34d92..14e92904770 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx @@ -3,23 +3,36 @@ import { useMemo } from 'react' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useFDWsQuery } from 'data/fdw/fdws-query' +import { useFlag } from 'common' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { EMPTY_ARR } from 'lib/void' +import { + INSTALLATION_INSTALLED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' import { wrapperMetaComparator } from '../Wrappers/Wrappers.utils' import { INTEGRATIONS } from './Integrations.constants' export const useInstalledIntegrations = () => { const { data: project } = useSelectedProjectQuery() const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) + const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration') const allIntegrations = useMemo(() => { - if (integrationsWrappers) { - return INTEGRATIONS - } else { - return INTEGRATIONS.filter((integration) => !integration.id.endsWith('_wrapper')) - } - }, [integrationsWrappers]) + return INTEGRATIONS.filter((integration) => { + if ( + !integrationsWrappers && + (integration.type === 'wrapper' || integration.id.endsWith('_wrapper')) + ) { + return false + } + if (!stripeSyncEnabled && integration.id === 'stripe_sync_engine') { + return false + } + return true + }) + }, [integrationsWrappers, stripeSyncEnabled]) const { data, @@ -58,16 +71,23 @@ export const useInstalledIntegrations = () => { const installedIntegrations = useMemo(() => { return allIntegrations - .filter((i) => { + .filter((integration) => { // special handling for supabase webhooks - if (i.id === 'webhooks') { + if (integration.id === 'webhooks') { return isHooksEnabled } - if (i.type === 'wrapper') { - return wrappers.find((w) => wrapperMetaComparator(i.meta, w)) + if (integration.id === 'stripe_sync_engine') { + const stripeSchema = schemas?.find(({ name }) => name === 'stripe') + return ( + !!stripeSchema?.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + !!stripeSchema.comment?.includes(INSTALLATION_INSTALLED_SUFFIX) + ) } - if (i.type === 'postgres_extension') { - return i.requiredExtensions.every((extName) => { + if (integration.type === 'wrapper') { + return wrappers.find((w) => wrapperMetaComparator(integration.meta, w)) + } + if (integration.type === 'postgres_extension') { + return integration.requiredExtensions.every((extName) => { const foundExtension = (extensions ?? []).find((ext) => ext.name === extName) return !!foundExtension?.installed_version }) @@ -75,13 +95,13 @@ export const useInstalledIntegrations = () => { return false }) .sort((a, b) => a.name.localeCompare(b.name)) - }, [wrappers, extensions, isHooksEnabled]) + }, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled]) // available integrations are all integrations that can be installed. If an integration can't be installed (needed // extensions are not available on this DB image), the UI will provide a tooltip explaining why. const availableIntegrations = useMemo( () => allIntegrations.sort((a, b) => a.name.localeCompare(b.name)), - [] + [allIntegrations] ) const error = fdwError || extensionsError || schemasError diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx new file mode 100644 index 00000000000..eb80da18dae --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx @@ -0,0 +1,403 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useStripeSyncInstallMutation } from 'data/database-integrations/stripe/stripe-sync-install-mutation' +import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' +import { useStripeSyncingState } from 'data/database-integrations/stripe/sync-state-query' +import { useSchemasQuery } from 'data/database/schemas-query' +import { formatRelative } from 'date-fns' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useTrack } from 'lib/telemetry/track' +import { AlertCircle, BadgeCheck, Check, ExternalLink, RefreshCwIcon } from 'lucide-react' +import Link from 'next/link' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useForm } from 'react-hook-form' +import { toast } from 'sonner' +import { + INSTALLATION_ERROR_SUFFIX, + INSTALLATION_INSTALLED_SUFFIX, + INSTALLATION_STARTED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' +import { + Button, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Sheet, + SheetContent, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, +} from 'ui' +import { Admonition } from 'ui-patterns' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' +import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab' +import { StripeSyncChangesCard } from './StripeSyncChangesCard' + +const installFormSchema = z.object({ + stripeSecretKey: z.string().min(1, 'Stripe API key is required'), +}) + +export const StripeSyncInstallationPage = () => { + const { data: project } = useSelectedProjectQuery() + const track = useTrack() + const hasTrackedInstallFailed = useRef(false) + + const [shouldShowInstallSheet, setShouldShowInstallSheet] = useState(false) + const [isInstallInitiated, setIsInstallInitiated] = useState(false) + + const formId = 'stripe-sync-install-form' + const form = useForm>({ + resolver: zodResolver(installFormSchema), + defaultValues: { + stripeSecretKey: '', + }, + mode: 'onSubmit', + }) + + const { data: schemas } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const { + mutate: installStripeSync, + isPending: isInstalling, + error: installError, + reset: resetInstallError, + } = useStripeSyncInstallMutation({ + onSuccess: () => { + toast.success('Stripe Sync installation started') + setShouldShowInstallSheet(false) + form.reset() + setIsInstallInitiated(true) + }, + }) + + const { mutate: uninstallStripeSync, isPending: isUninstalling } = useStripeSyncUninstallMutation( + { + onSuccess: () => { + toast.success('Stripe Sync uninstallation started') + }, + } + ) + + const stripeSchema = schemas?.find((s) => s.name === 'stripe') + + // Determine installation status from schema description + const isInstalled = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment.includes(INSTALLATION_INSTALLED_SUFFIX) + + const schemaShowsInProgress = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment?.includes(INSTALLATION_STARTED_SUFFIX) + + const setupInProgress = schemaShowsInProgress || isInstalling || isInstallInitiated + + const setupError = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment?.includes(INSTALLATION_ERROR_SUFFIX) + + useEffect(() => { + if (!setupError) { + hasTrackedInstallFailed.current = false + return + } + + if (!hasTrackedInstallFailed.current) { + hasTrackedInstallFailed.current = true + // This isn't ideal because it will fire on every page load while in error state + // in the future we should connect this in the backend to track accurately + track('integration_install_failed', { + integrationName: 'stripe_sync_engine', + }) + } + }, [setupError, track]) + + useEffect(() => { + // Clear the install initiated flag once we detect completion or error from the schema + if (isInstallInitiated && (isInstalled || setupError)) { + setIsInstallInitiated(false) + } + }, [isInstallInitiated, isInstalled, setupError]) + + // Check if there's an existing stripe schema that wasn't created by this integration + const hasConflictingSchema = + stripeSchema && !stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) + + const canInstall = !hasConflictingSchema && !isInstalled && !setupInProgress + + // Sync state query - only enabled when installed + const { data: syncState } = useStripeSyncingState( + { + projectRef: project?.ref!, + connectionString: project?.connectionString, + }, + { + refetchInterval: 4000, + enabled: !!isInstalled, + } + ) + + // Poll for schema changes during installation + useSchemasQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + }, + { + refetchInterval: setupInProgress ? 5000 : false, + } + ) + + const isSyncing = !!syncState && !syncState.closed_at && syncState.status === 'running' + + const handleUninstall = () => { + if (!project?.ref) return + + uninstallStripeSync({ + projectRef: project.ref, + }) + } + + const handleOpenInstallSheet = () => { + resetInstallError() + setShouldShowInstallSheet(true) + } + + const handleCloseInstallSheet = (isOpen: boolean) => { + if (isInstalling) return + + setShouldShowInstallSheet(isOpen) + if (!isOpen) { + form.reset() + resetInstallError() + } + } + + const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` + + const alert = useMemo(() => { + if (setupError) { + return ( + +
+ There was an error during the installation of the Stripe Sync Engine. Please try + reinstalling the integration. If the problem persists, contact support. +
+
+ + +
+
+ ) + } + + if (syncState) { + return ( + +
+ {isSyncing ? ( + <> +
+ +
Sync in progress...
+
+
+ Started {formatRelative(new Date(syncState.started_at!), new Date())} +
+ + ) : ( + <> +
+ +
All up to date
+ +
+
+ Last synced {formatRelative(new Date(syncState.closed_at!), new Date())} +
+ + )} +
+
+ ) + } + + return null + }, [ + setupError, + setupInProgress, + syncState, + isSyncing, + isUninstalling, + handleOpenInstallSheet, + handleUninstall, + ]) + + const status = useMemo(() => { + if (isInstalled) { + return ( + + Installed + + ) + } + if (setupInProgress) { + return ( + + + Installing... + + ) + } + if (syncState) { + return ( + + + Sync in progress... + + ) + } + if (setupError) { + return ( + + + Installation error + + ) + } + return ( + Not installed + ) + }, [isInstalled, setupInProgress, syncState, setupError]) + + return ( + setShouldShowInstallSheet(true)} + /> + ) : null + } + > + + + +
{ + if (!project?.ref) return + installStripeSync({ projectRef: project.ref, stripeSecretKey }) + })} + className="overflow-auto flex-grow px-0 flex flex-col" + > + + Install Stripe Sync Engine + + + +

Configuration

+ {installError && ( + +
+ +
+

Installation failed

+

{installError.message}

+
+
+
+ )} + + ( + + + field.onChange(e.target.value)} + /> + + + )} + /> + +
+ + +
+
+ + + + +
+
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx new file mode 100644 index 00000000000..6ca75a277a5 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx @@ -0,0 +1,72 @@ +import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { EdgeFunctions } from 'icons' +import { Layers, Table } from 'lucide-react' +import { Card, CardContent, CardFooter, cn } from 'ui' + +type StripeSyncChangesCardProps = { + className?: string + canInstall?: boolean + onInstall?: () => void +} + +export const StripeSyncChangesCard = ({ + className, + canInstall = true, + onInstall, +}: StripeSyncChangesCardProps) => { + const showInstallButton = typeof onInstall === 'function' + + return ( +
+

This integration will modify your Supabase project:

+ + +
    +
  • + + + Creates a new database schema named stripe + + +
  • +
  • + + Creates tables and views in the stripe schema for synced Stripe data + + +
  • + + Deploys Edge Functions to handle incoming webhooks from Stripe +
  • +
  • + + Schedules automatic Stripe data syncs using Supabase Queues +
  • + + + {showInstallButton && ( + + + Install integration + + + )} + + + ) +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx new file mode 100644 index 00000000000..6d563283210 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx @@ -0,0 +1,187 @@ +import { useSchemasQuery } from 'data/database/schemas-query' +import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { Loader2, Table2 } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' +import { + INSTALLATION_INSTALLED_SUFFIX, + STRIPE_SCHEMA_COMMENT_PREFIX, +} from 'stripe-experiment-sync/supabase' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Card, + CardContent, + WarningIcon, +} from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' +import { PageContainer } from 'ui-patterns/PageContainer' +import { + PageSection, + PageSectionContent, + PageSectionDescription, + PageSectionMeta, + PageSectionSummary, + PageSectionTitle, +} from 'ui-patterns/PageSection' + +export const StripeSyncSettingsPage = () => { + const router = useRouter() + const { data: project } = useSelectedProjectQuery() + const [showUninstallModal, setShowUninstallModal] = useState(false) + const [isUninstallInitiated, setIsUninstallInitiated] = useState(false) + + const { data: schemas, isLoading: isSchemasLoading } = useSchemasQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const stripeSchema = schemas?.find((s) => s.name === 'stripe') + + const isInstalled = + stripeSchema && + stripeSchema.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) && + stripeSchema.comment.includes(INSTALLATION_INSTALLED_SUFFIX) + + // Redirect to overview if not installed + useEffect(() => { + if (!isSchemasLoading && !isInstalled && project?.ref) { + router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) + } + }, [isSchemasLoading, isInstalled, project?.ref, router]) + + const { mutate: uninstallStripeSync, isPending: isUninstalling } = useStripeSyncUninstallMutation( + { + onSuccess: () => { + toast.success('Stripe Sync uninstallation started') + setShowUninstallModal(false) + setIsUninstallInitiated(true) + // Redirect to overview after uninstall + if (project?.ref) { + router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) + } + }, + } + ) + + const handleUninstall = () => { + if (!project?.ref) return + uninstallStripeSync({ projectRef: project.ref }) + } + + const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` + + // Show loading state while checking installation status + if (isSchemasLoading || !isInstalled) { + return ( +
    + +
    + ) + } + + return ( + <> + + + + + Stripe Schema + + Access and manage the synced Stripe data in your database. + + + + + + +
    + +
    +
    +
    Open Stripe schema in Table Editor
    +

    + The Stripe Sync Engine stores all synced data in the stripe{' '} + schema. You can view and query this data directly in the Table Editor. +

    +
    + +
    +
    +
    +
    +
    +
    + + + + + Uninstall Stripe Sync Engine + + Remove the integration and all synced data from your project. + + + + + +
    + +
    +
    +
    + + Uninstalling will remove the stripe schema and all synced data. + + This action cannot be undone. +
    +
    + +
    +
    +
    +
    +
    +
    + + setShowUninstallModal(false)} + onConfirm={handleUninstall} + > +

    + Are you sure you want to uninstall the Stripe Sync Engine? This will: +

    +
      +
    • + Remove the stripe schema and all tables +
    • +
    • Delete all synced Stripe data
    • +
    • Remove the associated Edge Functions
    • +
    • Remove the scheduled sync jobs
    • +
    +

    + This action cannot be undone. +

    +
    + + ) +} diff --git a/apps/studio/data/database-integrations/stripe/keys.ts b/apps/studio/data/database-integrations/stripe/keys.ts new file mode 100644 index 00000000000..d00b8f5b230 --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/keys.ts @@ -0,0 +1,6 @@ +export const stripeSyncKeys = { + all: ['stripe-sync'] as const, + syncState: (projectRef: string) => [...stripeSyncKeys.all, 'sync-state', projectRef] as const, + installationStatus: (projectRef: string) => + [...stripeSyncKeys.all, 'installation-status', projectRef] as const, +} diff --git a/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts b/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts new file mode 100644 index 00000000000..a3157d38138 --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/stripe-sync-install-mutation.ts @@ -0,0 +1,91 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { getAccessToken } from 'common' +import { databaseKeys } from 'data/database/keys' +import { BASE_PATH } from 'lib/constants' +import { useTrack } from 'lib/telemetry/track' +import type { ResponseError } from 'types' +import { stripeSyncKeys } from './keys' + +export type StripeSyncInstallVariables = { + projectRef: string + stripeSecretKey: string +} + +export type StripeSyncInstallResponse = { + data: { message: string } | null + error: { message: string } | null +} + +export async function installStripeSync({ + projectRef, + stripeSecretKey, +}: StripeSyncInstallVariables): Promise { + const accessToken = await getAccessToken() + + const response = await fetch(`${BASE_PATH}/api/integrations/stripe-sync`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + projectRef, + stripeSecretKey, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error?.message || 'Failed to install Stripe Sync') + } + + return result +} + +type StripeSyncInstallData = Awaited> + +export const useStripeSyncInstallMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + const track = useTrack() + + return useMutation({ + mutationFn: (vars) => installStripeSync(vars), + async onSuccess(data, variables, context) { + const { projectRef } = variables + + track('integration_install_started', { + integrationName: 'stripe_sync_engine', + }) + + // Invalidate schemas query to refresh installation status + await queryClient.invalidateQueries({ + queryKey: databaseKeys.schemas(projectRef), + }) + + // Also invalidate any stripe sync related queries + await queryClient.invalidateQueries({ + queryKey: stripeSyncKeys.all, + }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to install Stripe Sync: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts b/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts new file mode 100644 index 00000000000..d9830e22482 --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/stripe-sync-uninstall-mutation.ts @@ -0,0 +1,88 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { getAccessToken } from 'common' +import { databaseKeys } from 'data/database/keys' +import { BASE_PATH } from 'lib/constants' +import { useTrack } from 'lib/telemetry/track' +import type { ResponseError } from 'types' +import { stripeSyncKeys } from './keys' + +export type StripeSyncUninstallVariables = { + projectRef: string +} + +export type StripeSyncUninstallResponse = { + data: { message: string } | null + error: { message: string } | null +} + +export async function uninstallStripeSync({ + projectRef, +}: StripeSyncUninstallVariables): Promise { + const accessToken = await getAccessToken() + + const response = await fetch(`${BASE_PATH}/api/integrations/stripe-sync`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + projectRef, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error?.message || 'Failed to uninstall Stripe Sync') + } + + return result +} + +type StripeSyncUninstallData = Awaited> + +export const useStripeSyncUninstallMutation = ({ + onSuccess, + onError, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + const track = useTrack() + + return useMutation({ + mutationFn: (vars) => uninstallStripeSync(vars), + async onSuccess(data, variables, context) { + const { projectRef } = variables + + track('integration_uninstall_started', { + integrationName: 'stripe_sync_engine', + }) + + // Invalidate schemas query to refresh installation status + await queryClient.invalidateQueries({ + queryKey: databaseKeys.schemas(projectRef), + }) + + // Also invalidate any stripe sync related queries + await queryClient.invalidateQueries({ + queryKey: stripeSyncKeys.all, + }) + + await onSuccess?.(data, variables, context) + }, + async onError(data, variables, context) { + if (onError === undefined) { + toast.error(`Failed to uninstall Stripe Sync: ${data.message}`) + } else { + onError(data, variables, context) + } + }, + ...options, + }) +} diff --git a/apps/studio/data/database-integrations/stripe/sync-state-query.ts b/apps/studio/data/database-integrations/stripe/sync-state-query.ts new file mode 100644 index 00000000000..4e0f0db09ab --- /dev/null +++ b/apps/studio/data/database-integrations/stripe/sync-state-query.ts @@ -0,0 +1,64 @@ +import { QueryClient, useQuery, UseQueryOptions } from '@tanstack/react-query' +import { z } from 'zod' + +import { executeSql, ExecuteSqlError } from 'data/sql/execute-sql-query' +import { stripeSyncKeys } from './keys' + +export type DbConnection = { + projectRef: string + connectionString?: string | null +} + +const StripeSyncStateSchema = z + .object({ + started_at: z.string().nullable(), + closed_at: z.string().nullable(), + status: z.enum(['running', 'pending', 'complete', 'error']).nullable(), + }) + .nullable() + +export type StripeSyncState = z.infer + +export type StripeSyncStateData = z.infer +export type StripeSyncStateError = ExecuteSqlError + +export async function getStripeSyncState( + { projectRef, connectionString }: DbConnection, + signal?: AbortSignal +) { + const { result } = await executeSql( + { + projectRef, + connectionString, + sql: ` + SELECT started_at, closed_at, status FROM stripe.sync_runs WHERE status != 'pending' ORDER BY started_at DESC LIMIT 1; + `, + queryKey: stripeSyncKeys.syncState(projectRef), + }, + signal + ) + + return result.length > 0 ? StripeSyncStateSchema.parse(result[0]) : null +} + +export const useStripeSyncingState = ( + { projectRef, connectionString }: DbConnection, + { + enabled = true, + ...options + }: Omit< + UseQueryOptions, + 'queryKey' | 'queryFn' + > = {} +) => { + return useQuery({ + queryKey: stripeSyncKeys.syncState(projectRef), + queryFn: ({ signal }) => getStripeSyncState({ projectRef, connectionString }, signal), + enabled: enabled && typeof projectRef !== 'undefined', + ...options, + }) +} + +export function invalidateStripeSyncStateQuery(client: QueryClient, projectRef: string) { + return client.invalidateQueries({ queryKey: stripeSyncKeys.syncState(projectRef) }) +} diff --git a/apps/studio/package.json b/apps/studio/package.json index 46216f4cbab..aafb78a3d81 100644 --- a/apps/studio/package.json +++ b/apps/studio/package.json @@ -136,6 +136,7 @@ "sql-formatter": "^15.0.0", "sqlstring": "^2.3.2", "streamdown": "^1.3.0", + "stripe-experiment-sync": "^1.0.18", "tus-js-client": "^4.1.0", "ui": "workspace:*", "ui-patterns": "workspace:*", diff --git a/apps/studio/pages/api/integrations/stripe-sync.ts b/apps/studio/pages/api/integrations/stripe-sync.ts new file mode 100644 index 00000000000..e88c53f5b28 --- /dev/null +++ b/apps/studio/pages/api/integrations/stripe-sync.ts @@ -0,0 +1,141 @@ +import { NextApiRequest, NextApiResponse } from 'next' +import { z } from 'zod' +import { install, uninstall } from 'stripe-experiment-sync/supabase' +import { VERSION } from 'stripe-experiment-sync' +import { waitUntil } from '@vercel/functions' + +const ENABLE_FLAG_KEY = 'enableStripeSyncEngineIntegration' + +const InstallBodySchema = z.object({ + projectRef: z.string().min(1), + stripeSecretKey: z.string().min(1), +}) + +const UninstallBodySchema = z.object({ + projectRef: z.string().min(1), +}) + +async function isStripeSyncEnabled() { + // The ConfigClient doesn't seem to work properly so we'll just gate access from the frontend + // for now + return true +} + +function getBearerToken(req: NextApiRequest) { + const authHeader = req.headers.authorization + if (!authHeader || Array.isArray(authHeader)) return null + const match = authHeader.match(/^Bearer\s+(.+)$/i) + return match?.[1]?.trim() ?? null +} + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + // Hide endpoint if the integration is disabled. + if (!(await isStripeSyncEnabled())) { + return res.status(404).json({ data: null, error: { message: 'Not Found' } }) + } + + const { method } = req + switch (method) { + case 'POST': + return handleSetupStripeSyncInstall(req, res) + case 'DELETE': + return handleDeleteStripeSyncInstall(req, res) + default: + return res + .status(405) + .json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +async function handleDeleteStripeSyncInstall(req: NextApiRequest, res: NextApiResponse) { + const supabaseToken = getBearerToken(req) + if (!supabaseToken) { + return res + .status(401) + .json({ data: null, error: { message: 'Unauthorized: Invalid Authorization header' } }) + } + + const parsed = UninstallBodySchema.safeParse(req.body) + if (!parsed.success) { + return res + .status(400) + .json({ data: null, error: { message: 'Bad Request: Invalid request body' } }) + } + const { projectRef } = parsed.data + + waitUntil( + uninstall({ + supabaseAccessToken: supabaseToken, + supabaseProjectRef: projectRef, + baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN, + supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN, + }).catch((error) => { + console.error('Stripe Sync Engine uninstallation failed.', error) + throw error + }) + ) + + return res + .status(200) + .json({ data: { message: 'Stripe Sync uninstallation initiated' }, error: null }) +} + +async function handleSetupStripeSyncInstall(req: NextApiRequest, res: NextApiResponse) { + const supabaseToken = getBearerToken(req) + if (!supabaseToken) { + return res + .status(401) + .json({ data: null, error: { message: 'Unauthorized: Invalid Authorization header' } }) + } + + const parsed = InstallBodySchema.safeParse(req.body) + if (!parsed.success) { + return res + .status(400) + .json({ data: null, error: { message: 'Bad Request: Invalid request body' } }) + } + const { projectRef, stripeSecretKey } = parsed.data + + // Validate the Stripe API key before proceeding with installation + try { + const stripeResponse = await fetch('https://api.stripe.com/v1/account', { + method: 'GET', + headers: { + Authorization: `Bearer ${stripeSecretKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + + if (!stripeResponse.ok) { + const errorData = await stripeResponse.json() + const errorMessage = + errorData.error?.message || `Invalid Stripe API key (HTTP ${stripeResponse.status})` + return res.status(400).json({ + data: null, + error: { message: `Invalid Stripe API key: ${errorMessage}` }, + }) + } + } catch (error: any) { + return res.status(400).json({ + data: null, + error: { message: `Failed to validate Stripe API key: ${error.message}` }, + }) + } + waitUntil( + install({ + supabaseAccessToken: supabaseToken, + supabaseProjectRef: projectRef, + stripeKey: stripeSecretKey, + baseProjectUrl: process.env.NEXT_PUBLIC_CUSTOMER_DOMAIN, + supabaseManagementUrl: process.env.NEXT_PUBLIC_API_DOMAIN, + packageVersion: VERSION, + }).catch((error) => { + console.error('Stripe Sync Engine installation failed.', error) + throw error + }) + ) + + return res + .status(200) + .json({ data: { message: 'Stripe Sync setup initiated', version: VERSION }, error: null }) +} diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index 4577175c615..8ca9eb94dde 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -2,7 +2,7 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useMemo } from 'react' -import { useParams } from 'common' +import { useFlag, useParams } from 'common' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' @@ -41,6 +41,7 @@ const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() const { ref, id, pageId, childId } = useParams() const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) + const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration') const { installedIntegrations: installedIntegrations, isLoading: isIntegrationsLoading } = useInstalledIntegrations() @@ -135,6 +136,10 @@ const IntegrationPage: NextPageWithLayout = () => { return null } + if (id === 'stripe_sync_engine' && !stripeSyncEnabled) { + return + } + if (!integrationsWrappers && id?.endsWith('_wrapper')) { return } diff --git a/apps/studio/pages/project/[ref]/integrations/index.tsx b/apps/studio/pages/project/[ref]/integrations/index.tsx index 2788918925a..4e777008403 100644 --- a/apps/studio/pages/project/[ref]/integrations/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/index.tsx @@ -25,14 +25,16 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent, PageSectionMeta } from 'ui-patterns/PageSection' +import { useFlag } from 'common' -const FEATURED_INTEGRATIONS = ['cron', 'queues', 'stripe_wrapper'] +const FEATURED_INTEGRATIONS = ['cron', 'queues'] // + either stripe_sync_engine or stripe_wrapper depending on feature flag // Featured integration images const FEATURED_INTEGRATION_IMAGES: Record = { cron: 'img/integrations/covers/cron-cover.webp', queues: 'img/integrations/covers/queues-cover.png', stripe_wrapper: 'img/integrations/covers/stripe-cover.png', + stripe_sync_engine: 'img/integrations/covers/stripe-cover.png', } const IntegrationsPage: NextPageWithLayout = () => { @@ -45,6 +47,16 @@ const IntegrationsPage: NextPageWithLayout = () => { parseAsString.withDefault('').withOptions({ clearOnDefault: true }) ) + const isStripeSyncEngineEnabled = useFlag('enableStripeSyncEngineIntegration') + + const featuredIntegrationIds = useMemo(() => { + if (isStripeSyncEngineEnabled) { + return FEATURED_INTEGRATIONS.concat(['stripe_sync_engine']) + } else { + return FEATURED_INTEGRATIONS.concat(['stripe_wrapper']) + } + }, [isStripeSyncEngineEnabled]) + const { availableIntegrations, installedIntegrations, error, isError, isLoading, isSuccess } = useInstalledIntegrations() @@ -105,8 +117,9 @@ const IntegrationsPage: NextPageWithLayout = () => { } const featured = filteredAndSortedIntegrations.filter((i) => - FEATURED_INTEGRATIONS.includes(i.id) + featuredIntegrationIds.includes(i.id) ) + const allIntegrations = filteredAndSortedIntegrations // Include all integrations, including featured return { diff --git a/apps/studio/proxy.ts b/apps/studio/proxy.ts index 395a9d4661f..08380bd1bd1 100644 --- a/apps/studio/proxy.ts +++ b/apps/studio/proxy.ts @@ -25,6 +25,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/edge-functions/body', '/generate-attachment-url', '/incident-status', + '/api/integrations/stripe-sync', ] export function proxy(request: NextRequest) { diff --git a/apps/studio/static-data/integrations/stripe_sync_engine/overview.md b/apps/studio/static-data/integrations/stripe_sync_engine/overview.md new file mode 100644 index 00000000000..129ba58920e --- /dev/null +++ b/apps/studio/static-data/integrations/stripe_sync_engine/overview.md @@ -0,0 +1,3 @@ +[Stripe](https://stripe.com/) is an API driven payment processing and subscription management platform. + +The Stripe Sync Engine will sync data from your Stripe account including customers, subscriptions, invoices, and payments into Postgres tables in your Supabase account. diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 3a41d04f115..4887b9b4a24 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2601,6 +2601,98 @@ export interface RequestUpgradeSubmittedEvent { groups: Omit } +/** + * User succesfully installed an integration via the integrations marketplace in the dashboard. + * Note: This excludes Wrappers and Postgres Extensions. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationInstalledEvent { + action: 'integration_installed' + properties: { + /** + * The name of the integration installed + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * User started installing an integration via the integrations marketplace. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationInstallStartedEvent { + action: 'integration_install_started' + properties: { + /** + * The name of the integration being installed + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * User started uninstalling an integration via the integrations marketplace. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationUninstallStartedEvent { + action: 'integration_uninstall_started' + properties: { + /** + * The name of the integration being uninstalled + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * Installation failed for an integration. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationInstallFailedEvent { + action: 'integration_install_failed' + properties: { + /** + * The name of the integration whose installation failed + */ + integrationName: string + } + groups: TelemetryGroups +} + +/** + * User uninstalled an integration via the integrations marketplace in the dashboard. + * Note: This excludes Wrappers and Postgres Extensions. + * + * @group Events + * @source studio + * @page /dashboard/project/{ref}/integrations/{integration_slug} + */ +export interface IntegrationUninstalledEvent { + action: 'integration_uninstalled' + properties: { + /** + * The name of the integration installed + */ + integrationName: string + } + groups: TelemetryGroups +} + /** * @hidden */ @@ -2746,3 +2838,8 @@ export type TelemetryEvent = | QueryPerformanceAIExplanationButtonClickedEvent | RequestUpgradeModalOpenedEvent | RequestUpgradeSubmittedEvent + | IntegrationInstalledEvent + | IntegrationInstallStartedEvent + | IntegrationUninstallStartedEvent + | IntegrationInstallFailedEvent + | IntegrationUninstalledEvent diff --git a/packages/pg-meta/src/pg-meta-schemas.ts b/packages/pg-meta/src/pg-meta-schemas.ts index 5c2452bacd3..f66506244c7 100644 --- a/packages/pg-meta/src/pg-meta-schemas.ts +++ b/packages/pg-meta/src/pg-meta-schemas.ts @@ -7,6 +7,7 @@ const pgSchemaZod = z.object({ id: z.number(), name: z.string(), owner: z.string(), + comment: z.string().nullable(), }) const pgSchemaArrayZod = z.array(pgSchemaZod) const pgSchemaOptionalZod = z.optional(pgSchemaZod) diff --git a/packages/pg-meta/src/sql/schemas.ts b/packages/pg-meta/src/sql/schemas.ts index f48af16c72c..092e78495b8 100644 --- a/packages/pg-meta/src/sql/schemas.ts +++ b/packages/pg-meta/src/sql/schemas.ts @@ -4,7 +4,8 @@ export const SCHEMAS_SQL = /* SQL */ ` select n.oid as id, n.nspname as name, - u.rolname as owner + u.rolname as owner, + obj_description(n.oid, 'pg_namespace') AS comment from pg_namespace n, pg_roles u diff --git a/packages/pg-meta/test/schemas.test.ts b/packages/pg-meta/test/schemas.test.ts index 8e87e3c4ef0..a973770295c 100644 --- a/packages/pg-meta/test/schemas.test.ts +++ b/packages/pg-meta/test/schemas.test.ts @@ -33,11 +33,12 @@ withTestDatabase('list with system schemas', async ({ executeQuery }) => { { id: expect.any(Number) }, ` { + "comment": "system catalog schema", "id": Any, "name": "pg_catalog", "owner": "postgres", } - ` + ` ) }) @@ -51,11 +52,12 @@ withTestDatabase('list without system schemas', async ({ executeQuery }) => { { id: expect.any(Number) }, ` { + "comment": "standard public schema", "id": Any, "name": "public", "owner": "postgres", } - ` + ` ) }) @@ -71,11 +73,12 @@ withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { id: expect.any(Number) }, ` { + "comment": null, "id": Any, "name": "s", "owner": "postgres", } - ` + ` ) // Retrieve schema again to verify @@ -87,11 +90,12 @@ withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { id: expect.any(Number) }, ` { + "comment": null, "id": Any, "name": "s", "owner": "postgres", } - ` + ` ) // Update schema @@ -113,11 +117,12 @@ withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => { id: expect.any(Number) }, ` { + "comment": null, "id": Any, "name": "ss", "owner": "postgres", } - ` + ` ) // Delete schema diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fcfa507cb3..1febf67dc73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -489,7 +489,7 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) nuqs: specifier: ^1.19.1 - version: 1.19.1(next@15.5.9(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) + version: 1.19.1(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)) openai: specifier: ^4.75.1 version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) @@ -1034,6 +1034,9 @@ importers: streamdown: specifier: ^1.3.0 version: 1.3.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) + stripe-experiment-sync: + specifier: ^1.0.18 + version: 1.0.18(@types/node@22.13.14)(supports-color@8.1.1) tus-js-client: specifier: ^4.1.0 version: 4.1.0 @@ -4430,6 +4433,28 @@ packages: cpu: [x64] os: [win32] + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/confirm@5.1.8': resolution: {integrity: sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==} engines: {node: '>=18'} @@ -4448,10 +4473,122 @@ packages: '@types/node': optional: true + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/figures@1.0.11': resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==} engines: {node: '>=18'} + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@inquirer/type@3.0.5': resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==} engines: {node: '>=18'} @@ -4903,6 +5040,91 @@ packages: cpu: [x64] os: [win32] + '@ngrok/ngrok-android-arm64@1.6.0': + resolution: {integrity: sha512-gOfVVjYq7ruPMgH108j99uaBUkwD1DxDpnk+RufQHzxRKL72MZad7QfaRwc1v8iRMyOh3EKYYvU2y8HSXDbSkA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@ngrok/ngrok-darwin-arm64@1.6.0': + resolution: {integrity: sha512-YdKlEkz0PHHnoy2UlEH2XMrupetmT6K6bLrF+MFWrKYKpIbOuGV4HcVioh1iu0NTmn7xJwrAbo/Mwx2E95/qdg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ngrok/ngrok-darwin-universal@1.6.0': + resolution: {integrity: sha512-yiUCkg10+vUKgvkD2rAZHlzvZmINUNF7mna7iL+ydh4pOx6jamSwDQ2QRseVAyxC9eZVy/rKFYLVnz2Ci+0D9w==} + engines: {node: '>= 10'} + os: [darwin] + + '@ngrok/ngrok-darwin-x64@1.6.0': + resolution: {integrity: sha512-a6LbwODDDCRpriILe+1lq+zuIjGbj+pR1A0KThTP8riVTQ538DMqR09kv5Oz7ZqcclS/IK+3ZLKkxHuPX9ob3g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ngrok/ngrok-freebsd-x64@1.6.0': + resolution: {integrity: sha512-t77ayWr0zPrW75DstzvoE6Hwj7h1mpQSFKQ3I0B3iC9o7XZhcNkwFpX8QLbmQNkeHxz5UhlBtzOYPdzDJY+3uw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@ngrok/ngrok-linux-arm-gnueabihf@1.6.0': + resolution: {integrity: sha512-NBCJnPAbl44Ua3dpO+kOkyz4cGGLu2FiDcEHBES7OARiSscoui+DyNPLe5auZFjag5/0B3ZNiDgCcdRBAi+KNQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@ngrok/ngrok-linux-arm64-gnu@1.6.0': + resolution: {integrity: sha512-i65WIaeImO2/oyTfwUAVW1j/poLMzX/PAYsDRLXOac6/1DV6kIQX1kmr+CEs6NtmKgD76QiApbqtksd9MDlncg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@ngrok/ngrok-linux-arm64-musl@1.6.0': + resolution: {integrity: sha512-7osqE43Pl6HiTeLZSEnSTtgbNQ1zqYovYjZ7lGgeKJHlgWXE6RvE7qhdjDw17ebUuT0IS8JPGZFnlyF6J0UKPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@ngrok/ngrok-linux-x64-gnu@1.6.0': + resolution: {integrity: sha512-jP3enLStnTGTKLBXqctVt3wIVvGLP/BkLKYuXwrU1EqchtvDcsAhpspeUeX23SAtLXUgbX9vN1B6IvsAxWcyxw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@ngrok/ngrok-linux-x64-musl@1.6.0': + resolution: {integrity: sha512-hrBrXCofgmY9pc36N0nfDS+bfomu5hOqt0N5IQkL4n0UaQcl48tKddIex2eR+mU3QBYh6p1XKxAwyAVjwplD2w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@ngrok/ngrok-win32-arm64-msvc@1.6.0': + resolution: {integrity: sha512-EXJXFaXh/cNRaTqs02EmysILtcSImFWwC98mjG6PU5MEG+gqNz8AddnqFytY0RRxkR9SxwnC+7QAV3qYOSU6kA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ngrok/ngrok-win32-ia32-msvc@1.6.0': + resolution: {integrity: sha512-R+5WB6ONT4ynHFusxRw8kbXx2SBYftl6XUiNQNmwwCdwdNO7fPzdwhbmbzgfCEjQrYQrXD1vs5J+zJJeSaAicw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ngrok/ngrok-win32-x64-msvc@1.6.0': + resolution: {integrity: sha512-4QViW0GSPsjmghYPExO0Jt9DtIybK23MXPP0BltOuoFP0iyA15vb03QXAI4223zlSLWSWYnOx+qnCuzV1qT0lQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ngrok/ngrok@1.6.0': + resolution: {integrity: sha512-TWC6nh4Kl16HjhG/bVhpYNP5UIyJc9oZetCrnO4r5+gq95eIxZKoLXzuAyC0gF/J+NEY3c1euFAET5Sn9wOWaw==} + engines: {node: '>= 10'} + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -10040,6 +10262,9 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-includes@3.1.8: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} @@ -10245,6 +10470,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.1: resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} @@ -10436,6 +10665,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} @@ -10677,6 +10909,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.1: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} @@ -10795,6 +11031,9 @@ packages: cookie-es@2.0.0: resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -12177,6 +12416,10 @@ packages: peerDependencies: express: ^4.11 || 5 || ^5.0.0-beta.1 + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -12375,6 +12618,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + finalhandler@2.1.0: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} @@ -13277,6 +13524,15 @@ packages: react: ^16.8 || ^17.0 || ^18.0 react-dom: ^16.8 || ^17.0 || ^18.0 + inquirer@12.11.1: + resolution: {integrity: sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + inquirer@8.2.6: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} @@ -15041,7 +15297,6 @@ packages: next@15.5.9: resolution: {integrity: sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -15506,6 +15761,9 @@ packages: openapi-fetch@0.13.8: resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==} + openapi-fetch@0.6.2: + resolution: {integrity: sha512-Faj29Kzh7oCbt1bz6vAGNKtRJlV/GolOQTx87eYUnfCK7eVXdN9jQVojroc7tcJ5OQgyhbeOqD7LS/8UtGBnMQ==} + openapi-sampler@1.6.1: resolution: {integrity: sha512-s1cIatOqrrhSj2tmJ4abFYZQK6l5v+V4toO5q1Pa0DyN8mtyqy2I+Qrj5W9vOELEtybIMQs/TBZGVO/DtTFK8w==} @@ -15722,6 +15980,9 @@ packages: resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} engines: {node: 20 || >=22} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -15804,6 +16065,11 @@ packages: resolution: {integrity: sha512-NoSsPqXxbkD8RIe+peQCqiea4QzXgosdTKY8p7PsbbGsh2F8TifDj/vJxfuR8qJwNYrijdSs7uf0tAe6WOyCsQ==} engines: {node: '>=12.0.0'} + pg-node-migrations@0.0.8: + resolution: {integrity: sha512-44cMl9umOmCv0hzZyEcvjEq8Bm8u7mrzggZ06qXTJVSsMMB4j2OsjG+rSp+uzeKWyP2Vu0K9Ye2wKtjFUJwrdw==} + engines: {node: '>10.17.0'} + hasBin: true + pg-numeric@1.0.2: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} @@ -16452,6 +16718,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -17176,6 +17446,10 @@ packages: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -17185,6 +17459,9 @@ packages: rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -17651,6 +17928,10 @@ packages: sql-parser-cst@0.24.0: resolution: {integrity: sha512-38NNWamo0PKv80Zn8r1iXgatkWEB04CPhun8nEQhEJlYBMSIV4OOt4g3diynjVHJSunNSQqAjKcwmhM0EeLCtg==} + sql-template-strings@2.2.2: + resolution: {integrity: sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q==} + engines: {node: '>=4.0.0'} + sqlstring@2.3.3: resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} engines: {node: '>= 0.6'} @@ -17846,6 +18127,14 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stripe-experiment-sync@1.0.18: + resolution: {integrity: sha512-I1LGtpHghu/6uNDOGzsJt+bVZP5jnQyDIXlW9xq7YiZf5ik9MbKcUGMcq64g5A+dWSV2mhY65UgZIrI8VBDjOA==} + hasBin: true + + stripe@17.7.0: + resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} + engines: {node: '>=12.*'} + strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} @@ -17919,6 +18208,10 @@ packages: engines: {node: '>=8'} hasBin: true + supabase-management-js@0.1.6: + resolution: {integrity: sha512-TDQg6Nh7e2DaLmMjqpJt7Wwh2Au5loVPH2gwKMkD89e3pb5F/zMQGWt22asEXme031jEmhjazixFH0lwMgbRJA==} + engines: {node: '>=18.0.0'} + supabase@2.58.5: resolution: {integrity: sha512-mYZSkUIePTdmwlHd26Pff8wpmjfre8gcuWzrc5QqhZgZvCXugVzAQQhcjaQisw5kusbPQWNIjUwcHYEKmejhPw==} engines: {npm: '>=8'} @@ -18818,6 +19111,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true @@ -19360,6 +19657,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yesql@7.0.0: + resolution: {integrity: sha512-sosfr7agy4ibLM7BvXBkM6BpBmKMGuBO8DUYQEuey+QqaqrgW+2bsSg6D050ocBYIz0PuHxUyehyzEztZTU4pw==} + yjs@13.6.27: resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -19376,6 +19676,10 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -22356,6 +22660,25 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/confirm@5.1.21(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + '@inquirer/confirm@5.1.8(@types/node@22.13.14)': dependencies: '@inquirer/core': 10.1.9(@types/node@22.13.14) @@ -22366,7 +22689,7 @@ snapshots: '@inquirer/core@10.1.9(@types/node@22.13.14)': dependencies: '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -22376,8 +22699,114 @@ snapshots: optionalDependencies: '@types/node': 22.13.14 + '@inquirer/core@10.3.2(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/editor@4.2.23(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/external-editor': 1.0.3(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/expand@4.0.23(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/external-editor@1.0.3(@types/node@22.13.14)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 22.13.14 + '@inquirer/figures@1.0.11': {} + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/number@3.0.23(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/password@4.0.23(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/prompts@7.10.1(@types/node@22.13.14)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.13.14) + '@inquirer/confirm': 5.1.21(@types/node@22.13.14) + '@inquirer/editor': 4.2.23(@types/node@22.13.14) + '@inquirer/expand': 4.0.23(@types/node@22.13.14) + '@inquirer/input': 4.3.1(@types/node@22.13.14) + '@inquirer/number': 3.0.23(@types/node@22.13.14) + '@inquirer/password': 4.0.23(@types/node@22.13.14) + '@inquirer/rawlist': 4.1.11(@types/node@22.13.14) + '@inquirer/search': 3.2.2(@types/node@22.13.14) + '@inquirer/select': 4.4.2(@types/node@22.13.14) + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/rawlist@4.1.11(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/search@3.2.2(@types/node@22.13.14)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/select@4.4.2(@types/node@22.13.14)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.13.14) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.13.14 + + '@inquirer/type@3.0.10(@types/node@22.13.14)': + optionalDependencies: + '@types/node': 22.13.14 + '@inquirer/type@3.0.5(@types/node@22.13.14)': optionalDependencies: '@types/node': 22.13.14 @@ -22963,6 +23392,61 @@ snapshots: '@next/swc-win32-x64-msvc@16.0.10': optional: true + '@ngrok/ngrok-android-arm64@1.6.0': + optional: true + + '@ngrok/ngrok-darwin-arm64@1.6.0': + optional: true + + '@ngrok/ngrok-darwin-universal@1.6.0': + optional: true + + '@ngrok/ngrok-darwin-x64@1.6.0': + optional: true + + '@ngrok/ngrok-freebsd-x64@1.6.0': + optional: true + + '@ngrok/ngrok-linux-arm-gnueabihf@1.6.0': + optional: true + + '@ngrok/ngrok-linux-arm64-gnu@1.6.0': + optional: true + + '@ngrok/ngrok-linux-arm64-musl@1.6.0': + optional: true + + '@ngrok/ngrok-linux-x64-gnu@1.6.0': + optional: true + + '@ngrok/ngrok-linux-x64-musl@1.6.0': + optional: true + + '@ngrok/ngrok-win32-arm64-msvc@1.6.0': + optional: true + + '@ngrok/ngrok-win32-ia32-msvc@1.6.0': + optional: true + + '@ngrok/ngrok-win32-x64-msvc@1.6.0': + optional: true + + '@ngrok/ngrok@1.6.0': + optionalDependencies: + '@ngrok/ngrok-android-arm64': 1.6.0 + '@ngrok/ngrok-darwin-arm64': 1.6.0 + '@ngrok/ngrok-darwin-universal': 1.6.0 + '@ngrok/ngrok-darwin-x64': 1.6.0 + '@ngrok/ngrok-freebsd-x64': 1.6.0 + '@ngrok/ngrok-linux-arm-gnueabihf': 1.6.0 + '@ngrok/ngrok-linux-arm64-gnu': 1.6.0 + '@ngrok/ngrok-linux-arm64-musl': 1.6.0 + '@ngrok/ngrok-linux-x64-gnu': 1.6.0 + '@ngrok/ngrok-linux-x64-musl': 1.6.0 + '@ngrok/ngrok-win32-arm64-msvc': 1.6.0 + '@ngrok/ngrok-win32-ia32-msvc': 1.6.0 + '@ngrok/ngrok-win32-x64-msvc': 1.6.0 + '@noble/ciphers@1.3.0': {} '@noble/curves@1.9.7': @@ -29505,6 +29989,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-flatten@1.1.1: {} + array-includes@3.1.8: dependencies: call-bind: 1.0.8 @@ -29733,6 +30219,23 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + body-parser@1.20.4(supports-color@8.1.1): + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9(supports-color@8.1.1) + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.2.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 @@ -30006,6 +30509,8 @@ snapshots: chardet@0.7.0: {} + chardet@2.1.1: {} + charenc@0.0.2: {} check-error@2.1.1: {} @@ -30245,6 +30750,8 @@ snapshots: commander@11.1.0: {} + commander@12.1.0: {} + commander@14.0.1: {} commander@2.20.3: {} @@ -30380,6 +30887,8 @@ snapshots: cookie-es@2.0.0: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -31829,6 +32338,42 @@ snapshots: dependencies: express: 5.1.0(supports-color@8.1.1) + express@4.22.1(supports-color@8.1.1): + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4(supports-color@8.1.1) + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9(supports-color@8.1.1) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2(supports-color@8.1.1) + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0(supports-color@8.1.1) + serve-static: 1.16.2(supports-color@8.1.1) + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.1.0(supports-color@8.1.1): dependencies: accepts: 2.0.0 @@ -32079,6 +32624,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.2(supports-color@8.1.1): + dependencies: + debug: 2.6.9(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.1.0(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -33185,6 +33742,18 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + inquirer@12.11.1(@types/node@22.13.14): + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.13.14) + '@inquirer/prompts': 7.10.1(@types/node@22.13.14) + '@inquirer/type': 3.0.10(@types/node@22.13.14) + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.13.14 + inquirer@8.2.6: dependencies: ansi-escapes: 4.3.2 @@ -35951,7 +36520,7 @@ snapshots: number-flow@0.3.7: {} - nuqs@1.19.1(next@15.5.9(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)): + nuqs@1.19.1(next@15.5.9(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)): dependencies: mitt: 3.0.1 next: 15.5.9(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) @@ -36289,6 +36858,8 @@ snapshots: dependencies: openapi-typescript-helpers: 0.0.15 + openapi-fetch@0.6.2: {} + openapi-sampler@1.6.1: dependencies: '@types/json-schema': 7.0.15 @@ -36582,6 +37153,8 @@ snapshots: lru-cache: 11.0.1 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + path-to-regexp@6.3.0: {} path-to-regexp@8.1.0: {} @@ -36678,6 +37251,13 @@ snapshots: pg-minify@1.6.3: {} + pg-node-migrations@0.0.8: + dependencies: + pg: 8.16.3 + sql-template-strings: 2.2.2 + transitivePeerDependencies: + - pg-native + pg-numeric@1.0.2: {} pg-pool@3.10.1(pg@8.16.3): @@ -37325,6 +37905,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -38327,6 +38914,8 @@ snapshots: run-async@2.4.1: {} + run-async@4.0.6: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -38337,6 +38926,10 @@ snapshots: dependencies: tslib: 2.8.1 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + sade@1.8.1: dependencies: mri: 1.2.0 @@ -38975,6 +39568,8 @@ snapshots: sql-parser-cst@0.24.0: {} + sql-template-strings@2.2.2: {} + sqlstring@2.3.3: {} sse.js@2.2.0: {} @@ -39207,6 +39802,33 @@ snapshots: dependencies: js-tokens: 9.0.1 + stripe-experiment-sync@1.0.18(@types/node@22.13.14)(supports-color@8.1.1): + dependencies: + '@ngrok/ngrok': 1.6.0 + chalk: 5.4.1 + commander: 12.1.0 + dotenv: 16.5.0 + express: 4.22.1(supports-color@8.1.1) + inquirer: 12.11.1(@types/node@22.13.14) + papaparse: 5.4.1 + pg: 8.16.3 + pg-node-migrations: 0.0.8 + stripe: 17.7.0 + supabase-management-js: 0.1.6 + ws: 8.18.3 + yesql: 7.0.0 + transitivePeerDependencies: + - '@types/node' + - bufferutil + - pg-native + - supports-color + - utf-8-validate + + stripe@17.7.0: + dependencies: + '@types/node': 22.13.14 + qs: 6.14.0 + strnum@1.1.2: {} strtok3@8.1.0: @@ -39295,6 +39917,10 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + supabase-management-js@0.1.6: + dependencies: + openapi-fetch: 0.6.2 + supabase@2.58.5(supports-color@8.1.1): dependencies: bin-links: 6.0.0 @@ -40239,6 +40865,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + uuid@10.0.0: {} uuid@11.1.0: {} @@ -40938,6 +41566,8 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yesql@7.0.0: {} + yjs@13.6.27: dependencies: lib0: 0.2.108 @@ -40949,6 +41579,8 @@ snapshots: yoctocolors-cjs@2.1.2: {} + yoctocolors-cjs@2.1.3: {} + yoctocolors@2.1.2: {} yoga-wasm-web@0.3.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c4344ed6716..423c8e85fb1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,6 +50,7 @@ minimumReleaseAgeExclude: - supabase - iceberg-js - '@vitejs/plugin-rsc' + - stripe-experiment-sync # TODO(matlin) remove, temp just to unblock launch onlyBuiltDependencies: - supabase