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(); + }); +});