From 9e3c472515848aa9e32963dffe04fe927d485219 Mon Sep 17 00:00:00 2001 From: sonpiaz Date: Mon, 12 Oct 2026 23:33:13 -0700 Subject: [PATCH] fix: setup stripe points to the dashboard instead of a dead OAuth flow The CLI flow could not complete: the token exchange needs Affitor's platform secret, POST /api/v1/cli/stripe-connect answers 410, and the webhook it created pointed at /webhooks/stripe/, which the CMS does not serve (and subscribed to invoice.paid, which the CMS does not handle). Now it prints where to connect Stripe and exits 1, with no network call. Bump to 0.4.3 (not published). Co-Authored-By: Claude Opus 5.5 (1M context) --- package-lock.json | 20 +- packages/cli/CHANGELOG.md | 3 + packages/cli/README.md | 20 +- packages/cli/package.json | 3 +- packages/cli/src/commands/setup-stripe.ts | 185 ++------------- packages/cli/src/commands/status.ts | 2 +- packages/cli/src/lib/api-client.ts | 13 -- packages/cli/src/lib/config.ts | 5 +- packages/cli/src/lib/stripe-oauth.ts | 262 ---------------------- packages/cli/src/types.ts | 20 -- packages/cli/tests/setup-stripe.test.ts | 45 ++++ 11 files changed, 79 insertions(+), 499 deletions(-) delete mode 100644 packages/cli/src/lib/stripe-oauth.ts create mode 100644 packages/cli/tests/setup-stripe.test.ts diff --git a/package-lock.json b/package-lock.json index 3c368a4..a4745a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1253,6 +1253,7 @@ "version": "22.19.21", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2985,19 +2986,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/stripe": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-17.7.0.tgz", - "integrity": "sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==", - "license": "MIT", - "dependencies": { - "@types/node": ">=8.1.0", - "qs": "^6.11.0" - }, - "engines": { - "node": ">=12.*" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3117,6 +3105,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3407,15 +3396,14 @@ }, "packages/cli": { "name": "affitor", - "version": "0.4.2", + "version": "0.4.3", "license": "MIT", "dependencies": { "@affitor/recipes": "^0.1.0", "@inquirer/prompts": "^7.5.0", "commander": "^13.1.0", "open": "^10.1.0", - "picocolors": "^1.1.1", - "stripe": "^17.7.0" + "picocolors": "^1.1.1" }, "bin": { "affitor": "dist/cli.js" diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 0ae87e1..85258ff 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog — affitor (CLI) +## 0.4.3 +- `affitor setup stripe` now points to the dashboard (program Settings, Connect Stripe) and exits 1 (`{"error": "stripe_setup_moved"}` with `--json`). The old flow could not complete: its token exchange needed Affitor's platform secret, the API that saved the connection is retired (410), and the webhook it created pointed at a URL the API does not serve. It no longer creates anything on your Stripe account. The unused `stripe` dependency is removed. + ## 0.4.2 - Security: the secrets file is written with owner-only permissions, and login URLs are opened safely (#26). - API key rotation: warnings for a key that must be replaced, a clear error for a retired key, and the `Deprecation`/`Sunset`/`Link` headers are read so a retired key is never silent (#27, #30). A refused key points to Regenerate, not to `affitor init` (#28). diff --git a/packages/cli/README.md b/packages/cli/README.md index df89898..0d9f79b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -8,8 +8,7 @@ CLI-native affiliate tracking. Connect Stripe, track commissions, manage partner # Create your affiliate program npx affitor init -# Connect Stripe for automatic payment tracking -npx affitor setup stripe +# Connect Stripe in the Affitor dashboard: program Settings, Connect Stripe # Check program health npx affitor status @@ -26,7 +25,7 @@ npx affitor test sale |---|---| | `affitor init` | Create a new affiliate program. Interactive prompts or `--no-interactive` with flags. | | `affitor onboard` | Wire Affitor into an existing app end-to-end: detect → install browser tracking → inject the Stripe sale call → verify. | -| `affitor setup stripe` | Connect Stripe via OAuth. Auto-configures webhooks for payment tracking. | +| `affitor setup stripe` | Points you to the dashboard, where Stripe is connected (program Settings, Connect Stripe). Exits 1. | | `affitor setup dns` | Set up DNS CNAME tracking (coming soon). | | `affitor status` | Show program health: DNS, Stripe connection, recent events. | | `affitor test [type]` | Send a test event (`click`, `lead`, or `sale`). | @@ -92,24 +91,13 @@ Running `npx affitor init` creates: skills.md -- AI agent instructions ``` -## Stripe Auto-Connect +## Stripe -`npx affitor setup stripe` automates the entire Stripe integration: - -1. Opens Stripe Connect OAuth in your browser -2. Creates webhook endpoint on your Stripe account -3. Configures event listeners for: - - `customer.created` -- lead tracking - - `checkout.session.completed` -- sale tracking - - `invoice.paid` -- recurring commissions - - `invoice.payment_failed` -- failed payment alerts - - `charge.refunded` -- automatic commission clawback - - `customer.subscription.deleted` -- churn tracking +Stripe is connected from the Affitor dashboard (program Settings, Connect Stripe), not from the CLI. Affitor's own webhook then records sales and renewals; add Affitor metadata to your checkout so sales are attributed reliably. ## Requirements - Node.js >= 18 -- Stripe account (for `setup stripe`) ## License diff --git a/packages/cli/package.json b/packages/cli/package.json index ed27916..e5b2770 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "affitor", - "version": "0.4.2", + "version": "0.4.3", "description": "CLI-native affiliate tracking. Connect Stripe, track commissions, manage partners — all from your terminal.", "type": "module", "bin": { @@ -44,7 +44,6 @@ "@inquirer/prompts": "^7.5.0", "picocolors": "^1.1.1", "open": "^10.1.0", - "stripe": "^17.7.0", "@affitor/recipes": "^0.1.0" }, "devDependencies": { diff --git a/packages/cli/src/commands/setup-stripe.ts b/packages/cli/src/commands/setup-stripe.ts index 24b8878..eaeb5e9 100644 --- a/packages/cli/src/commands/setup-stripe.ts +++ b/packages/cli/src/commands/setup-stripe.ts @@ -1,12 +1,18 @@ import type { Command } from "commander"; import * as logger from "../lib/logger.js"; import { format } from "../lib/logger.js"; -import { readConfig, updateConfig, writeSecrets, readSecrets, ConfigNotFoundError } from "../lib/config.js"; -import { runStripeOAuth, StripeOAuthError } from "../lib/stripe-oauth.js"; -import { AffitorAPI, APIError, NetworkError } from "../lib/api-client.js"; +import { readConfig, ConfigNotFoundError } from "../lib/config.js"; import { getFlags } from "../lib/flags.js"; import type { CLIFlags } from "../types.js"; +/** + * Stripe is connected from the dashboard. The CLI flow this replaced could not work: its token + * exchange needs Affitor's platform secret, the CMS endpoint that saved the connection answers + * 410 Gone (W37-1690), and the webhook it created pointed at a URL the CMS does not serve. + */ +export const STRIPE_SETUP_MOVED = + "Stripe is connected from the Affitor dashboard, not the CLI: open the program, go to Settings, and use Connect Stripe."; + export function registerSetupCommand(program: Command) { const setup = program .command("setup") @@ -14,11 +20,11 @@ export function registerSetupCommand(program: Command) { setup .command("stripe") - .description("Connect Stripe for automatic payment tracking") - .option("--stripe-client-id ", "Stripe Connect client ID override") - .option("--stripe-secret-key ", "Stripe secret key (for webhook creation)") - .action(async (opts, cmd) => { - await runSetupStripe(opts, getFlags(cmd)); + .description("Connect Stripe (points you to the dashboard)") + .allowUnknownOption() + .allowExcessArguments() + .action((_opts, cmd) => { + runSetupStripe(getFlags(cmd)); }); setup @@ -29,163 +35,10 @@ export function registerSetupCommand(program: Command) { }); } -async function runSetupStripe( - opts: { - stripeClientId?: string; - stripeSecretKey?: string; - }, - flags: CLIFlags, -) { - let config; - try { - config = readConfig(); - } catch (err) { - if (err instanceof ConfigNotFoundError) { - logger.error(err.message); - if (flags.json) logger.json({ error: "no_config" }); - process.exit(1); - } - throw err; - } - - // Check if already connected via secrets or legacy config - const secrets = readSecrets(); - if (secrets?.stripe_account_id || config.stripe_connected) { - const accountId = secrets?.stripe_account_id ?? config.stripe_account_id; - logger.warn("Stripe is already connected."); - logger.info(` Account: ${accountId}`); - logger.info(" To reconnect, disconnect first via the dashboard."); - if (flags.json) { - logger.json({ - status: "already_connected", - stripe_account_id: accountId, - }); - } - process.exit(0); - } - - const clientId = - opts.stripeClientId ?? - process.env.STRIPE_CONNECT_CLIENT_ID ?? - process.env.AFFITOR_STRIPE_CLIENT_ID; - - if (!clientId) { - logger.error( - "Stripe Connect client ID not configured.\n" + - " Set STRIPE_CONNECT_CLIENT_ID or AFFITOR_STRIPE_CLIENT_ID environment variable,\n" + - " or pass --stripe-client-id .", - ); - if (flags.json) logger.json({ error: "missing_stripe_client_id" }); - process.exit(1); - } - - const stripeSecretKey = - opts.stripeSecretKey ?? - process.env.STRIPE_SECRET_KEY ?? - process.env.AFFITOR_STRIPE_SECRET_KEY; - - if (!stripeSecretKey) { - logger.error( - "Stripe secret key not configured.\n" + - " Set STRIPE_SECRET_KEY or AFFITOR_STRIPE_SECRET_KEY environment variable,\n" + - " or pass --stripe-secret-key .", - ); - if (flags.json) logger.json({ error: "missing_stripe_secret_key" }); - process.exit(1); - } - - const apiUrl = flags.apiUrl ?? config.api_url; - const webhookUrl = `${apiUrl}/webhooks/stripe/${config.program_id}`; - - try { - const totalSteps = 3; - logger.newline(); - - const result = await runStripeOAuth({ - clientId, - programId: config.program_id, - webhookUrl, - stripeSecretKey, - }); - - logger.progressStep(1, totalSteps, "Stripe authorized", true); - - const api = AffitorAPI.fromFlags( - { apiKey: flags.apiKey, apiUrl }, - ); - - const saveResult = await api.saveStripeConnection({ - program_id: config.program_id, - stripe_user_id: result.stripe_user_id, - webhook_endpoint_id: result.webhook_endpoint_id, - webhook_secret: result.webhook_secret, - }); - - logger.progressStep(2, totalSteps, "Connection saved", true); - - // Update secrets with Stripe account ID - const currentSecrets = readSecrets(); - if (currentSecrets) { - writeSecrets({ - ...currentSecrets, - stripe_account_id: result.stripe_user_id, - }); - } - - // Update config (for backward compat) - updateConfig({ - stripe_connected: true, - stripe_account_id: result.stripe_user_id, - }); - - logger.progressStep(3, totalSteps, "Config updated", true); - - if (flags.json) { - logger.json({ - status: "connected", - stripe_account_id: result.stripe_user_id, - webhook_endpoint_id: result.webhook_endpoint_id, - charges_enabled: saveResult.charges_enabled, - payouts_enabled: saveResult.payouts_enabled, - }); - return; - } - - const chargesIcon = saveResult.charges_enabled ? format.green("✓") : format.yellow("⚠"); - const payoutsIcon = saveResult.payouts_enabled ? format.green("✓") : format.yellow("⚠"); - - logger.titledBox("Stripe Connected", [ - "", - ` Account: ${format.bold(result.stripe_user_id)}`, - ` Charges: ${chargesIcon} ${saveResult.charges_enabled ? "Enabled" : "Pending verification"}`, - ` Payouts: ${payoutsIcon} ${saveResult.payouts_enabled ? "Enabled" : "Pending verification"}`, - "", - ` ${format.bold("Webhooks auto-configured:")}`, - ` ${format.green("✓")} customer.created ${format.dim("→ lead tracking")}`, - ` ${format.green("✓")} checkout.session.completed ${format.dim("→ sale tracking")}`, - ` ${format.green("✓")} invoice.paid ${format.dim("→ recurring commission")}`, - ` ${format.green("✓")} charge.refunded ${format.dim("→ auto clawback")}`, - "", - ]); - - logger.info(` Test it: ${format.dim("$")} npx affitor test sale`); - logger.newline(); - } catch (err) { - if (err instanceof StripeOAuthError) { - logger.error(err.message); - if (flags.json) logger.json({ error: "stripe_oauth_error", message: err.message }); - } else if (err instanceof APIError) { - logger.error(`API error: ${err.message}`); - if (flags.json) logger.json({ error: "api_error", message: err.message }); - } else if (err instanceof NetworkError) { - logger.error(err.message); - if (flags.json) logger.json({ error: "network_error" }); - } else { - logger.error(`Unexpected error: ${(err as Error).message}`); - if (flags.json) logger.json({ error: "unexpected_error", message: (err as Error).message }); - } - process.exit(1); - } +function runSetupStripe(flags: CLIFlags) { + logger.error(STRIPE_SETUP_MOVED); + if (flags.json) logger.json({ error: "stripe_setup_moved", message: STRIPE_SETUP_MOVED }); + process.exitCode = 1; } function runSetupDns(flags: CLIFlags) { @@ -227,6 +80,6 @@ function runSetupDns(flags: CLIFlags) { "", ]); - logger.info(` Get started: ${format.dim("$")} npx affitor setup stripe`); + logger.info(` Get started: connect Stripe from the Affitor dashboard (program Settings, Connect Stripe).`); logger.newline(); } diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index d06a728..0c84173 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -83,7 +83,7 @@ export async function runStatus(flags: CLIFlags) { logger.step(`DNS: Add CNAME t.${config.domain} → track.affitor.com`); } if (!status.stripe_connected) { - logger.step("Stripe: Run npx affitor setup stripe"); + logger.step("Stripe: Connect it in the Affitor dashboard (program Settings, Connect Stripe)"); } } catch (err) { if (err instanceof APIError) { diff --git a/packages/cli/src/lib/api-client.ts b/packages/cli/src/lib/api-client.ts index 1136b88..c79b150 100644 --- a/packages/cli/src/lib/api-client.ts +++ b/packages/cli/src/lib/api-client.ts @@ -1,7 +1,6 @@ import { DEFAULT_API_URL } from "../types.js"; import type { InitResponse, - StripeConnectResponse, ProgramStatus, ProgramSummary, TestEventResult, @@ -115,18 +114,6 @@ export class AffitorAPI { }); } - async saveStripeConnection(data: { - program_id: string; - stripe_user_id: string; - webhook_endpoint_id: string; - webhook_secret: string; - }): Promise { - return this.request("/api/v1/cli/stripe-connect", { - method: "POST", - body: data, - }); - } - async getStatus(programId: string): Promise { return this.request( `/api/v1/cli/status?program_id=${programId}`, diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index e8de2af..f58ba21 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -277,7 +277,7 @@ export function writeEnvExample( AFFITOR_API_KEY=${apiKey} AFFITOR_PROGRAM_ID=${config.program_id} -# STRIPE_CONNECTED_ACCOUNT_ID= (run \`npx affitor setup stripe\` to connect) +# STRIPE_CONNECTED_ACCOUNT_ID= (connect Stripe in the Affitor dashboard: program Settings, Connect Stripe) `; writeFileSync(join(dir, ".env.example"), content); } @@ -353,7 +353,7 @@ curl -X POST ${config.api_url}/api/v1/track/lead \\ ### 3. Sale/Revenue Tracking -**Option A: Stripe (recommended)** — run \`npx affitor setup stripe\` to auto-connect. Webhooks handle attribution automatically. +**Option A: Stripe (recommended)**: connect Stripe in the Affitor dashboard (program Settings, Connect Stripe). Affitor's webhook records sales; add Affitor metadata to your checkout so sales are attributed reliably. **Option B: Sale API (any payment provider)** @@ -399,7 +399,6 @@ Use the same internal user ID everywhere: \`\`\`bash npx affitor onboard # Wire Affitor in end-to-end (detect→inject→verify) npx affitor status # Check program health -npx affitor setup stripe # Auto-connect Stripe via OAuth npx affitor test click # Send test click event npx affitor test lead # Send test lead event npx affitor test sale # Send test sale event diff --git a/packages/cli/src/lib/stripe-oauth.ts b/packages/cli/src/lib/stripe-oauth.ts deleted file mode 100644 index 17b513b..0000000 --- a/packages/cli/src/lib/stripe-oauth.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; -import { randomBytes } from "node:crypto"; -import { URL } from "node:url"; -import Stripe from "stripe"; -import open from "open"; -import * as logger from "./logger.js"; -import { - OAUTH_CALLBACK_PORT_START, - OAUTH_CALLBACK_PORT_END, - OAUTH_TIMEOUT_MS, - WEBHOOK_EVENTS, -} from "../types.js"; - -interface OAuthResult { - stripe_user_id: string; - stripe_publishable_key?: string; - access_token: string; -} - -interface WebhookResult { - webhook_endpoint_id: string; - webhook_secret: string; -} - -export interface StripeSetupResult { - stripe_user_id: string; - webhook_endpoint_id: string; - webhook_secret: string; -} - -export async function runStripeOAuth(opts: { - clientId: string; - programId: string; - webhookUrl: string; - stripeSecretKey: string; -}): Promise { - const nonce = randomBytes(16).toString("hex"); - const state = Buffer.from( - JSON.stringify({ program_id: opts.programId, nonce }), - ).toString("base64url"); - - const port = await findAvailablePort(); - const redirectUri = `http://localhost:${port}/callback`; - - const authCode = await startOAuthFlow({ - clientId: opts.clientId, - redirectUri, - state, - nonce, - port, - }); - - // Exchange authorization code for connected account ID - logger.step("Exchanging authorization code..."); - const stripe = new Stripe(opts.stripeSecretKey); - const tokenResponse = await stripe.oauth.token({ - grant_type: "authorization_code", - code: authCode, - }); - - if (!tokenResponse.stripe_user_id) { - throw new StripeOAuthError( - "Stripe did not return an account ID. Authorization may have failed.", - ); - } - - const oauthResult: OAuthResult = { - stripe_user_id: tokenResponse.stripe_user_id, - access_token: tokenResponse.access_token ?? "", - stripe_publishable_key: tokenResponse.stripe_publishable_key, - }; - - logger.success("Stripe authorized"); - logger.step(`Account: ${oauthResult.stripe_user_id}`); - - const webhook = await createWebhookEndpoint({ - stripeSecretKey: opts.stripeSecretKey, - stripeAccountId: oauthResult.stripe_user_id, - webhookUrl: opts.webhookUrl, - }); - - logger.success("Webhook endpoint created"); - for (const event of WEBHOOK_EVENTS) { - logger.step(`✓ ${event}`); - } - - return { - stripe_user_id: oauthResult.stripe_user_id, - webhook_endpoint_id: webhook.webhook_endpoint_id, - webhook_secret: webhook.webhook_secret, - }; -} - -async function startOAuthFlow(opts: { - clientId: string; - redirectUri: string; - state: string; - nonce: string; - port: number; -}): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - server.close(); - reject( - new StripeOAuthError( - `Timed out after ${OAUTH_TIMEOUT_MS / 1000}s waiting for Stripe authorization.\n` + - "Run `npx affitor setup stripe` to try again.", - ), - ); - }, OAUTH_TIMEOUT_MS); - - const server = createServer( - (req: IncomingMessage, res: ServerResponse) => { - const url = new URL(req.url ?? "/", `http://localhost:${opts.port}`); - - if (url.pathname !== "/callback") { - res.writeHead(404); - res.end("Not found"); - return; - } - - const error = url.searchParams.get("error"); - if (error) { - clearTimeout(timeout); - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(errorPage("Stripe authorization was cancelled.")); - server.close(); - reject( - new StripeOAuthError( - `Stripe authorization cancelled: ${error}.\n` + - "Run `npx affitor setup stripe` to try again.", - ), - ); - return; - } - - const code = url.searchParams.get("code"); - const returnedState = url.searchParams.get("state"); - - if (!code || returnedState !== opts.state) { - clearTimeout(timeout); - res.writeHead(200, { "Content-Type": "text/html" }); - res.end(errorPage("Invalid OAuth callback. Please try again.")); - server.close(); - reject(new StripeOAuthError("Invalid OAuth state parameter.")); - return; - } - - clearTimeout(timeout); - res.writeHead(200, { "Content-Type": "text/html" }); - res.end( - successPage( - "Stripe connected! You can close this tab and return to the terminal.", - ), - ); - server.close(); - - resolve(code); - }, - ); - - server.listen(opts.port, () => { - const authorizeUrl = - `https://connect.stripe.com/oauth/authorize` + - `?response_type=code` + - `&client_id=${opts.clientId}` + - `&scope=read_write` + - `&redirect_uri=${encodeURIComponent(opts.redirectUri)}` + - `&state=${opts.state}`; - - logger.info("Opening Stripe Connect in your browser..."); - logger.debug(`URL: ${authorizeUrl}`); - - open(authorizeUrl).catch(() => { - logger.warn("Could not open browser automatically."); - logger.info(`Open this URL manually:\n ${authorizeUrl}`); - }); - }); - - server.on("error", (err) => { - clearTimeout(timeout); - reject( - new StripeOAuthError(`Failed to start OAuth callback server: ${err.message}`), - ); - }); - }); -} - -async function createWebhookEndpoint(opts: { - stripeSecretKey: string; - stripeAccountId: string; - webhookUrl: string; -}): Promise { - const stripe = new Stripe(opts.stripeSecretKey); - - const endpoint = await stripe.webhookEndpoints.create( - { - url: opts.webhookUrl, - enabled_events: [...WEBHOOK_EVENTS], - description: "Affitor affiliate tracking — auto-configured by CLI", - }, - { stripeAccount: opts.stripeAccountId }, - ); - - if (!endpoint.secret) { - throw new StripeOAuthError( - "Webhook endpoint created but no signing secret returned. Contact support.", - ); - } - - return { - webhook_endpoint_id: endpoint.id, - webhook_secret: endpoint.secret, - }; -} - -async function findAvailablePort(): Promise { - for (let port = OAUTH_CALLBACK_PORT_START; port <= OAUTH_CALLBACK_PORT_END; port++) { - const available = await checkPort(port); - if (available) return port; - } - throw new StripeOAuthError( - `Ports ${OAUTH_CALLBACK_PORT_START}-${OAUTH_CALLBACK_PORT_END} are all in use.\n` + - "Close other applications and try again.", - ); -} - -function checkPort(port: number): Promise { - return new Promise((resolve) => { - const server = createServer(); - server.once("error", () => resolve(false)); - server.once("listening", () => { - server.close(() => resolve(true)); - }); - server.listen(port); - }); -} - -function successPage(message: string): string { - return ` -Affitor - -

