This commit is contained in:
John Yeo
2026-04-10 19:18:51 +01:00
46 changed files with 1790 additions and 157 deletions

View File

@@ -253,6 +253,18 @@
"typescript-eslint": "^8.26.0",
},
},
"packages/stripe-sync": {
"name": "@autumn/stripe-sync",
"version": "1.0.0",
"dependencies": {
"@supabase/stripe-sync-engine": "^0.48.5",
"pg": "^8.20.0",
"stripe": "catalog:",
},
"devDependencies": {
"@types/pg": "^8.20.0",
},
},
"scripts": {
"name": "@autumn/scripts",
"version": "1.0.0",
@@ -282,6 +294,7 @@
"@anthropic-ai/sdk": "^0.32.1",
"@autumn/ksuid": "workspace:*",
"@autumn/shared": "workspace:*",
"@autumn/stripe-sync": "workspace:*",
"@aws-sdk/client-s3": "^3.1017.0",
"@aws-sdk/client-scheduler": "^3.1004.0",
"@aws-sdk/client-sqs": "^3.958.0",
@@ -604,6 +617,8 @@
"@autumn/shared": ["@autumn/shared@workspace:shared"],
"@autumn/stripe-sync": ["@autumn/stripe-sync@workspace:packages/stripe-sync"],
"@autumn/vite": ["@autumn/vite@workspace:vite"],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
@@ -1178,11 +1193,11 @@
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="],
"@mintlify/cli": ["@mintlify/cli@4.0.1107", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.847", "@mintlify/link-rot": "3.0.1022", "@mintlify/prebuild": "1.0.989", "@mintlify/previewing": "4.0.1050", "@mintlify/validation": "0.1.662", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.2.0", "open": "^8.4.2", "openid-client": "^6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "^4.3.6" }, "optionalDependencies": { "keytar": "^7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-kOj8kFyebIzPkitgiCy7eWiJnCzPALOzEccQCQcaYHA7GUtXcMi6qK1IfssYaWbKs4Nbv9GmyodWPTCZEDFYzg=="],
"@mintlify/cli": ["@mintlify/cli@4.0.1109", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.849", "@mintlify/link-rot": "3.0.1024", "@mintlify/prebuild": "1.0.991", "@mintlify/previewing": "4.0.1052", "@mintlify/validation": "0.1.663", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.2.0", "open": "^8.4.2", "openid-client": "^6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "^4.3.6" }, "optionalDependencies": { "keytar": "^7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-pw3XejzgnZLhMbcEUjMpS5+muIOWsxbpKVhikK3G3cmzq2FXtZgHpfH5JkA0uHPoNhacrPN2b3I3Aj2LyNKc/A=="],
"@mintlify/common": ["@mintlify/common@1.0.847", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.662", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.6", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "^3.34.0", "tailwindcss": "^3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-6MNx0xn5M4KLSyWwbyUc3jpHUZ9vJFjRmRzo0n06Dt5OcpUFt89NwXYLQBbjNxFinklw9JN2+02vudoZsAdtnA=="],
"@mintlify/common": ["@mintlify/common@1.0.849", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.663", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.0", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.6", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "^3.34.0", "tailwindcss": "^3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-VGo1tRAImwccC/oyTSxD2rATKj2AQ9HR9mO5pbs4lJQGOTIfpAVtyie7iSohmtnu4WRJOBLRofSv/jgYGnG0zg=="],
"@mintlify/link-rot": ["@mintlify/link-rot@3.0.1022", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/prebuild": "1.0.989", "@mintlify/previewing": "4.0.1050", "@mintlify/scraping": "4.0.522", "@mintlify/validation": "0.1.662", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-/I0FsmLToYiw6D8XNjWzQaexjsrYYosY0bPcQDO0xvfIYfYRX0jAyEoLJ3n6tGfHZYi5C666kT7ti5evIO37zg=="],
"@mintlify/link-rot": ["@mintlify/link-rot@3.0.1024", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/prebuild": "1.0.991", "@mintlify/previewing": "4.0.1052", "@mintlify/scraping": "4.0.522", "@mintlify/validation": "0.1.663", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-+0AeKU24PvdAguipUUmmGq17CzOq7X2B3FLUNig63Zc4v47t5DeSQ/WSOo36sNv6LD3c1//RJKBRO2gfa9XTnw=="],
"@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="],
@@ -1190,13 +1205,13 @@
"@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="],
"@mintlify/prebuild": ["@mintlify/prebuild@1.0.989", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.711", "@mintlify/validation": "0.1.662", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.0" } }, "sha512-4ywkAhn2+F0vDEO3MtVmrl89gGx4uG3+2cqHDZgTfI88/VLSyryzT1GJKEI8xDSZtTCJpM/2A18fvhw7kPPxsA=="],
"@mintlify/prebuild": ["@mintlify/prebuild@1.0.991", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.713", "@mintlify/validation": "0.1.663", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.0" } }, "sha512-pyQAWFYVmPnc20jpo6DnItmvHhJytKzqW/Q3GIYDqHsj/6JctcCfv07+Sc2m2bfu84kAd71yl1BIvXyBNFrD1Q=="],
"@mintlify/previewing": ["@mintlify/previewing@4.0.1050", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/prebuild": "1.0.989", "@mintlify/validation": "0.1.662", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.18.2", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.7.2", "tar": "6.1.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-0NHlSkQRIrYKyHBsG6YFcilIVy1/1iEVkpdOlZJ3lSP8Mo40iiYlzT475GmBNuNBWMSd0CcH2PCJUWptWNDsXQ=="],
"@mintlify/previewing": ["@mintlify/previewing@4.0.1052", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/prebuild": "1.0.991", "@mintlify/validation": "0.1.663", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.18.2", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.7.2", "tar": "6.1.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-3d3PkyAk+7ErNQN7zJfIRKjlkpU4iHNVTFQLd38hi5717y6FH6+RpyoVT1iBsM0q6ZqW5ZZeDvdvVTF9obqnCQ=="],
"@mintlify/scraping": ["@mintlify/scraping@4.0.522", "", { "dependencies": { "@mintlify/common": "1.0.661", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.21.4" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-PL2k52WT5S5OAgnT2K13bP7J2El6XwiVvQlrLvxDYw5KMMV+y34YVJI8ZscKb4trjitWDgyK0UTq2KN6NQgn6g=="],
"@mintlify/validation": ["@mintlify/validation@0.1.662", "", { "dependencies": { "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "arktype": "2.1.27", "js-yaml": "4.1.0", "lcm": "0.0.3", "lodash": "4.17.21", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.0", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-yCKiIDDAZjJ6AQVg8bpJ+jU/8a7pSmqjmJfAjNBuvDoNYi1XhRSnSGDtDTfl+Ubx1mbwF5ryEc4nCEQZ0VShwQ=="],
"@mintlify/validation": ["@mintlify/validation@0.1.663", "", { "dependencies": { "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "arktype": "2.1.27", "js-yaml": "4.1.0", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.0", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-MU2PECIyOJaS0VHokMp4snEERSBzeHdMtGAMteRr9gl76hTlRKmp+i/9GUQL1XxrGfzY9s2D30YJjsMUUq2Lyw=="],
"@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="],
@@ -1426,7 +1441,7 @@
"@playwright/test": ["@playwright/test@1.58.1", "", { "dependencies": { "playwright": "1.58.1" }, "bin": { "playwright": "cli.js" } }, "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w=="],
"@posthog/ai": ["@posthog/ai@7.13.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.43.0", "@langchain/core": "^1.1.29", "@posthog/core": "1.25.2", "langchain": "^1.2.28", "openai": "^6.25.0", "uuid": "^11.1.0", "zod": "^4.1.13" }, "peerDependencies": { "@ai-sdk/provider": "^2.0.0 || ^3.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", "posthog-node": "^5.0.0" }, "optionalPeers": ["@ai-sdk/provider", "@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http"] }, "sha512-1hnHYR7vvdIhg7gRcy77xFSuxRub8/xo9TsZze8q6u4XlHw+dJxyFTj0wKvzTxXZzZRmTXM0uLhgEWeZHuXmeA=="],
"@posthog/ai": ["@posthog/ai@7.14.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.43.0", "@langchain/core": "^1.1.29", "@posthog/core": "1.25.2", "langchain": "^1.2.28", "openai": "^6.25.0", "uuid": "^11.1.0", "zod": "^4.1.13" }, "peerDependencies": { "@ai-sdk/provider": "^2.0.0 || ^3.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", "posthog-node": "^5.0.0" }, "optionalPeers": ["@ai-sdk/provider", "@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http"] }, "sha512-gKWim9vwPEtFMg2zrzPyf3UVb7YeLcsWFA8sRWyKXvxh04O92bbpG+v3OR3g6zVmE8wCrmXdtEs14IDgnmCz5g=="],
"@posthog/core": ["@posthog/core@1.25.2", "", {}, "sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg=="],
@@ -1922,6 +1937,8 @@
"@supabase/storage-js": ["@supabase/storage-js@2.103.0", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-DHmlvdAXwtOmZNbkIZi4lkobPR3XjIzoOgzoz5duMf6G+sDeY015YrzMJCnqdccuYr7X5x4yYuSwF//RoN2dvQ=="],
"@supabase/stripe-sync-engine": ["@supabase/stripe-sync-engine@0.48.5", "", { "dependencies": { "pg": "^8.20.0", "pg-node-migrations": "0.0.8", "yesql": "^7.0.0" }, "peerDependencies": { "stripe": "> 18" } }, "sha512-+LbtJH8n5Xiu289AL3FuWFdKXd0K7kDF0z4Lm+zMYoImWmOuGd3TgSx9gm/nv4nzLooOmIxGZh6LojoYBcJM+g=="],
"@supabase/supabase-js": ["@supabase/supabase-js@2.103.0", "", { "dependencies": { "@supabase/auth-js": "2.103.0", "@supabase/functions-js": "2.103.0", "@supabase/postgrest-js": "2.103.0", "@supabase/realtime-js": "2.103.0", "@supabase/storage-js": "2.103.0" } }, "sha512-j/6q5+LtXbR/YOLSLhy7Na74RD1cV2v+KwIIuuqMEjk1JpLEEyu0ynwDHpGoxMncDQl+R5FogaVqZm+85lZvtw=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
@@ -1962,13 +1979,13 @@
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="],
"@tanstack/form-core": ["@tanstack/form-core@1.28.6", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog=="],
"@tanstack/form-core": ["@tanstack/form-core@1.29.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-uyeKEdJBfbj0bkBSwvSYVRtWLOaXvfNX3CeVw1HqGOXVLxpBBGAqWdYLc+UoX/9xcoFwFXrjR9QqMPzvwm2yyQ=="],
"@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="],
"@tanstack/query-core": ["@tanstack/query-core@5.85.6", "", {}, "sha512-hCj0TktzdCv2bCepIdfwqVwUVWb+GSHm1Jnn8w+40lfhQ3m7lCO7ADRUJy+2unxQ/nzjh2ipC6ye69NDW3l73g=="],
"@tanstack/react-form": ["@tanstack/react-form@1.28.6", "", { "dependencies": { "@tanstack/form-core": "1.28.6", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA=="],
"@tanstack/react-form": ["@tanstack/react-form@1.29.0", "", { "dependencies": { "@tanstack/form-core": "1.29.0", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-jj425NNX0QKqbUzqSNiYI3HCPHSk2df47acXCJyXczWOTmG81ECZGkgofgqamFsSU9kMiH6Di5RLUnftrlhWSw=="],
"@tanstack/react-query": ["@tanstack/react-query@5.85.6", "", { "dependencies": { "@tanstack/query-core": "5.85.6" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-VUAag4ERjh+qlmg0wNivQIVCZUrYndqYu3/wPCVZd4r0E+1IqotbeyGTc+ICroL/PqbpSaGZg02zSWYfcvxbdA=="],
@@ -3798,7 +3815,7 @@
"ksuid": ["ksuid@3.0.0", "", { "dependencies": { "base-convert-int-array": "^1.0.1" } }, "sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA=="],
"kysely": ["kysely@0.28.15", "", {}, "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA=="],
"kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="],
"langchain": ["langchain@1.3.1", "", { "dependencies": { "@langchain/langgraph": "^1.2.8", "@langchain/langgraph-checkpoint": "^1.0.1", "langsmith": ">=0.5.0 <1.0.0", "uuid": "^11.1.0", "zod": "^3.25.76 || ^4" }, "peerDependencies": { "@langchain/core": "^1.1.39" } }, "sha512-tJu8Ibf3NAuDW8pMT7VIBCc96m8TymjSMqRBpQawlG8zeTGPZK5TKA5gBYRNZvrz0YkJA57O2rcjP+mIxrS0+g=="],
@@ -4102,7 +4119,7 @@
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mintlify": ["mintlify@4.2.504", "", { "dependencies": { "@mintlify/cli": "4.0.1107" }, "bin": { "mintlify": "index.js" } }, "sha512-N7Upy3DHiuvVzith7MTI8cRRGuzlYgAAKjMgIeWi4Cz8JQVDH94NbJ+icl3gVhef6DRtZkmYHzpeZRpYATASpA=="],
"mintlify": ["mintlify@4.2.506", "", { "dependencies": { "@mintlify/cli": "4.0.1109" }, "bin": { "mintlify": "index.js" } }, "sha512-RpFXl57t0tX5NuPbw+69O7kKW2AFTHf5UdwPWHAd9NKXyPjj4K7FxXB1DlkEHyvp6Yhewr6LTjJigNFp4vrPQw=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
@@ -4368,6 +4385,8 @@
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-node-migrations": ["pg-node-migrations@0.0.8", "", { "dependencies": { "pg": "^8.6.0", "sql-template-strings": "^2.2.2" }, "bin": { "pg-validate-migrations": "dist/bin/validate.js" } }, "sha512-44cMl9umOmCv0hzZyEcvjEq8Bm8u7mrzggZ06qXTJVSsMMB4j2OsjG+rSp+uzeKWyP2Vu0K9Ye2wKtjFUJwrdw=="],
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
@@ -4864,6 +4883,8 @@
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"sql-template-strings": ["sql-template-strings@2.2.2", "", {}, "sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q=="],
"sqs-consumer": ["sqs-consumer@6.0.2", "", { "dependencies": { "@aws-sdk/client-sqs": "^3.226.0", "debug": "^4.3.4" } }, "sha512-Y8ztFBc1VPj4q72j9Uji4XR6n2O88n9X83aOUaCvz9zZqjpGJaI4PBHBPnE8RDBgD0EQR8Dh+sQwH/+8mzr+Bg=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
@@ -5374,6 +5395,8 @@
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"yesql": ["yesql@7.0.0", "", {}, "sha512-sosfr7agy4ibLM7BvXBkM6BpBmKMGuBO8DUYQEuey+QqaqrgW+2bsSg6D050ocBYIz0PuHxUyehyzEztZTU4pw=="],
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
@@ -5754,7 +5777,7 @@
"@mintlify/openapi-parser/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.711", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-e0tWdbZoxZ+/OKm29stpAoJkevL1hawpCdsTKhV5/9O8o4cTpLhEnAkS6fSlBv5t0k/Ut/FQeW10kIfgXj+Yww=="],
"@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.713", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-V0+gUqL3LgJygTL7PunRMaxirHp0oGKCLyFNmAQMJ3gX4WPUBYc+5GLiJ7hQn6lsp6m6wLmztVpeKdX8OwD8GA=="],
"@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="],
@@ -5928,20 +5951,30 @@
"@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="],
"@opentelemetry/instrumentation-amqplib/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-connect/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-connect/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-dataloader/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-express/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-express/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-fs/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-fs/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-generic-pool/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-graphql/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-hapi/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-hapi/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="],
@@ -5954,18 +5987,24 @@
"@opentelemetry/instrumentation-knex/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-koa/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-koa/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-lru-memoizer/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-mongodb/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-mongoose/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-mongoose/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-mysql/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-mysql2/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-pg/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
@@ -5974,6 +6013,8 @@
"@opentelemetry/instrumentation-tedious/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/instrumentation-undici/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@opentelemetry/instrumentation-undici/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="],
@@ -6014,6 +6055,8 @@
"@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="],
"@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@orpc/openapi/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
"@orpc/server/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
@@ -6078,8 +6121,14 @@
"@sentry/node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="],
"@sentry/node/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"@sentry/node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
"@sentry/node/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"@sentry/node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
"@sentry/node/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
"@sentry/node-core/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
@@ -6662,6 +6711,8 @@
"posthog-js/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="],
"posthog-js/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
"posthog-js/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="],
"prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
@@ -8138,6 +8189,8 @@
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="],
"posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
"posthog-js/@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
"posthog-js/@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],

View File

@@ -16,6 +16,7 @@ COPY packages/atmn/package.json ./packages/atmn/package.json
COPY packages/atmn-tests/package.json ./packages/atmn-tests/package.json
COPY packages/openapi/package.json ./packages/openapi/package.json
COPY packages/ksuid/package.json ./packages/ksuid/package.json
COPY packages/stripe-sync/package.json ./packages/stripe-sync/package.json
COPY packages/sdk/package.json ./packages/sdk/package.json
COPY apps/sdk-test/package.json ./apps/sdk-test/package.json
COPY apps/docs/package.json ./apps/docs/package.json

View File

@@ -15,7 +15,8 @@
"packages/sdk",
"packages/autumn-js",
"packages/openapi",
"packages/ksuid"
"packages/ksuid",
"packages/stripe-sync"
],
"catalog": {
"stripe": "19.3.0-beta.1",
@@ -108,7 +109,9 @@
"js:publish": "bun js:build && cd packages/autumn-js && npm publish",
"js:publish-beta": "bun js:build && cd packages/autumn-js && npm publish --tag beta",
"js:publish-dry": "bun js:build && cd packages/autumn-js && npm publish --dry-run",
"js:version": "git fetch --tags origin && git tag -l 'autumn-js-v*' --sort=-v:refname | head -1"
"js:version": "git fetch --tags origin && git tag -l 'autumn-js-v*' --sort=-v:refname | head -1",
"stripe-sync:migrate": "bun packages/stripe-sync/scripts/migrate.ts",
"stripe-sync:migrate:prod": "NODE_ENV=production infisical run --env=prod -- bun packages/stripe-sync/scripts/migrate.ts"
},
"dependencies": {
"@aws-sdk/client-sqs": "^3.985.0",

View File

@@ -0,0 +1,24 @@
{
"name": "@autumn/stripe-sync",
"version": "1.0.0",
"description": "Stripe-to-Postgres sync engine wrapper for Autumn",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts",
"default": "./src/index.ts"
}
},
"private": true,
"dependencies": {
"@supabase/stripe-sync-engine": "^0.48.5",
"pg": "^8.20.0",
"stripe": "catalog:"
},
"devDependencies": {
"@types/pg": "^8.20.0"
}
}

View File

@@ -0,0 +1,26 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { config } from "dotenv";
import { addAutumnColumns, runStripeSyncMigrations } from "../src/index.js";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, "../../..");
config({ path: path.join(repoRoot, "server/.env"), override: true });
const databaseUrl = process.env.STRIPE_SYNC_DATABASE_URL;
if (!databaseUrl) {
console.error("STRIPE_SYNC_DATABASE_URL is required");
process.exit(1);
}
console.log("Running stripe sync migrations...");
await runStripeSyncMigrations({ databaseUrl });
console.log("Stripe sync migrations complete");
console.log(
"Adding Autumn columns (stripe_account_id, org_id) to synced tables...",
);
await addAutumnColumns({ databaseUrl });
console.log("Autumn columns added");

View File

@@ -0,0 +1,38 @@
import pg from "pg";
import { SYNCED_TABLES } from "./eventTypeToTable.js";
/**
* Adds `stripe_account_id` and `org_id` columns + indexes to all synced tables.
* Idempotent -- safe to run multiple times.
*/
export const addAutumnColumns = async ({
databaseUrl,
schema = "stripe",
}: {
databaseUrl: string;
schema?: string;
}): Promise<void> => {
const client = new pg.Client({ connectionString: databaseUrl });
await client.connect();
try {
for (const table of SYNCED_TABLES) {
await client.query(`
ALTER TABLE "${schema}"."${table}"
ADD COLUMN IF NOT EXISTS stripe_account_id TEXT,
ADD COLUMN IF NOT EXISTS org_id TEXT,
ADD COLUMN IF NOT EXISTS env TEXT
`);
await client.query(`
CREATE INDEX IF NOT EXISTS idx_${table}_stripe_account_id
ON "${schema}"."${table}" (stripe_account_id)
`);
await client.query(`
CREATE INDEX IF NOT EXISTS idx_${table}_org_id
ON "${schema}"."${table}" (org_id)
`);
}
} finally {
await client.end();
}
};

View File

@@ -0,0 +1,61 @@
/**
* Maps a Stripe event type to the sync DB table that stores the object.
* Returns undefined for event types that don't map to a known table.
*/
export const eventTypeToTable = ({
eventType,
}: {
eventType: string;
}): string | undefined => {
if (eventType.startsWith("charge.dispute.")) return "disputes";
if (eventType.startsWith("charge.")) return "charges";
if (eventType.startsWith("checkout.session.")) return "checkout_sessions";
if (eventType.startsWith("customer.subscription.")) return "subscriptions";
if (eventType.startsWith("customer.tax_id.")) return "tax_ids";
if (eventType.startsWith("customer.")) return "customers";
if (eventType.startsWith("invoice.")) return "invoices";
if (eventType.startsWith("product.")) return "products";
if (eventType.startsWith("price.")) return "prices";
if (eventType.startsWith("plan.")) return "plans";
if (eventType.startsWith("setup_intent.")) return "setup_intents";
if (eventType.startsWith("subscription_schedule."))
return "subscription_schedules";
if (eventType.startsWith("payment_method.")) return "payment_methods";
if (eventType.startsWith("payment_intent.")) return "payment_intents";
if (eventType.startsWith("credit_note.")) return "credit_notes";
if (eventType.startsWith("radar.early_fraud_warning."))
return "early_fraud_warnings";
if (eventType.startsWith("refund.")) return "refunds";
if (eventType.startsWith("review.")) return "reviews";
if (eventType === "invoice_payment.paid") return "invoice_payments";
return undefined;
};
/** All tables in the stripe sync schema that store Stripe objects. */
export const SYNCED_TABLES = [
"charges",
"checkout_sessions",
"checkout_session_line_items",
"coupons",
"credit_notes",
"customers",
"disputes",
"early_fraud_warnings",
"events",
"invoices",
"invoice_payments",
"payment_intents",
"payment_methods",
"payouts",
"plans",
"prices",
"products",
"refunds",
"reviews",
"setup_intents",
"subscription_items",
"subscription_schedules",
"subscriptions",
"tax_ids",
] as const;

View File

@@ -0,0 +1,12 @@
export { addAutumnColumns } from "./addAccountIdColumn.js";
export { eventTypeToTable, SYNCED_TABLES } from "./eventTypeToTable.js";
export {
closeStripeSyncEngine,
getStripeSyncEngine,
processStripeSyncEvent,
} from "./initStripeSync.js";
export { runStripeSyncMigrations } from "./runStripeSyncMigrations.js";
export {
isSyncableEvent,
SYNCABLE_EVENT_PREFIXES,
} from "./syncableResources.js";

View File

@@ -0,0 +1,102 @@
import { StripeSync } from "@supabase/stripe-sync-engine";
import type Stripe from "stripe";
import { eventTypeToTable } from "./eventTypeToTable.js";
const SCHEMA = "stripe";
let instance: StripeSync | null = null;
let initAttempted = false;
/**
* Lazily creates a singleton StripeSync instance.
* Returns null if STRIPE_SYNC_DATABASE_URL is not configured or init fails.
*/
export const getStripeSyncEngine = (): StripeSync | null => {
if (instance) return instance;
if (initAttempted) return null;
initAttempted = true;
const databaseUrl = process.env.STRIPE_SYNC_DATABASE_URL;
const stripeSecretKey =
process.env.STRIPE_LIVE_SECRET_KEY || process.env.STRIPE_SANDBOX_SECRET_KEY;
if (!databaseUrl || !stripeSecretKey) return null;
try {
instance = new StripeSync({
poolConfig: {
connectionString: databaseUrl,
max: 5,
keepAlive: true,
connectionTimeoutMillis: 5_000,
idleTimeoutMillis: 30_000,
},
stripeSecretKey,
stripeWebhookSecret: "unused-processEvent-only",
schema: SCHEMA,
});
} catch {
instance = null;
}
return instance;
};
/**
* Upserts the Stripe event into the sync DB, then stamps the row
* with the originating Stripe account ID and org ID for multi-tenancy.
* Fully fail-open: any error is swallowed and returns silently.
*/
export const processStripeSyncEvent = async ({
event,
stripeAccountId,
orgId,
env,
}: {
event: Stripe.Event;
stripeAccountId?: string;
orgId?: string;
env?: string;
}): Promise<void> => {
const engine = getStripeSyncEngine();
if (!engine) return;
try {
await engine.processEvent(event);
} catch {
return;
}
const table = eventTypeToTable({ eventType: event.type });
if (!table) return;
const objectId = (event.data.object as { id?: string }).id;
if (!objectId) return;
const accountId = stripeAccountId ?? event.account ?? null;
if (!accountId && !orgId && !env) return;
try {
await engine.postgresClient.pool.query(
`UPDATE "${SCHEMA}"."${table}" SET stripe_account_id = COALESCE($1, stripe_account_id), org_id = COALESCE($2, org_id), env = COALESCE($3, env) WHERE id = $4`,
[accountId, orgId, env, objectId],
);
} catch {
// Fail-open: metadata stamp is best-effort
}
};
/** Gracefully close the sync engine's PG pool (call on server shutdown). */
export const closeStripeSyncEngine = async (): Promise<void> => {
if (!instance) return;
try {
await instance.close();
} catch {
// Best-effort cleanup
}
instance = null;
initAttempted = false;
};

View File

@@ -0,0 +1,15 @@
import { runMigrations } from "@supabase/stripe-sync-engine";
/**
* Run stripe-sync-engine migrations against the sync DB.
* Idempotent -- safe to run multiple times.
*/
export const runStripeSyncMigrations = async ({
databaseUrl,
schema = "stripe",
}: {
databaseUrl: string;
schema?: string;
}): Promise<void> => {
await runMigrations({ databaseUrl, schema });
};

View File

@@ -0,0 +1,16 @@
export const SYNCABLE_EVENT_PREFIXES = [
"customer.subscription.",
"payment_intent.",
"invoice.",
"customer.",
"product.",
"price.",
] as const;
export const isSyncableEvent = ({
eventType,
}: {
eventType: string;
}): boolean => {
return SYNCABLE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
};

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"strict": true,
"composite": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"module": "Preserve",
"moduleResolution": "bundler",
"target": "ES2020",
"outDir": "./dist",
"rootDir": "."
},
"include": ["src/**/*"]
}

View File

@@ -24,17 +24,18 @@ const main = async () => {
const { db } = initDrizzle();
const query = getFullCusQuery(
customerId,
const query = getFullCusQuery({
idOrInternalId: customerId,
orgId,
env,
RELEVANT_STATUSES,
true, // includeInvoices
true, // withEntities
false, // withTrialsUsed
true, // withSubs
false, // withEvents
);
inStatuses: RELEVANT_STATUSES,
includeInvoices: true,
withEntities: true,
withTrialsUsed: false,
withSubs: true,
withEvents: false,
cusProductLimit: 15,
});
// Run the actual query to measure wall-clock time
console.log("--- Running query ---");

View File

@@ -108,6 +108,7 @@ const main = async () => {
limit: config.limit,
offset: config.offset,
search: config.search,
cusProductLimit: 15,
});
const filteredCountQuery = getFilteredCountQuery({
orgId: config.orgId,

View File

@@ -46,6 +46,7 @@
"@anthropic-ai/sdk": "^0.32.1",
"@autumn/ksuid": "workspace:*",
"@autumn/shared": "workspace:*",
"@autumn/stripe-sync": "workspace:*",
"@aws-sdk/client-s3": "^3.1017.0",
"@aws-sdk/client-scheduler": "^3.1004.0",
"@aws-sdk/client-sqs": "^3.958.0",

View File

@@ -1,29 +1,23 @@
import "dotenv/config";
import { loadLocalEnv } from "./src/utils/envUtils";
import Stripe from "stripe";
import {
MAIN_STRIPE_EVENT_TYPES,
SYNC_STRIPE_EVENT_TYPES,
} from "./src/external/stripe/common/stripeConstants";
loadLocalEnv();
const allEventTypes = [
...new Set([...MAIN_STRIPE_EVENT_TYPES, ...SYNC_STRIPE_EVENT_TYPES]),
] as Stripe.WebhookEndpointCreateParams.EnabledEvent[];
const main = async () => {
const stripe = new Stripe(process.env.STRIPE_SANDBOX_SECRET_KEY || "");
const result = await stripe.webhookEndpoints.create({
url: `${process.env.STRIPE_WEBHOOK_URL}/webhooks/connect/sandbox`,
enabled_events: [
"checkout.session.completed",
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted",
"customer.discount.deleted",
"invoice.paid",
"invoice.upcoming",
"invoice.created",
"invoice.finalized",
"invoice.updated",
"subscription_schedule.canceled",
"subscription_schedule.updated",
],
enabled_events: allEventTypes,
connect: true,
});

View File

@@ -2,6 +2,7 @@ export const ADMIN_REQUEST_BLOCK_CONFIG_KEY = "admin/request-block-config.json";
export const ADMIN_FEATURE_FLAGS_CONFIG_KEY = "admin/feature-flags-config.json";
export const ADMIN_CUSTOMER_BLOCK_CONFIG_KEY =
"admin/customer-block-config.json";
export const ADMIN_ORG_LIMITS_CONFIG_KEY = "admin/org-limits-config.json";
type AdminS3Target = "dev" | "prod";

View File

@@ -0,0 +1,64 @@
import type Stripe from "stripe";
type StripeEventType = Stripe.WebhookEndpointCreateParams.EnabledEvent;
/** Events Autumn actively handles in its webhook handler. */
export const MAIN_STRIPE_EVENT_TYPES: StripeEventType[] = [
"checkout.session.completed",
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted",
"customer.discount.deleted",
"invoice.paid",
"invoice.upcoming",
"invoice.created",
"invoice.finalized",
"invoice.updated",
"subscription_schedule.canceled",
"subscription_schedule.updated",
];
/** Additional events needed to keep the stripe-sync DB up to date. */
export const SYNC_STRIPE_EVENT_TYPES: StripeEventType[] = [
// customers
"customer.created",
"customer.updated",
"customer.deleted",
// subscriptions (extras beyond main)
"customer.subscription.paused",
"customer.subscription.resumed",
// subscription schedules (extras beyond main)
"subscription_schedule.created",
"subscription_schedule.completed",
"subscription_schedule.released",
// payment methods
"payment_method.attached",
"payment_method.detached",
"payment_method.updated",
// products
"product.created",
"product.updated",
"product.deleted",
// prices
"price.created",
"price.updated",
"price.deleted",
// invoices (extras beyond main)
"invoice.deleted",
"invoice.payment_failed",
"invoice.payment_succeeded",
"invoice.voided",
"invoice.marked_uncollectible",
// payment intents
"payment_intent.created",
"payment_intent.succeeded",
"payment_intent.payment_failed",
"payment_intent.canceled",
];

View File

@@ -1,6 +1,10 @@
import { type AppEnv, ErrCode } from "@autumn/shared";
import Stripe from "stripe";
import RecaseError from "@/utils/errorUtils.js";
import {
MAIN_STRIPE_EVENT_TYPES,
SYNC_STRIPE_EVENT_TYPES,
} from "./common/stripeConstants";
export const checkKeyValid = async (apiKey: string) => {
const stripe = new Stripe(apiKey);
@@ -29,20 +33,22 @@ export const createWebhookEndpoint = async (
const endpoint = await stripe.webhookEndpoints.create({
url: `${webhookBaseUrl}/webhooks/stripe/${orgId}/${env}`,
enabled_events: [
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted",
"checkout.session.completed",
"invoice.paid",
"invoice.upcoming",
"invoice.created",
"invoice.finalized",
"invoice.updated",
"subscription_schedule.canceled",
"subscription_schedule.updated",
"customer.discount.deleted",
],
enabled_events: [...MAIN_STRIPE_EVENT_TYPES, ...SYNC_STRIPE_EVENT_TYPES],
// [
// "customer.subscription.created",
// "customer.subscription.updated",
// "customer.subscription.deleted",
// "checkout.session.completed",
// "invoice.paid",
// "invoice.upcoming",
// "invoice.created",
// "invoice.finalized",
// "invoice.updated",
// "subscription_schedule.canceled",
// "subscription_schedule.updated",
// "customer.discount.deleted",
// ],
});
return endpoint;

View File

@@ -4,6 +4,7 @@ import { handleStripeWebhookEvent } from "./handleStripeWebhookEvent.js";
import { stripeConnectSeederMiddleware } from "./webhookMiddlewares/stripeConnectSeederMiddleware.js";
import { stripeIdempotencyMiddleware } from "./webhookMiddlewares/stripeIdempotencyMiddleware.js";
import { stripeLegacySeederMiddleware } from "./webhookMiddlewares/stripeLegacySeederMiddleware.js";
import { stripeSyncMiddleware } from "./webhookMiddlewares/stripeSyncMiddleware.js";
import { stripeToAutumnCustomerMiddleware } from "./webhookMiddlewares/stripeToAutumnCustomerMiddleware.js";
import type { StripeWebhookHonoEnv } from "./webhookMiddlewares/stripeWebhookContext.js";
import { stripeWebhookRefreshMiddleware } from "./webhookMiddlewares/stripeWebhookRefreshMiddleware.js";
@@ -15,6 +16,7 @@ stripeWebhookRouter.post(
"/webhooks/stripe/:orgId/:env",
stripeLegacySeederMiddleware,
stripeWebhookRefreshMiddleware,
stripeSyncMiddleware,
stripeToAutumnCustomerMiddleware,
stripeLoggerMiddleware,
stripeIdempotencyMiddleware,
@@ -26,6 +28,7 @@ stripeWebhookRouter.post(
"/webhooks/connect/:env",
stripeConnectSeederMiddleware,
stripeWebhookRefreshMiddleware,
stripeSyncMiddleware,
stripeToAutumnCustomerMiddleware,
stripeLoggerMiddleware,
stripeIdempotencyMiddleware,

View File

@@ -0,0 +1,54 @@
import { isSyncableEvent, processStripeSyncEvent } from "@autumn/stripe-sync";
import type { Context, Next } from "hono";
import { isStripeSyncEnabled } from "@/internal/misc/stripeSync/stripeSyncStore.js";
import type {
StripeWebhookContext,
StripeWebhookHonoEnv,
} from "./stripeWebhookContext.js";
/**
* Post-handler middleware that syncs Stripe events to the sync DB.
* Fire-and-forget -- errors are caught and logged, never propagated.
*/
export const stripeSyncMiddleware = async (
c: Context<StripeWebhookHonoEnv>,
next: Next,
) => {
await next();
const ctx = c.get("ctx") as StripeWebhookContext;
const { logger, org, stripeEvent } = ctx;
if (!org || !stripeEvent) return;
if (
process.env.NODE_ENV === "production" &&
!isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug })
)
return;
if (!isSyncableEvent({ eventType: stripeEvent.type })) return;
try {
const stripeAccountId = stripeEvent.account ?? undefined;
void processStripeSyncEvent({
event: stripeEvent,
stripeAccountId,
orgId: org.id,
env: ctx.env,
}).catch((error) => {
logger.error(`Stripe sync failed for event ${stripeEvent.id}: ${error}`, {
error: {
message: error instanceof Error ? error.message : String(error),
},
data: {
eventId: stripeEvent.id,
eventType: stripeEvent.type,
orgId: org.id,
},
});
});
} catch (error) {
logger.error(`Stripe sync middleware error: ${error}`);
}
};

View File

@@ -20,6 +20,9 @@ import {
import "./internal/misc/requestBlocks/requestBlockStore.js";
import "./internal/misc/featureFlags/featureFlagStore.js";
import "./internal/misc/customerBlocks/customerBlockStore.js";
import "./internal/misc/edgeConfig/orgLimitsStore.js";
import "./internal/misc/stripeSync/stripeSyncStore.js";
import { closeStripeSyncEngine } from "@autumn/stripe-sync";
import { warmupRegionalRedis } from "./external/redis/initRedis.js";
import { createHonoApp } from "./initHono.js";
import { otelSdk } from "./instrumentation.js";
@@ -117,6 +120,7 @@ async function gracefulShutdown() {
client.end(),
clientCritical.end(),
clientReplica?.end(),
closeStripeSyncEngine(),
]);
console.log("Shutdown complete. Exiting process.");
process.exit(0);

View File

@@ -2,8 +2,11 @@ import { Hono } from "hono";
import type { HonoEnv } from "../../honoUtils/HonoEnv";
import { handleGetAdminCustomerBlockConfig } from "./handleGetAdminCustomerBlockConfig";
import { handleGetAdminFeatureFlagsConfig } from "./handleGetAdminFeatureFlagsConfig";
import { handleGetAdminOrgLimitsConfig } from "./handleGetAdminOrgLimitsConfig";
import { handleGetAdminOrgRequestBlock } from "./handleGetAdminOrgRequestBlock";
import { handleGetAdminRequestBlockConfig } from "./handleGetAdminRequestBlockConfig";
import { handleGetAdminStripeSyncConfig } from "./handleGetAdminStripeSyncConfig";
import { handleGetInvoiceLineItems } from "./handleGetInvoiceLineItems";
import { handleGetMasterStripeAccount } from "./handleGetMasterStripeAccount";
import { handleGetOrgMember } from "./handleGetOrgMember";
@@ -12,8 +15,10 @@ import { handleListAdminUsers } from "./handleListAdminUsers";
import { handleListOAuthClients } from "./handleListOAuthClients";
import { handleUpsertAdminCustomerBlockConfig } from "./handleUpsertAdminCustomerBlockConfig";
import { handleUpsertAdminFeatureFlagsConfig } from "./handleUpsertAdminFeatureFlagsConfig";
import { handleUpsertAdminOrgLimitsConfig } from "./handleUpsertAdminOrgLimitsConfig";
import { handleUpsertAdminOrgRequestBlock } from "./handleUpsertAdminOrgRequestBlock";
import { handleUpsertAdminRequestBlockConfig } from "./handleUpsertAdminRequestBlockConfig";
import { handleUpsertAdminStripeSyncConfig } from "./handleUpsertAdminStripeSyncConfig";
export const honoAdminRouter = new Hono<HonoEnv>();
@@ -51,6 +56,13 @@ honoAdminRouter.put(
"/customer-block-config",
...handleUpsertAdminCustomerBlockConfig,
);
honoAdminRouter.get("/org-limits-config", ...handleGetAdminOrgLimitsConfig);
honoAdminRouter.put("/org-limits-config", ...handleUpsertAdminOrgLimitsConfig);
honoAdminRouter.get("/stripe-sync-config", ...handleGetAdminStripeSyncConfig);
honoAdminRouter.put(
"/stripe-sync-config",
...handleUpsertAdminStripeSyncConfig,
);
honoAdminRouter.get("/org-member", ...handleGetOrgMember);
honoAdminRouter.get("/master-stripe-account", ...handleGetMasterStripeAccount);
honoAdminRouter.get("/oauth-clients", ...handleListOAuthClients);

View File

@@ -0,0 +1,20 @@
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import {
getOrgLimitsConfigFromSource,
getRuntimeOrgLimitsStatus,
} from "@/internal/misc/edgeConfig/orgLimitsStore.js";
export const handleGetAdminOrgLimitsConfig = createRoute({
handler: async (c) => {
const status = getRuntimeOrgLimitsStatus();
const config = await getOrgLimitsConfigFromSource();
return c.json({
...config,
configHealthy: status.healthy,
configConfigured: status.configured,
lastSuccessAt: status.lastSuccessAt ?? null,
error: status.error ?? null,
});
},
});

View File

@@ -0,0 +1,20 @@
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import {
getRuntimeStripeSyncStatus,
getStripeSyncConfigFromSource,
} from "@/internal/misc/stripeSync/stripeSyncStore.js";
export const handleGetAdminStripeSyncConfig = createRoute({
handler: async (c) => {
const status = getRuntimeStripeSyncStatus();
const config = await getStripeSyncConfigFromSource();
return c.json({
...config,
configHealthy: status.healthy,
configConfigured: status.configured,
lastSuccessAt: status.lastSuccessAt ?? null,
error: status.error ?? null,
});
},
});

View File

@@ -0,0 +1,14 @@
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { OrgLimitsConfigSchema } from "@/internal/misc/edgeConfig/orgLimitsSchemas.js";
import { updateFullOrgLimitsConfig } from "@/internal/misc/edgeConfig/orgLimitsStore.js";
export const handleUpsertAdminOrgLimitsConfig = createRoute({
body: OrgLimitsConfigSchema,
handler: async (c) => {
const body = c.req.valid("json");
await updateFullOrgLimitsConfig({ config: body });
return c.json({ success: true });
},
});

View File

@@ -0,0 +1,14 @@
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { StripeSyncConfigSchema } from "@/internal/misc/stripeSync/stripeSyncSchemas.js";
import { updateFullStripeSyncConfig } from "@/internal/misc/stripeSync/stripeSyncStore.js";
export const handleUpsertAdminStripeSyncConfig = createRoute({
body: StripeSyncConfigSchema,
handler: async (c) => {
const body = c.req.valid("json");
await updateFullStripeSyncConfig({ config: body });
return c.json({ success: true });
},
});

View File

@@ -11,6 +11,7 @@ import {
} from "@autumn/shared";
import * as Sentry from "@sentry/bun";
import type { AutumnContext, RequestContext } from "@/honoUtils/HonoEnv.js";
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
import { triggerBatchResetCustomerEntitlements } from "./actions/resetCustomerEntitlements/triggerBatchResetCustomerEntitlements.js";
import { getApiCustomerBase } from "./cusUtils/apiCusUtils/getApiCustomerBase.js";
import { getPaginatedFullCusQuery } from "./getFullCusQuery.js";
@@ -24,6 +25,11 @@ export class CusBatchService {
internalCustomerIds: string[];
}) {
const { org, env, db } = ctx;
const cusProductLimit = getOrgCusProductLimit({
orgId: ctx.org.id,
orgSlug: ctx.org.slug,
});
const query = getPaginatedFullCusQuery({
orgId: ctx.org.id,
env: ctx.env,
@@ -34,6 +40,7 @@ export class CusBatchService {
limit: internalCustomerIds.length || 100,
offset: 0,
internalCustomerIds,
cusProductLimit,
});
const results = await db.execute(query);
const fullCustomers = results as unknown as FullCustomer[];
@@ -66,6 +73,10 @@ export class CusBatchService {
const { limit, offset, plans, subscription_status, search } = query;
const cusProductLimit = getOrgCusProductLimit({
orgId: ctx.org.id,
orgSlug: ctx.org.slug,
});
const sqlQuery = getPaginatedFullCusQuery({
orgId: ctx.org.id,
env: ctx.env,
@@ -80,6 +91,7 @@ export class CusBatchService {
offset,
search,
plans,
cusProductLimit,
});
const results = await ctx.db.execute(sqlQuery);
const finals = [];

View File

@@ -8,6 +8,7 @@ import {
import {
and,
asc,
desc,
eq,
gt,
@@ -21,6 +22,7 @@ import {
} from "drizzle-orm";
import { alias } from "drizzle-orm/pg-core";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
// Create alias for subquery
const customerProductsAlias = alias(customerProducts, "cp_alias");
@@ -47,6 +49,7 @@ const productFields = {
id: products.id,
name: products.name,
version: products.version,
is_add_on: products.is_add_on,
};
interface SearchFilters {
@@ -60,25 +63,21 @@ export class CusSearchService {
static async searchByProduct({
db,
orgId,
orgSlug,
env,
search,
filters,
pageSize = 50,
pageNumber,
// lastItem,
}: {
db: DrizzleCli;
orgId: string;
orgSlug?: string;
env: AppEnv;
search: string;
filters: SearchFilters;
pageSize?: number;
pageNumber: number;
// lastItem?: {
// internal_id: string;
// created_at?: string;
// name?: string;
// } | null;
}) {
// If we have a lastItem with only internal_id, fetch the full customer data for cursor pagination
// let resolvedLastItem = lastItem;
@@ -304,14 +303,14 @@ export class CusSearchService {
const offset = (pageNumber - 1) * pageSize;
productQueryResult = buildQuery()
.where(whereClause)
.orderBy(desc(customers.internal_id))
.orderBy(desc(customers.internal_id), asc(products.is_add_on))
.offset(offset)
.limit(pageSize);
} else {
// Use cursor-based pagination
productQueryResult = buildQuery()
.where(whereClause)
.orderBy(desc(customers.internal_id))
.orderBy(desc(customers.internal_id), asc(products.is_add_on))
.limit(pageSize);
}
@@ -375,7 +374,14 @@ export class CusSearchService {
}
}
const cusProductLimit = getOrgCusProductLimit({ orgId, orgSlug });
const processedData = Array.from(customerMap.values());
for (const customer of processedData) {
customer.customer_products = customer.customer_products.slice(
0,
cusProductLimit,
);
}
const totalCount = totalCountResult[0]?.totalCount || 0;
@@ -495,9 +501,7 @@ export class CusSearchService {
return sql`(${customers.processors}->>'vercel' IS NOT NULL)`;
return undefined;
})
.filter(
(c): c is NonNullable<typeof c> => c !== undefined,
),
.filter((c): c is NonNullable<typeof c> => c !== undefined),
)
: undefined,
),
@@ -510,6 +514,7 @@ export class CusSearchService {
static async search({
db,
orgId,
orgSlug,
env,
search,
pageSize = 50,
@@ -519,6 +524,7 @@ export class CusSearchService {
}: {
db: DrizzleCli;
orgId: string;
orgSlug?: string;
env: AppEnv;
search: string;
lastItem?: {
@@ -570,7 +576,6 @@ export class CusSearchService {
search,
filters,
pageSize,
// lastItem: resolvedLastItem,
pageNumber,
});
}
@@ -583,11 +588,11 @@ export class CusSearchService {
return await CusSearchService.searchByProduct({
db,
orgId,
orgSlug,
env,
search,
filters,
pageSize,
// lastItem: resolvedLastItem,
pageNumber,
});
}
@@ -685,7 +690,7 @@ export class CusSearchService {
products,
eq(customerProducts.internal_product_id, products.internal_id),
)
.orderBy(desc(baseQuery.internal_id)),
.orderBy(desc(baseQuery.internal_id), asc(products.is_add_on)),
totalCountQuery,
]);
@@ -718,7 +723,14 @@ export class CusSearchService {
}
}
const cusProductLimit = getOrgCusProductLimit({ orgId, orgSlug });
const finalResults = Array.from(customerMap.values());
for (const customer of finalResults) {
customer.customer_products = customer.customer_products.slice(
0,
cusProductLimit,
);
}
return { data: finalResults, count: totalCount };
}

View File

@@ -6,6 +6,7 @@ import {
CustomerNotFoundError,
customerProducts,
customers,
type Entity,
type EntityExpand,
ErrCode,
type FullCusProduct,
@@ -33,6 +34,7 @@ import { executeWithHealthTracking } from "@/db/pgHealthMonitor.js";
import type { RepoContext } from "@/db/repoContext.js";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import { withSpan } from "../analytics/tracer/spanUtils.js";
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
import { resetCustomerEntitlements } from "./actions/resetCustomerEntitlements/resetCustomerEntitlements.js";
import {
ACTIVE_STATUSES,
@@ -86,7 +88,11 @@ export class CusService {
withSubs,
},
fn: async () => {
const query = getFullCusQuery(
const cusProductLimit = getOrgCusProductLimit({
orgId,
orgSlug: org.slug,
});
const query = getFullCusQuery({
idOrInternalId,
orgId,
env,
@@ -97,7 +103,8 @@ export class CusService {
withSubs,
withEvents,
entityId,
);
cusProductLimit,
});
if (explain) {
const explainQuery = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`;
@@ -144,10 +151,18 @@ export class CusService {
.slice(0, 5);
}
// Skip reset when reading from replica — it writes to primary,
// and replica data is stale anyway. When degraded WITHOUT a replica
// (falls back to primary), the reset should still run.
if (orgId === "GG6tnmO7cHb40PNhwYBTZtxQdeL74NHF") {
fullCus.customer_products = (
fullCus.customer_products as FullCusProduct[]
)
.sort((a, b) => b.customer_prices.length - a.customer_prices.length)
.slice(0, 5);
fullCus.entities = (fullCus.entities as Entity[]).slice(0, 50);
}
if (!usedReplica && !skipReset) {
// Skip reset only when executeWithHealthTracking explicitly chose the
// replica. Lazy reset writes themselves go through dbGeneral.
await resetCustomerEntitlements({
fullCus,
ctx,
@@ -318,22 +333,21 @@ export class CusService {
? [query.subscription_status as CusProductStatus]
: ACTIVE_STATUSES;
const processorFilter =
query?.processors?.length
? or(
...query.processors
.map((proc) => {
if (proc === "stripe")
return sql`(${customers.processor}->>'id' IS NOT NULL)`;
if (proc === "revenuecat")
return sql`(${customers.processors}->>'revenuecat' IS NOT NULL)`;
if (proc === "vercel")
return sql`(${customers.processors}->>'vercel' IS NOT NULL)`;
return undefined;
})
.filter((c): c is NonNullable<typeof c> => c !== undefined),
)
: undefined;
const processorFilter = query?.processors?.length
? or(
...query.processors
.map((proc) => {
if (proc === "stripe")
return sql`(${customers.processor}->>'id' IS NOT NULL)`;
if (proc === "revenuecat")
return sql`(${customers.processors}->>'revenuecat' IS NOT NULL)`;
if (proc === "vercel")
return sql`(${customers.processors}->>'vercel' IS NOT NULL)`;
return undefined;
})
.filter((c): c is NonNullable<typeof c> => c !== undefined),
)
: undefined;
if (!query?.plans?.length) {
const [result] = await ctx.db

View File

@@ -8,10 +8,27 @@ import { executeBillingPlan } from "@/internal/billing/v2/execute/executeBilling
import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan";
import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan";
import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult";
import { buildBillingLockKey } from "@/internal/billing/v2/utils/billingLock/buildBillingLockKey";
import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan";
export const handleCancelV2 = createRoute({
// body: CancelBodySchema,
lock:
process.env.NODE_ENV !== "development"
? {
ttlMs: 120000,
errorMessage:
"Cancel already in progress for this customer, try again in a few seconds",
getKey: (c) => {
const ctx = c.get("ctx");
if (!ctx.customerId) return null;
return buildBillingLockKey({
orgId: ctx.org.id,
env: ctx.env,
customerId: ctx.customerId,
});
},
}
: undefined,
handler: async (c) => {
const ctx = c.get("ctx");

View File

@@ -149,6 +149,10 @@ export const getCachedFullCustomer = async ({
data: JSON.parse(cached),
});
// if (fullCustomer.entities?.[0] === null) {
// fullCustomer.entities = fullCustomer.entities?.filter((e) => e !== null);
// }
if (entityId) {
fullCustomer.entity = fullCustomer.entities?.find((e) => e.id === entityId);
} else {

View File

@@ -6,7 +6,13 @@ import {
} from "@autumn/shared";
import { type SQL, sql } from "drizzle-orm";
const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
const buildOptimizedCusProductsCTE = ({
inStatuses,
cusProductLimit,
}: {
inStatuses?: CusProductStatus[];
cusProductLimit: number;
}) => {
const withStatusFilter = () => {
return inStatuses
? sql`AND cp.status = ANY(ARRAY[${sql.join(
@@ -86,8 +92,8 @@ const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
) ft_data ON true
WHERE cp.internal_customer_id = (SELECT internal_id FROM customer_record)
${withStatusFilter()}
ORDER BY ${relevantStatusFirst}, cp.created_at DESC
LIMIT 15
ORDER BY ${relevantStatusFirst}, prod.is_add_on ASC, cp.created_at DESC
LIMIT ${cusProductLimit}
)
`;
};
@@ -101,7 +107,7 @@ const buildEntitiesCTE = (withEntities: boolean) => {
customer_entities AS (
SELECT
COALESCE(
json_agg(row_to_json(e) ORDER BY e.internal_id DESC),
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.internal_id IS NOT NULL),
'[]'::json
) AS entities
FROM (
@@ -266,18 +272,31 @@ const buildInvoicesCTE = (hasEntityCTE: boolean) => {
`;
};
export const getFullCusQuery = (
idOrInternalId: string,
orgId: string,
env: AppEnv,
inStatuses: CusProductStatus[],
includeInvoices: boolean,
withEntities: boolean,
withTrialsUsed: boolean,
withSubs: boolean,
withEvents: boolean,
entityId?: string,
) => {
export const getFullCusQuery = ({
idOrInternalId,
orgId,
env,
inStatuses,
includeInvoices,
withEntities,
withTrialsUsed,
withSubs,
withEvents,
entityId,
cusProductLimit,
}: {
idOrInternalId: string;
orgId: string;
env: AppEnv;
inStatuses: CusProductStatus[];
includeInvoices: boolean;
withEntities: boolean;
withTrialsUsed: boolean;
withSubs: boolean;
withEvents: boolean;
entityId?: string;
cusProductLimit: number;
}) => {
const sqlChunks: SQL[] = [];
// Step 1: Get customer record
@@ -309,7 +328,7 @@ export const getFullCusQuery = (
// Add customer products CTE
sqlChunks.push(sql`, `);
// sqlChunks.push(buildCusProductsCTE(inStatuses));
sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
sqlChunks.push(buildOptimizedCusProductsCTE({ inStatuses, cusProductLimit }));
// Conditionally add trials used CTE
if (withTrialsUsed) {
@@ -429,6 +448,7 @@ export const getPaginatedFullCusQuery = ({
internalCustomerIds,
plans,
search,
cusProductLimit,
}: {
orgId: string;
env: AppEnv;
@@ -444,6 +464,7 @@ export const getPaginatedFullCusQuery = ({
internalCustomerIds?: string[];
plans?: ListCustomersV2Params["plans"];
search?: string;
cusProductLimit: number;
}) => {
const withStatusFilter = () => {
return inStatuses?.length
@@ -539,8 +560,8 @@ export const getPaginatedFullCusQuery = ({
FROM customer_products cp
WHERE cp.internal_customer_id = cr.internal_id
${withStatusFilter()}
ORDER BY cp.created_at DESC
LIMIT 15
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
LIMIT ${cusProductLimit}
) cp ON true
JOIN products prod ON cp.internal_product_id = prod.internal_id
LEFT JOIN LATERAL (
@@ -659,7 +680,7 @@ export const getPaginatedFullCusQuery = ({
SELECT
cr.internal_id AS internal_customer_id,
COALESCE(
json_agg(row_to_json(e) ORDER BY e.internal_id DESC),
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.internal_id IS NOT NULL),
'[]'::json
) AS entities
FROM customer_records cr

View File

@@ -24,6 +24,7 @@ export const handleSearchCustomers = createRoute({
const { data: customers, count } = await CusSearchService.search({
db,
orgId: org.id,
orgSlug: org.slug,
env,
search: search ?? "",
filters,

View File

@@ -0,0 +1,14 @@
import { z } from "zod/v4";
export const OrgLimitsConfigSchema = z.object({
orgs: z
.record(
z.string(),
z.object({
maxCusProducts: z.number().min(1).optional(),
}),
)
.default({}),
});
export type OrgLimitsConfig = z.infer<typeof OrgLimitsConfigSchema>;

View File

@@ -0,0 +1,65 @@
import { ADMIN_ORG_LIMITS_CONFIG_KEY } from "@/external/aws/s3/adminS3Config.js";
import { registerEdgeConfig } from "@/internal/misc/edgeConfig/edgeConfigRegistry.js";
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
import {
type OrgLimitsConfig,
OrgLimitsConfigSchema,
} from "./orgLimitsSchemas.js";
export const DEFAULT_CUS_PRODUCT_LIMIT = 15;
const store = createEdgeConfigStore<OrgLimitsConfig>({
s3Key: ADMIN_ORG_LIMITS_CONFIG_KEY,
schema: OrgLimitsConfigSchema,
defaultValue: () => ({ orgs: {} }),
pollIntervalMs: 30_000,
});
registerEdgeConfig({ store });
export const getRuntimeOrgLimitsStatus = () => store.getStatus();
export const getOrgCusProductLimit = ({
orgId,
orgSlug,
}: {
orgId?: string;
orgSlug?: string;
}): number => {
const orgs = store.get().orgs;
const orgConfig =
(orgId ? orgs[orgId] : undefined) ??
(orgSlug ? orgs[orgSlug] : undefined);
return orgConfig?.maxCusProducts ?? DEFAULT_CUS_PRODUCT_LIMIT;
};
export const getOrgLimitsConfigFromSource = async () => {
return await store.readFromSource();
};
export const updateOrgLimitsInSource = async ({
orgId,
maxCusProducts,
}: {
orgId: string;
maxCusProducts?: number;
}) => {
const config = await store.readFromSource();
if (maxCusProducts === undefined) {
delete config.orgs[orgId];
} else {
config.orgs[orgId] = { maxCusProducts };
}
await store.writeToSource({ config });
return config.orgs[orgId];
};
export const updateFullOrgLimitsConfig = async ({
config,
}: {
config: OrgLimitsConfig;
}) => {
await store.writeToSource({ config });
};

View File

@@ -0,0 +1,7 @@
import { z } from "zod/v4";
export const StripeSyncConfigSchema = z.object({
enabledOrgIds: z.array(z.string()).default([]),
});
export type StripeSyncConfig = z.infer<typeof StripeSyncConfigSchema>;

View File

@@ -0,0 +1,39 @@
import { registerEdgeConfig } from "@/internal/misc/edgeConfig/edgeConfigRegistry.js";
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
import {
type StripeSyncConfig,
StripeSyncConfigSchema,
} from "./stripeSyncSchemas.js";
const store = createEdgeConfigStore<StripeSyncConfig>({
s3Key: "admin/stripe-sync-config.json",
schema: StripeSyncConfigSchema,
defaultValue: () => ({ enabledOrgIds: [] }),
pollIntervalMs: 60_000,
});
registerEdgeConfig({ store });
/** Pure in-memory lookup -- zero I/O, sync. Matches on org ID or slug. */
export const isStripeSyncEnabled = ({
orgId,
orgSlug,
}: {
orgId: string;
orgSlug?: string;
}): boolean => {
const enabled = store.get().enabledOrgIds;
return enabled.includes(orgId) || (!!orgSlug && enabled.includes(orgSlug));
};
export const getRuntimeStripeSyncStatus = () => store.getStatus();
export const getStripeSyncConfigFromSource = async () => store.readFromSource();
export const updateFullStripeSyncConfig = async ({
config,
}: {
config: StripeSyncConfig;
}) => {
await store.writeToSource({ config });
};

View File

@@ -1,67 +1,106 @@
import { expect, test } from "bun:test";
import { test } from "bun:test";
import type { AttachParamsV1Input } from "@autumn/shared";
import {
applySubscriptionDiscount,
createPercentCoupon,
getStripeSubscription,
} from "@tests/integration/billing/utils/discounts/discountTestUtils";
import { TestFeature } from "@tests/setup/v2Features.js";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
import { CusService } from "@/internal/customers/CusService";
test(`${chalk.yellowBright("check where default payment method lives after attach")}`, async () => {
const customerId = "pm-location-check";
/**
* Repro for Mintlify multi-entity discount bug.
*
* Production sequence (from Axiom logs):
* 1. Entity 1 attaches pro (upgrade from Hobby) — creates subscription
* 2. Discount applied to subscription
* 3. Entity 1 cancels pro (end_of_cycle) — creates subscription schedule
* 4. Entity 1 uncancels pro — schedule modified
* 5. Entity 2 attaches pro — subscription update succeeds but schedule
* creation/update fails: "Discount di_xxx has exceeded its maximum
* number of applications and cannot be reused"
*
* The error is in stripeDiscountsToPhaseDiscounts which passes
* { discount: "di_xxx" } to schedule phases — but the discount is bound
* to the subscription, not the schedule.
*/
test(`${chalk.yellowBright("bug repro: entity 2 attach after cancel+uncancel with discount on sub")}`, async () => {
const customerId = "multi-ent-cancel-uncancel";
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const pro = products.pro({
id: "pro-pm-check",
id: "pro",
items: [messagesItem],
});
const { ctx } = await initScenario({
// Step 1: Entity 1 attaches pro
const { autumnV2_2, autumnV1, entities } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
s.entities({ count: 2, featureId: TestFeature.Users }),
],
actions: [
s.billing.attach({ productId: pro.id, entityIndex: 0 }),
],
actions: [s.attach({ productId: pro.id })],
});
const fullCustomer = await CusService.getFull({
ctx,
idOrInternalId: customerId,
// Step 2: Apply a discount to entity 1's subscription
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
const stripeCustomerId = fullCustomer.processor?.id;
expect(stripeCustomerId).toBeDefined();
const subs = await ctx.stripeCli.subscriptions.list({
customer: stripeCustomerId!,
status: "all",
const coupon = await createPercentCoupon({
stripeCli,
percentOff: 10,
});
const subscription = subs.data.find(
(sub) => sub.status === "active" || sub.status === "trialing",
);
expect(subscription).toBeDefined();
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon.id],
});
const subDefaultPm = subscription!.default_payment_method;
console.log(`Subscription default_payment_method: ${JSON.stringify(subDefaultPm)}`);
// Step 3: Entity 1 cancels pro (end_of_cycle) — creates subscription schedule
console.log("Canceling entity 1 pro (end of cycle)...");
await autumnV2_2.subscriptions.update({
customer_id: customerId,
entity_id: entities[0].id,
plan_id: pro.id,
cancel_action: "cancel_end_of_cycle",
});
const stripeCustomer = await ctx.stripeCli.customers.retrieve(stripeCustomerId!);
const customerDefaultPm = "deleted" in stripeCustomer
? null
: stripeCustomer.invoice_settings?.default_payment_method;
console.log(`Customer invoice_settings.default_payment_method: ${JSON.stringify(customerDefaultPm)}`);
await new Promise((resolve) => setTimeout(resolve, 3000));
const customerDefaultSource = "deleted" in stripeCustomer
? null
: stripeCustomer.default_source;
console.log(`Customer default_source: ${JSON.stringify(customerDefaultSource)}`);
// Step 4: Entity 1 uncancels pro — schedule released/modified
console.log("Uncanceling entity 1 pro...");
await autumnV2_2.subscriptions.update({
customer_id: customerId,
entity_id: entities[0].id,
plan_id: pro.id,
cancel_action: "uncancel",
});
if (subDefaultPm) {
console.log(chalk.green("Payment method is set at the SUBSCRIPTION level"));
} else if (customerDefaultPm) {
console.log(chalk.green("Payment method is set at the CUSTOMER level (invoice_settings)"));
} else if (customerDefaultSource) {
console.log(chalk.green("Payment method is set at the CUSTOMER level (default_source)"));
} else {
console.log(chalk.red("No default payment method found on subscription or customer"));
await new Promise((resolve) => setTimeout(resolve, 3000));
// Step 5: Entity 2 attaches pro
console.log("Attaching pro to entity 2...");
try {
const result = await autumnV2_2.billing.attach<AttachParamsV1Input>({
customer_id: customerId,
entity_id: entities[1].id,
plan_id: pro.id,
redirect_mode: "if_required",
});
console.log("Entity 2 result:", JSON.stringify(result, null, 2));
console.log(chalk.red("BUG NOT REPRODUCED — entity 2 attach succeeded"));
} catch (error: any) {
console.log(chalk.green("BUG REPRODUCED — entity 2 attach failed:"));
console.log("Error:", JSON.stringify(error, null, 2));
}
});

View File

@@ -0,0 +1,113 @@
import { test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import {
applySubscriptionDiscount,
createPercentCoupon,
getStripeSubscription,
} from "@tests/integration/billing/utils/discounts/discountTestUtils";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import {
expectProductCanceling,
expectProductScheduled,
} from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { items } from "@tests/utils/fixtures/items.js";
import { products } from "@tests/utils/fixtures/products.js";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk";
/**
* Reproduces: attaching a one-off add-on to a subscription that already
* has a schedule (from a prior downgrade) fails with Stripe error
* "You cannot migrate a subscription that is already attached to a schedule".
*
* The root cause is that setupStripeBillingContext skips fetching the schedule
* when targetCustomerProduct is undefined (new attachment, no transition).
* buildStripeSubscriptionScheduleAction then sees hasSchedule=false and emits
* type:"create" instead of type:"update", which Stripe rejects.
*/
test.concurrent(`${chalk.yellowBright("bug-repro: attach one-off add-on after scheduled downgrade hits 'subscription already attached to schedule'")}`, async () => {
const customerId = "repro-schedule-addon-attach";
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const premium = products.premium({
id: "premium",
items: [messagesItem],
});
const pro = products.pro({
id: "pro",
items: [messagesItem],
});
const topupAddon = products.base({
id: "topup-addon",
items: [
items.oneOffWords({
includedUsage: 0,
billingUnits: 100,
price: 25,
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [premium, pro, topupAddon] }),
],
actions: [s.billing.attach({ productId: premium.id })],
});
// Apply a 20% discount to the subscription (matches production scenario)
const { stripeCli, subscription: subBefore } = await getStripeSubscription({
customerId,
});
const coupon = await createPercentCoupon({
stripeCli,
percentOff: 20,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subBefore.id,
couponIds: [coupon.id],
});
// Schedule downgrade: Premium -> Pro (creates a subscription schedule)
await autumnV1.billing.attach({
customer_id: customerId,
product_id: pro.id,
redirect_mode: "if_required",
});
// Verify the downgrade was scheduled correctly
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectProductCanceling({ customer, productId: premium.id });
await expectProductScheduled({ customer, productId: pro.id });
await autumnV1.billing.attach({
customer_id: customerId,
product_id: topupAddon.id,
redirect_mode: "if_required",
options: [{ feature_id: TestFeature.Words, quantity: 100 }],
});
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectCustomerFeatureCorrect({
customer: customerAfter,
featureId: TestFeature.Words,
balance: 100,
usage: 0,
});
await expectCustomerInvoiceCorrect({
customer: customerAfter,
count: 2,
latestTotal: 25,
});
});

View File

@@ -21,10 +21,15 @@ export const useEntity = () => {
const { customer } = useCusQuery();
const entities = (customer as FullCustomer)?.entities || [];
// #region agent log
fetch('http://127.0.0.1:7322/ingest/302190cd-01f7-494e-9c36-a1625f5cf969',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'0967f4'},body:JSON.stringify({sessionId:'0967f4',location:'useSubscriptionStore.ts:useEntity',message:'entities debug',data:{customerExists:!!customer,entitiesRaw:(customer as any)?.entities,entitiesIsArray:Array.isArray((customer as any)?.entities),entitiesLength:entities.length,entitiesNullCount:entities.filter((e:any) => e == null).length,firstFewEntities:entities.slice(0,3).map((e:any) => e == null ? 'NULL' : {id:e?.id,internal_id:e?.internal_id}),selectedEntityId},timestamp:Date.now()})}).catch(()=>{});
// #endregion
// Find the full entity object
const entity = entities.find(
(e: Entity) =>
e.id === selectedEntityId || e.internal_id === selectedEntityId,
e != null &&
(e.id === selectedEntityId || e.internal_id === selectedEntityId),
);
// Sync query param to store on mount/change

View File

@@ -3,13 +3,17 @@ import { Button } from "@/components/v2/buttons/Button";
import { CustomerBlockDialog } from "./CustomerBlockDialog";
import { EdgeConfigDialog } from "./EdgeConfigDialog";
import { FeatureFlagsDialog } from "./FeatureFlagsDialog";
import { OrgLimitsDialog } from "./OrgLimitsDialog";
import { RawEdgeConfigDialog } from "./RawEdgeConfigDialog";
import { StripeSyncDialog } from "./StripeSyncDialog";
export function EdgeConfigTab() {
const [requestBlockEditOpen, setRequestBlockEditOpen] = useState(false);
const [requestBlockRawOpen, setRequestBlockRawOpen] = useState(false);
const [featureFlagsOpen, setFeatureFlagsOpen] = useState(false);
const [customerBlockOpen, setCustomerBlockOpen] = useState(false);
const [orgLimitsOpen, setOrgLimitsOpen] = useState(false);
const [stripeSyncOpen, setStripeSyncOpen] = useState(false);
return (
<div className="flex flex-col gap-4">
@@ -71,6 +75,39 @@ export function EdgeConfigTab() {
Edit
</Button>
</div>
<div className="flex items-center justify-between border-t border-border p-4 last:border-b-0">
<div className="flex flex-col gap-0.5">
<div className="text-sm font-medium text-t1">Org Limits</div>
<div className="text-xs text-t3">
Per-org overrides for max customer products returned in queries
(default 15).
</div>
</div>
<Button
variant="primary"
size="sm"
onClick={() => setOrgLimitsOpen(true)}
>
Edit
</Button>
</div>
<div className="flex items-center justify-between border-t border-border p-4 last:border-b-0">
<div className="flex flex-col gap-0.5">
<div className="text-sm font-medium text-t1">Stripe Sync</div>
<div className="text-xs text-t3">
Enable Stripe webhook event syncing to the sync DB per org.
</div>
</div>
<Button
variant="primary"
size="sm"
onClick={() => setStripeSyncOpen(true)}
>
Edit
</Button>
</div>
</div>
<FeatureFlagsDialog
@@ -95,6 +132,13 @@ export function EdgeConfigTab() {
onOpenChange={setRequestBlockRawOpen}
configId="request-block"
/>
<OrgLimitsDialog open={orgLimitsOpen} onOpenChange={setOrgLimitsOpen} />
<StripeSyncDialog
open={stripeSyncOpen}
onOpenChange={setStripeSyncOpen}
/>
</div>
);
}

View File

@@ -0,0 +1,342 @@
import Editor from "@monaco-editor/react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/v2/badges/Badge";
import { Button } from "@/components/v2/buttons/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/v2/dialogs/Dialog";
import { Input } from "@/components/v2/inputs/Input";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { getBackendErr } from "@/utils/genUtils";
const DEFAULT_CUS_PRODUCT_LIMIT = 15;
type OrgLimitsEntry = {
maxCusProducts?: number;
};
type OrgLimitsConfig = {
orgs: Record<string, OrgLimitsEntry>;
configHealthy: boolean;
configConfigured: boolean;
lastSuccessAt: string | null;
error: string | null;
};
const DEFAULT_CONFIG: OrgLimitsConfig = {
orgs: {},
configHealthy: false,
configConfigured: false,
lastSuccessAt: null,
error: null,
};
const getEditableConfig = ({ config }: { config: OrgLimitsConfig }) => ({
orgs: config.orgs,
});
const getEntryRows = ({ config }: { config: OrgLimitsConfig }) => {
return Object.entries(config.orgs)
.map(([orgId, entry]) => ({
orgId,
maxCusProducts: entry.maxCusProducts ?? DEFAULT_CUS_PRODUCT_LIMIT,
}))
.sort((a, b) => a.orgId.localeCompare(b.orgId));
};
export function OrgLimitsDialog({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const axiosInstance = useAxiosInstance();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [config, setConfig] = useState<OrgLimitsConfig>(DEFAULT_CONFIG);
const [jsonText, setJsonText] = useState("");
const [jsonError, setJsonError] = useState<string | null>(null);
const [syncSource, setSyncSource] = useState<"form" | "json">("form");
const [newOrgId, setNewOrgId] = useState("");
const [newLimit, setNewLimit] = useState("");
useEffect(() => {
if (!open) return;
let cancelled = false;
setLoading(true);
void axiosInstance
.get<OrgLimitsConfig>("/admin/org-limits-config")
.then(({ data }) => {
if (cancelled) return;
const mergedConfig: OrgLimitsConfig = {
...DEFAULT_CONFIG,
...data,
};
setConfig(mergedConfig);
setJsonText(
JSON.stringify(getEditableConfig({ config: mergedConfig }), null, 2),
);
setJsonError(null);
setSyncSource("form");
})
.catch((error) => {
if (!cancelled) {
toast.error(getBackendErr(error, "Failed to load org limits config"));
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [axiosInstance, open]);
useEffect(() => {
if (syncSource !== "form") return;
setJsonText(JSON.stringify(getEditableConfig({ config }), null, 2));
setJsonError(null);
}, [config, syncSource]);
const entryRows = useMemo(() => getEntryRows({ config }), [config]);
const handleJsonChange = (value: string | undefined) => {
const text = value ?? "";
setJsonText(text);
setSyncSource("json");
try {
const parsed = JSON.parse(text) as {
orgs?: Record<string, OrgLimitsEntry>;
};
setConfig((current) => ({
...current,
orgs: parsed.orgs ?? {},
}));
setJsonError(null);
} catch {
setJsonError("Invalid JSON");
}
};
const addEntry = () => {
const orgId = newOrgId.trim();
const limit = parseInt(newLimit.trim(), 10);
if (!orgId || Number.isNaN(limit) || limit < 1) return;
setSyncSource("form");
setConfig((current) => ({
...current,
orgs: {
...current.orgs,
[orgId]: { maxCusProducts: limit },
},
}));
setNewOrgId("");
setNewLimit("");
};
const removeEntry = ({ orgId }: { orgId: string }) => {
setSyncSource("form");
setConfig((current) => {
const nextOrgs = { ...current.orgs };
delete nextOrgs[orgId];
return { ...current, orgs: nextOrgs };
});
};
const handleSave = async () => {
if (jsonError) {
toast.error("Fix JSON errors before saving");
return;
}
let payload: unknown;
try {
payload = JSON.parse(jsonText);
} catch {
toast.error("Invalid JSON");
return;
}
setSaving(true);
try {
await axiosInstance.put("/admin/org-limits-config", payload);
toast.success("Org limits config saved");
onOpenChange(false);
} catch (error) {
toast.error(getBackendErr(error, "Failed to save org limits config"));
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl bg-card">
<DialogHeader>
<DialogTitle>Org Limits</DialogTitle>
<DialogDescription>
Configure per-org limits such as max customer products returned in
queries. Default is {DEFAULT_CUS_PRODUCT_LIMIT}.
</DialogDescription>
</DialogHeader>
{loading ? (
<div className="py-8 text-center text-sm text-t3">Loading...</div>
) : (
<div className="grid grid-cols-[320px_1fr] gap-6">
<div className="flex flex-col gap-4">
<div className="text-xs font-medium uppercase tracking-wide text-t3">
Org Overrides
</div>
<div className="rounded-lg border border-border p-3">
<div className="mb-3 flex flex-col gap-2">
<Input
placeholder="Org ID or slug"
value={newOrgId}
onChange={(event) => setNewOrgId(event.target.value)}
/>
<Input
placeholder="Max cusProducts (e.g. 30)"
type="number"
min={1}
value={newLimit}
onChange={(event) => setNewLimit(event.target.value)}
/>
<Button
variant="secondary"
size="sm"
onClick={addEntry}
disabled={
!newOrgId.trim() ||
!newLimit.trim() ||
Number.isNaN(parseInt(newLimit, 10)) ||
parseInt(newLimit, 10) < 1
}
>
Add org limit
</Button>
</div>
<div className="flex flex-col gap-2 border-t border-border pt-3">
{entryRows.length === 0 ? (
<div className="text-xs italic text-t3">
No org overrides all orgs use default (
{DEFAULT_CUS_PRODUCT_LIMIT})
</div>
) : (
entryRows.map((entry) => (
<div
key={entry.orgId}
className="flex items-start justify-between gap-3 rounded-lg border border-border p-2"
>
<div className="min-w-0 flex-1">
<div className="truncate font-mono text-xs text-t1">
{entry.orgId}
</div>
<div className="text-xs text-t2">
maxCusProducts: {entry.maxCusProducts}
</div>
</div>
<Button
variant="secondary"
size="sm"
onClick={() => removeEntry({ orgId: entry.orgId })}
>
Remove
</Button>
</div>
))
)}
</div>
</div>
<div className="rounded-lg border border-border p-3 text-xs text-t3">
<div className="mb-2 flex items-center gap-2">
<Badge
variant="muted"
className={
config.configHealthy
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: "border-amber-200 bg-amber-50 text-amber-700"
}
>
{config.configHealthy
? "Config healthy"
: "Config unavailable"}
</Badge>
{config.lastSuccessAt && (
<span>
Last refresh:{" "}
{new Date(config.lastSuccessAt).toLocaleString()}
</span>
)}
</div>
<div>
{config.configConfigured === false
? "S3 org limits config is not configured."
: config.error ||
"Limits update within 5 minutes of saving."}
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-xs font-medium uppercase tracking-wide text-t3">
Raw JSON
</div>
<div className="overflow-hidden rounded-md border border-border">
<Editor
height="420px"
language="json"
value={jsonText}
onChange={handleJsonChange}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 12,
tabSize: 2,
wordWrap: "on",
formatOnPaste: true,
formatOnType: true,
}}
theme="vs-dark"
/>
</div>
{jsonError && (
<div className="text-xs text-red-500">{jsonError}</div>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
isLoading={saving}
disabled={loading || !!jsonError}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,306 @@
import Editor from "@monaco-editor/react";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { Badge } from "@/components/v2/badges/Badge";
import { Button } from "@/components/v2/buttons/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/v2/dialogs/Dialog";
import { Input } from "@/components/v2/inputs/Input";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { getBackendErr } from "@/utils/genUtils";
type StripeSyncConfig = {
enabledOrgIds: string[];
configHealthy: boolean;
configConfigured: boolean;
lastSuccessAt: string | null;
error: string | null;
};
const DEFAULT_CONFIG: StripeSyncConfig = {
enabledOrgIds: [],
configHealthy: false,
configConfigured: false,
lastSuccessAt: null,
error: null,
};
export function StripeSyncDialog({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const axiosInstance = useAxiosInstance();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [config, setConfig] = useState<StripeSyncConfig>(DEFAULT_CONFIG);
const [jsonText, setJsonText] = useState("");
const [jsonError, setJsonError] = useState<string | null>(null);
const [syncSource, setSyncSource] = useState<"form" | "json">("form");
const [newOrgId, setNewOrgId] = useState("");
useEffect(() => {
if (!open) return;
let cancelled = false;
setLoading(true);
void axiosInstance
.get<StripeSyncConfig>("/admin/stripe-sync-config")
.then(({ data }) => {
if (cancelled) return;
const merged = { ...DEFAULT_CONFIG, ...data };
setConfig(merged);
setJsonText(
JSON.stringify({ enabledOrgIds: merged.enabledOrgIds }, null, 2),
);
setJsonError(null);
setSyncSource("form");
})
.catch((error) => {
if (!cancelled) {
toast.error(
getBackendErr(error, "Failed to load stripe sync config"),
);
}
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [axiosInstance, open]);
useEffect(() => {
if (syncSource !== "form") return;
setJsonText(
JSON.stringify({ enabledOrgIds: config.enabledOrgIds }, null, 2),
);
setJsonError(null);
}, [config, syncSource]);
const sortedOrgIds = useMemo(
() => [...config.enabledOrgIds].sort(),
[config.enabledOrgIds],
);
const handleJsonChange = (value: string | undefined) => {
const text = value ?? "";
setJsonText(text);
setSyncSource("json");
try {
const parsed = JSON.parse(text) as { enabledOrgIds?: string[] };
setConfig((current) => ({
...current,
enabledOrgIds: parsed.enabledOrgIds ?? [],
}));
setJsonError(null);
} catch {
setJsonError("Invalid JSON");
}
};
const addOrg = () => {
const orgId = newOrgId.trim();
if (!orgId) return;
if (config.enabledOrgIds.includes(orgId)) {
toast.error("Org already in list");
return;
}
setSyncSource("form");
setConfig((current) => ({
...current,
enabledOrgIds: [...current.enabledOrgIds, orgId],
}));
setNewOrgId("");
};
const removeOrg = ({ orgId }: { orgId: string }) => {
setSyncSource("form");
setConfig((current) => ({
...current,
enabledOrgIds: current.enabledOrgIds.filter((id) => id !== orgId),
}));
};
const handleSave = async () => {
if (jsonError) {
toast.error("Fix JSON errors before saving");
return;
}
let payload: unknown;
try {
payload = JSON.parse(jsonText);
} catch {
toast.error("Invalid JSON");
return;
}
setSaving(true);
try {
await axiosInstance.put("/admin/stripe-sync-config", payload);
toast.success("Stripe sync config saved");
onOpenChange(false);
} catch (error) {
toast.error(getBackendErr(error, "Failed to save stripe sync config"));
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl bg-card">
<DialogHeader>
<DialogTitle>Stripe Sync</DialogTitle>
<DialogDescription>
Manage which orgs have Stripe webhook events synced to the sync DB.
Changes take effect within 60 seconds.
</DialogDescription>
</DialogHeader>
{loading ? (
<div className="py-8 text-center text-sm text-t3">Loading...</div>
) : (
<div className="grid grid-cols-[320px_1fr] gap-6">
<div className="flex flex-col gap-4">
<div className="text-xs font-medium uppercase tracking-wide text-t3">
Enabled Orgs
</div>
<div className="rounded-lg border border-border p-3">
<div className="mb-3 flex gap-2">
<Input
placeholder="Org ID or slug"
value={newOrgId}
onChange={(event) => setNewOrgId(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") addOrg();
}}
/>
<Button
variant="secondary"
size="sm"
onClick={addOrg}
disabled={!newOrgId.trim()}
>
Add
</Button>
</div>
<div className="flex flex-col gap-2 border-t border-border pt-3">
{sortedOrgIds.length === 0 ? (
<div className="text-xs italic text-t3">
No orgs enabled sync is disabled for all orgs.
</div>
) : (
sortedOrgIds.map((orgId) => (
<div
key={orgId}
className="flex items-center justify-between gap-3 rounded-lg border border-border p-2"
>
<div className="truncate font-mono text-xs text-t1">
{orgId}
</div>
<Button
variant="secondary"
size="sm"
onClick={() => removeOrg({ orgId })}
>
Remove
</Button>
</div>
))
)}
</div>
</div>
<div className="rounded-lg border border-border p-3 text-xs text-t3">
<div className="mb-2 flex items-center gap-2">
<Badge
variant="muted"
className={
config.configHealthy
? "border-emerald-200 bg-emerald-50 text-emerald-700"
: "border-amber-200 bg-amber-50 text-amber-700"
}
>
{config.configHealthy
? "Config healthy"
: "Config unavailable"}
</Badge>
{config.lastSuccessAt && (
<span>
Last refresh:{" "}
{new Date(config.lastSuccessAt).toLocaleString()}
</span>
)}
</div>
<div>
{config.configConfigured === false
? "S3 stripe sync config is not configured."
: config.error ||
"Config updates within 60 seconds of saving."}
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-xs font-medium uppercase tracking-wide text-t3">
Raw JSON
</div>
<div className="overflow-hidden rounded-md border border-border">
<Editor
height="420px"
language="json"
value={jsonText}
onChange={handleJsonChange}
options={{
minimap: { enabled: false },
scrollBeyondLastLine: false,
fontSize: 12,
tabSize: 2,
wordWrap: "on",
formatOnPaste: true,
formatOnType: true,
}}
theme="vs-dark"
/>
</div>
{jsonError && (
<div className="text-xs text-red-500">{jsonError}</div>
)}
</div>
</div>
)}
<DialogFooter>
<Button variant="secondary" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
isLoading={saving}
disabled={loading || !!jsonError}
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -9,13 +9,13 @@ export const OrgLogo = ({ org }: { org: FrontendOrg }) => {
"rounded-md overflow-hidden flex items-center justify-center scale-100 translate-x-px bg-zinc-200 w-5 h-5 min-w-5 min-h-5",
)}
>
{org.logo ? (
{/* {org.logo ? (
<img src={org.logo} alt={org.name} className="w-full h-full" />
) : (
<span className="w-5 h-5 flex items-center justify-center bg-linear-to-r from-purple-600 via-purple-500 to-purple-gradient text-white text-xs">
{firstLetter}
</span>
)}
) : ( */}
<span className="w-5 h-5 flex items-center justify-center bg-linear-to-r from-purple-600 via-purple-500 to-purple-gradient text-white text-xs">
{firstLetter}
</span>
{/* )} */}
</div>
);
};

View File

@@ -194,7 +194,10 @@ export const EditPlanHeader = () => {
{hasRCMapping && (
<Tooltip>
<TooltipTrigger>
<IconBadge variant="muted" icon={<RevenueCatIcon size={14} />}>
<IconBadge
variant="muted"
icon={<RevenueCatIcon size={14} />}
>
RC
</IconBadge>
</TooltipTrigger>
@@ -209,8 +212,7 @@ export const EditPlanHeader = () => {
<IconBadge
variant="muted"
icon={<TriangleIcon size={12} weight="fill" />}
>
</IconBadge>
></IconBadge>
</TooltipTrigger>
<TooltipContent>
This plan is linked to Vercel Marketplace