✓ ${message}

Return to your terminal.

`; -} - -function errorPage(message: string): string { - return ` -Affitor - -

✗ ${message}

Return to your terminal.

`; -} - -export class StripeOAuthError extends Error { - constructor(message: string) { - super(message); - this.name = "StripeOAuthError"; - } -} diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts index 555e2c0..7487999 100644 --- a/packages/cli/src/types.ts +++ b/packages/cli/src/types.ts @@ -58,14 +58,6 @@ export interface InitResponse { domain: string; } -export interface StripeConnectResponse { - connected: boolean; - stripe_account_id: string; - webhook_endpoint_id: string; - charges_enabled: boolean; - payouts_enabled: boolean; -} - export interface ProgramStatus { program_id: string; name: string; @@ -192,15 +184,3 @@ export const CONFIG_FILE = "config.json"; export const SECRETS_FILE = ".env"; export const GLOBAL_CONFIG_DIR = ".affitor"; export const CREDENTIALS_FILE = "credentials.json"; -export const OAUTH_CALLBACK_PORT_START = 3456; -export const OAUTH_CALLBACK_PORT_END = 3465; -export const OAUTH_TIMEOUT_MS = 120_000; - -export const WEBHOOK_EVENTS = [ - "customer.created", - "checkout.session.completed", - "invoice.paid", - "invoice.payment_failed", - "charge.refunded", - "customer.subscription.deleted", -] as const; diff --git a/packages/cli/tests/setup-stripe.test.ts b/packages/cli/tests/setup-stripe.test.ts new file mode 100644 index 0000000..39d34c7 --- /dev/null +++ b/packages/cli/tests/setup-stripe.test.ts @@ -0,0 +1,45 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { Command } from "commander"; +import { registerSetupCommand, STRIPE_SETUP_MOVED } from "../src/commands/setup-stripe"; +import { setLoggerOptions } from "../src/lib/logger"; + +/** + * Stripe is connected from the dashboard. `setup stripe` must point there, exit 1, and never + * touch the network: the old flow created a webhook on the brand's Stripe account that pointed + * at a URL the API does not serve. + */ +describe("affitor setup stripe", () => { + beforeEach(() => { + process.exitCode = undefined; + vi.stubGlobal("fetch", vi.fn()); + }); + + afterEach(() => { + process.exitCode = undefined; + setLoggerOptions({ json: false }); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + async function run(args: string[]): Promise { + const stdout = vi.spyOn(console, "log").mockImplementation(() => {}); + vi.spyOn(console, "error").mockImplementation(() => {}); + const program = new Command().option("--json", "", false); + registerSetupCommand(program); + await program.parseAsync(["node", "affitor", ...args]); + return stdout.mock.calls.map((call) => call.join(" ")); + } + + it("--json: prints stripe_setup_moved and exits 1 without a network call", async () => { + const out = await run(["--json", "setup", "stripe"]); + expect(out).toEqual([JSON.stringify({ error: "stripe_setup_moved", message: STRIPE_SETUP_MOVED }, null, 2)]); + expect(process.exitCode).toBe(1); + expect(fetch).not.toHaveBeenCalled(); + }); + + it("old flags are accepted and still get the directions", async () => { + await run(["setup", "stripe", "--stripe-secret-key", "sk_test_x", "--stripe-client-id", "ca_x"]); + expect(process.exitCode).toBe(1); + expect(fetch).not.toHaveBeenCalled(); + }); +});