From c37bf2505d1096b5dc4ec1fa243aa88e052e7b41 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Wed, 6 May 2026 16:49:19 -0400 Subject: [PATCH 1/3] feat: allow specifying a credential file --- CLAUDE.md | 7 ++++ README.md | 7 ++++ packages/cli/src/cli.tsx | 41 +++++++++++++++---- packages/cli/src/commands/auth/index.tsx | 13 ++++-- packages/cli/src/commands/auth/login.tsx | 7 +++- packages/cli/src/commands/auth/logout.tsx | 12 ++++-- packages/cli/src/commands/auth/status.tsx | 11 +++-- .../cli/src/commands/demo/demo-runner.tsx | 7 +++- packages/cli/src/commands/demo/index.tsx | 3 ++ packages/cli/src/commands/mpp/index.tsx | 10 +++-- packages/cli/src/commands/onboard/index.tsx | 3 ++ .../src/commands/onboard/onboard-runner.tsx | 8 +++- .../src/commands/payment-methods/index.tsx | 6 ++- .../cli/src/commands/spend-request/index.tsx | 9 +++- packages/cli/src/utils/resource-factory.ts | 9 ++++ packages/sdk/src/index.ts | 8 +++- .../sdk/src/utils/__tests__/storage.test.ts | 35 ++++++++++++++++ packages/sdk/src/utils/storage.ts | 15 ++++++- skills/create-payment-credential/SKILL.md | 1 + 19 files changed, 180 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 46faa33..b731c00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,10 +97,17 @@ Key input field notes: - **React 18 + Ink 5** for interactive rendering - **`conf`** for local auth token storage +## Global Flags + +| Flag | Effect | +|------|--------| +| `--auth ` | Read/write auth credentials to a specific file instead of the default platform config location. Parsed from `process.argv` and stripped before incur processes flags. | + ## Environment Variables | Variable | Effect | |----------|--------| +| `LINK_AUTH_FILE` | Same as `--auth` — override the auth credential file path (flag takes precedence) | | `LINK_API_BASE_URL` | Override API base URL | | `LINK_AUTH_BASE_URL` | Override auth base URL | | `LINK_HTTP_PROXY` | Route all SDK requests through an HTTP proxy (requires `undici` installed) | diff --git a/README.md b/README.md index aa9e053..705f899 100644 --- a/README.md +++ b/README.md @@ -226,10 +226,17 @@ link-cli mpp decode \ --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' ``` +### Global flags + +| Flag | Effect | +|------|--------| +| `--auth ` | Read/write auth credentials to a specific file instead of the default location. Useful for running multiple sessions with separate identities. | + ### Environment variables | Variable | Effect | |----------|--------| +| `LINK_AUTH_FILE` | Same as `--auth` — override the auth credential file path (flag takes precedence) | | `LINK_API_BASE_URL` | Override the API base URL | | `LINK_AUTH_BASE_URL` | Override the auth base URL | | `LINK_HTTP_PROXY` | Route all requests through an HTTP proxy (requires `undici`) | diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index de9ff9b..1e0c40b 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,3 +1,4 @@ +import { type AuthStorage, Storage, storage } from '@stripe/link-sdk'; import { Cli } from 'incur'; import { createAuthCli } from './commands/auth'; import { createDemoCli } from './commands/demo'; @@ -25,7 +26,20 @@ const defaultHeaders = { }; const verbose = process.argv.includes('--verbose'); -const factory = new ResourceFactory({ verbose, defaultHeaders }); + +const authFileIndex = process.argv.indexOf('--auth'); +const credentialFilePath = + authFileIndex !== -1 + ? process.argv[authFileIndex + 1] + : process.env.LINK_AUTH_FILE; +if (authFileIndex !== -1) { + process.argv.splice(authFileIndex, 2); +} +const authStorage: AuthStorage = credentialFilePath + ? new Storage({ configPath: credentialFilePath }) + : storage; + +const factory = new ResourceFactory({ verbose, defaultHeaders, authStorage }); const authRepo = factory.createAuthResource(); const spendRequestRepo = factory.createSpendRequestResource(); @@ -51,20 +65,29 @@ if (!isAgent && process.stdout.isTTY) { } } -cli.command(createAuthCli(authRepo, getUpdateInfo)); -cli.command(createSpendRequestCli(spendRequestRepo)); +cli.command(createAuthCli(authRepo, getUpdateInfo, authStorage)); +cli.command(createSpendRequestCli(spendRequestRepo, authStorage)); cli.command( - createPaymentMethodsCli(() => factory.createPaymentMethodsResource()), + createPaymentMethodsCli( + () => factory.createPaymentMethodsResource(), + authStorage, + ), ); -cli.command(createMppCli(spendRequestRepo)); +cli.command(createMppCli(spendRequestRepo, authStorage)); cli.command( - createDemoCli(authRepo, spendRequestRepo, () => - factory.createPaymentMethodsResource(), + createDemoCli( + authRepo, + spendRequestRepo, + () => factory.createPaymentMethodsResource(), + authStorage, ), ); cli.command( - createOnboardCli(authRepo, spendRequestRepo, () => - factory.createPaymentMethodsResource(), + createOnboardCli( + authRepo, + spendRequestRepo, + () => factory.createPaymentMethodsResource(), + authStorage, ), ); diff --git a/packages/cli/src/commands/auth/index.tsx b/packages/cli/src/commands/auth/index.tsx index 1e694ee..865990c 100644 --- a/packages/cli/src/commands/auth/index.tsx +++ b/packages/cli/src/commands/auth/index.tsx @@ -1,4 +1,4 @@ -import { storage } from '@stripe/link-sdk'; +import { type AuthStorage, storage as defaultStorage } from '@stripe/link-sdk'; import { Cli } from 'incur'; import { render } from 'ink'; import React from 'react'; @@ -12,7 +12,9 @@ import { AuthStatus } from './status'; export function createAuthCli( authResource: IAuthResource, getUpdateInfo?: UpdateInfoProvider, + authStorage?: AuthStorage, ) { + const storage = authStorage ?? defaultStorage; const cli = Cli.create('auth', { description: 'Authentication commands', }); @@ -36,6 +38,7 @@ export function createAuthCli( {}} />, ); @@ -89,7 +92,11 @@ export function createAuthCli( if (!c.agent && !c.formatExplicit) { return new Promise((resolve) => { const { waitUntilExit } = render( - {}} />, + {}} + />, ); waitUntilExit().then(() => resolve(result)); }); @@ -122,7 +129,7 @@ export function createAuthCli( if (!c.agent && !c.formatExplicit) { return new Promise((resolve) => { const { waitUntilExit } = render( - {}} />, + {}} />, ); waitUntilExit().then(() => { const auth = storage.getAuth(); diff --git a/packages/cli/src/commands/auth/login.tsx b/packages/cli/src/commands/auth/login.tsx index ffd7b4e..efa17c2 100644 --- a/packages/cli/src/commands/auth/login.tsx +++ b/packages/cli/src/commands/auth/login.tsx @@ -1,4 +1,4 @@ -import { storage } from '@stripe/link-sdk'; +import { type AuthStorage, storage as defaultStorage } from '@stripe/link-sdk'; import { Box, Text, useInput } from 'ink'; import Spinner from 'ink-spinner'; import type React from 'react'; @@ -9,14 +9,17 @@ import { openUrl } from '../../utils/open-url'; interface LoginProps { authResource: IAuthResource; clientName?: string; + authStorage?: AuthStorage; onComplete: () => void; } export const Login: React.FC = ({ authResource, clientName, + authStorage = defaultStorage, onComplete, }) => { + const storage = authStorage; const [status, setStatus] = useState< 'initiating' | 'waiting' | 'polling' | 'success' | 'error' >('initiating'); @@ -81,7 +84,7 @@ export const Login: React.FC = ({ // Wait 1 second before starting to poll const timeout = setTimeout(startPolling, 1000); return () => clearTimeout(timeout); - }, [status, deviceCode, authResource, onComplete]); + }, [status, deviceCode, authResource, onComplete, storage]); if (status === 'initiating') { return ( diff --git a/packages/cli/src/commands/auth/logout.tsx b/packages/cli/src/commands/auth/logout.tsx index f43caeb..7703b34 100644 --- a/packages/cli/src/commands/auth/logout.tsx +++ b/packages/cli/src/commands/auth/logout.tsx @@ -1,4 +1,4 @@ -import { storage } from '@stripe/link-sdk'; +import { type AuthStorage, storage as defaultStorage } from '@stripe/link-sdk'; import { Box, Text } from 'ink'; import type React from 'react'; import { useEffect, useState } from 'react'; @@ -6,10 +6,16 @@ import type { IAuthResource } from '../../auth/types'; interface LogoutProps { authResource: IAuthResource; + authStorage?: AuthStorage; onComplete: () => void; } -export const Logout: React.FC = ({ authResource, onComplete }) => { +export const Logout: React.FC = ({ + authResource, + authStorage = defaultStorage, + onComplete, +}) => { + const storage = authStorage; const [done, setDone] = useState(false); useEffect(() => { @@ -28,7 +34,7 @@ export const Logout: React.FC = ({ authResource, onComplete }) => { setTimeout(onComplete, 1000); }; run(); - }, [authResource, onComplete]); + }, [authResource, onComplete, storage]); if (!done) { return null; diff --git a/packages/cli/src/commands/auth/status.tsx b/packages/cli/src/commands/auth/status.tsx index 008906d..dc7883a 100644 --- a/packages/cli/src/commands/auth/status.tsx +++ b/packages/cli/src/commands/auth/status.tsx @@ -1,13 +1,18 @@ -import { storage } from '@stripe/link-sdk'; +import { type AuthStorage, storage as defaultStorage } from '@stripe/link-sdk'; import { Box, Text } from 'ink'; import type React from 'react'; import { useEffect, useState } from 'react'; interface AuthStatusProps { + authStorage?: AuthStorage; onComplete: () => void; } -export const AuthStatus: React.FC = ({ onComplete }) => { +export const AuthStatus: React.FC = ({ + authStorage = defaultStorage, + onComplete, +}) => { + const storage = authStorage; const [checked, setChecked] = useState(false); const [authenticated, setAuthenticated] = useState(false); const [tokenPreview, setTokenPreview] = useState(''); @@ -25,7 +30,7 @@ export const AuthStatus: React.FC = ({ onComplete }) => { setCredentialsPath(credentialsPath); setChecked(true); setTimeout(onComplete, 1000); - }, [onComplete]); + }, [onComplete, storage]); if (!checked) { return null; diff --git a/packages/cli/src/commands/demo/demo-runner.tsx b/packages/cli/src/commands/demo/demo-runner.tsx index 96876a5..ff6568c 100644 --- a/packages/cli/src/commands/demo/demo-runner.tsx +++ b/packages/cli/src/commands/demo/demo-runner.tsx @@ -1,8 +1,9 @@ import type { + AuthStorage, IPaymentMethodsResource, ISpendRequestResource, } from '@stripe/link-sdk'; -import { storage } from '@stripe/link-sdk'; +import { storage as defaultStorage } from '@stripe/link-sdk'; import { Box, Text, useInput } from 'ink'; import type React from 'react'; import { useCallback, useState } from 'react'; @@ -27,6 +28,7 @@ interface DemoRunnerProps { authRepo: IAuthResource; spendRequestRepo: ISpendRequestResource; paymentMethodsResource: IPaymentMethodsResource; + authStorage?: AuthStorage; paymentMethodId?: string; onlyCard?: boolean; onlySpt?: boolean; @@ -37,11 +39,13 @@ export const DemoRunner: React.FC = ({ authRepo, spendRequestRepo, paymentMethodsResource, + authStorage = defaultStorage, paymentMethodId: preselectedPmId, onlyCard, onlySpt, onComplete, }) => { + const storage = authStorage; const preselected = onlyCard ? 'card' : onlySpt ? 'spt' : null; const [choice, setChoice] = useState(preselected); const [menuIndex, setMenuIndex] = useState(0); @@ -112,6 +116,7 @@ export const DemoRunner: React.FC = ({ setPhase(postAuthPhase)} /> )} diff --git a/packages/cli/src/commands/demo/index.tsx b/packages/cli/src/commands/demo/index.tsx index e190415..08200a1 100644 --- a/packages/cli/src/commands/demo/index.tsx +++ b/packages/cli/src/commands/demo/index.tsx @@ -1,4 +1,5 @@ import type { + AuthStorage, IPaymentMethodsResource, ISpendRequestResource, } from '@stripe/link-sdk'; @@ -23,6 +24,7 @@ export function createDemoCli( authRepo: IAuthResource, spendRequestRepo: ISpendRequestResource, createPaymentMethodsResource: () => IPaymentMethodsResource, + authStorage?: AuthStorage, ) { return Cli.create('demo', { description: @@ -45,6 +47,7 @@ export function createDemoCli( authRepo={authRepo} spendRequestRepo={spendRequestRepo} paymentMethodsResource={paymentMethodsResource} + authStorage={authStorage} onlyCard={c.options.onlyCard} onlySpt={c.options.onlySpt} onComplete={() => unmount()} diff --git a/packages/cli/src/commands/mpp/index.tsx b/packages/cli/src/commands/mpp/index.tsx index 8e4c0f3..ec2ecb0 100644 --- a/packages/cli/src/commands/mpp/index.tsx +++ b/packages/cli/src/commands/mpp/index.tsx @@ -1,5 +1,5 @@ -import type { ISpendRequestResource } from '@stripe/link-sdk'; -import { storage } from '@stripe/link-sdk'; +import type { AuthStorage, ISpendRequestResource } from '@stripe/link-sdk'; +import { storage as defaultStorage } from '@stripe/link-sdk'; import { Cli, z } from 'incur'; import { render } from 'ink'; import React from 'react'; @@ -8,7 +8,11 @@ import { DecodeChallengeView } from './decode-view'; import { MppPay, runMppPay } from './pay'; import { decodeOptions, payOptions } from './schema'; -export function createMppCli(repository: ISpendRequestResource) { +export function createMppCli( + repository: ISpendRequestResource, + authStorage?: AuthStorage, +) { + const storage = authStorage ?? defaultStorage; const cli = Cli.create('mpp', { description: 'Machine payment protocol (MPP) commands', }); diff --git a/packages/cli/src/commands/onboard/index.tsx b/packages/cli/src/commands/onboard/index.tsx index 8183ce5..a39d831 100644 --- a/packages/cli/src/commands/onboard/index.tsx +++ b/packages/cli/src/commands/onboard/index.tsx @@ -1,4 +1,5 @@ import type { + AuthStorage, IPaymentMethodsResource, ISpendRequestResource, } from '@stripe/link-sdk'; @@ -12,6 +13,7 @@ export function createOnboardCli( authRepo: IAuthResource, spendRequestRepo: ISpendRequestResource, createPaymentMethodsResource: () => IPaymentMethodsResource, + authStorage?: AuthStorage, ) { return Cli.create('onboard', { description: @@ -33,6 +35,7 @@ export function createOnboardCli( authRepo={authRepo} spendRequestRepo={spendRequestRepo} paymentMethodsResource={paymentMethodsResource} + authStorage={authStorage} onComplete={() => unmount()} />, ); diff --git a/packages/cli/src/commands/onboard/onboard-runner.tsx b/packages/cli/src/commands/onboard/onboard-runner.tsx index 1040c7e..b5a40d0 100644 --- a/packages/cli/src/commands/onboard/onboard-runner.tsx +++ b/packages/cli/src/commands/onboard/onboard-runner.tsx @@ -1,8 +1,9 @@ import type { + AuthStorage, IPaymentMethodsResource, ISpendRequestResource, } from '@stripe/link-sdk'; -import { storage } from '@stripe/link-sdk'; +import { storage as defaultStorage } from '@stripe/link-sdk'; import { Box, Text, useInput } from 'ink'; import type React from 'react'; import { useEffect, useRef, useState } from 'react'; @@ -17,6 +18,7 @@ interface OnboardRunnerProps { authRepo: IAuthResource; spendRequestRepo: ISpendRequestResource; paymentMethodsResource: IPaymentMethodsResource; + authStorage?: AuthStorage; onComplete: () => void; } @@ -24,8 +26,10 @@ export const OnboardRunner: React.FC = ({ authRepo, spendRequestRepo, paymentMethodsResource, + authStorage = defaultStorage, onComplete, }) => { + const storage = authStorage; const [phase, setPhase] = useState('welcome'); const [authSkipped, setAuthSkipped] = useState(false); const [pmMissing, setPmMissing] = useState(false); @@ -121,6 +125,7 @@ export const OnboardRunner: React.FC = ({ authResolver.current?.()} /> ) : null} @@ -163,6 +168,7 @@ export const OnboardRunner: React.FC = ({ authRepo={authRepo} spendRequestRepo={spendRequestRepo} paymentMethodsResource={paymentMethodsResource} + authStorage={storage} onComplete={onComplete} /> diff --git a/packages/cli/src/commands/payment-methods/index.tsx b/packages/cli/src/commands/payment-methods/index.tsx index 3504916..7df05ae 100644 --- a/packages/cli/src/commands/payment-methods/index.tsx +++ b/packages/cli/src/commands/payment-methods/index.tsx @@ -1,5 +1,5 @@ -import type { IPaymentMethodsResource } from '@stripe/link-sdk'; -import { storage } from '@stripe/link-sdk'; +import type { AuthStorage, IPaymentMethodsResource } from '@stripe/link-sdk'; +import { storage as defaultStorage } from '@stripe/link-sdk'; import { Cli } from 'incur'; import { render } from 'ink'; import React from 'react'; @@ -8,7 +8,9 @@ import { PaymentMethodsList } from './list'; export function createPaymentMethodsCli( createResource: () => IPaymentMethodsResource, + authStorage?: AuthStorage, ) { + const storage = authStorage ?? defaultStorage; const cli = Cli.create('payment-methods', { description: 'Payment methods management commands', }); diff --git a/packages/cli/src/commands/spend-request/index.tsx b/packages/cli/src/commands/spend-request/index.tsx index deefcd0..f2c7f56 100644 --- a/packages/cli/src/commands/spend-request/index.tsx +++ b/packages/cli/src/commands/spend-request/index.tsx @@ -1,11 +1,12 @@ import type { + AuthStorage, CredentialType, ISpendRequestResource, LineItem, SpendRequest, Total, } from '@stripe/link-sdk'; -import { storage } from '@stripe/link-sdk'; +import { storage as defaultStorage } from '@stripe/link-sdk'; import { Cli, z } from 'incur'; import { render } from 'ink'; import React from 'react'; @@ -43,7 +44,11 @@ async function applyOutputFile( } as SpendRequest & { card_output_file?: string }; } -export function createSpendRequestCli(repository: ISpendRequestResource) { +export function createSpendRequestCli( + repository: ISpendRequestResource, + authStorage?: AuthStorage, +) { + const storage = authStorage ?? defaultStorage; const cli = Cli.create('spend-request', { description: 'Spend request management commands', }); diff --git a/packages/cli/src/utils/resource-factory.ts b/packages/cli/src/utils/resource-factory.ts index 8c2d901..c251a26 100644 --- a/packages/cli/src/utils/resource-factory.ts +++ b/packages/cli/src/utils/resource-factory.ts @@ -1,4 +1,5 @@ import { + type AuthStorage, type IPaymentMethodsResource, type ISpendRequestResource, PaymentMethodsResource, @@ -11,11 +12,13 @@ import type { IAuthResource } from '../auth/types'; interface ResourceFactoryOptions { verbose?: boolean; defaultHeaders?: Record; + authStorage?: AuthStorage; } export class ResourceFactory { private readonly verbose: boolean; private readonly defaultHeaders?: Record; + private readonly authStorage?: AuthStorage; private authResource?: IAuthResource; private accessTokenProvider?: ReturnType; private spendRequestResource?: ISpendRequestResource; @@ -24,6 +27,7 @@ export class ResourceFactory { constructor(options: ResourceFactoryOptions = {}) { this.verbose = options.verbose ?? false; this.defaultHeaders = options.defaultHeaders; + this.authStorage = options.authStorage; } createAuthResource(): IAuthResource { @@ -39,6 +43,10 @@ export class ResourceFactory { return this.authResource; } + getAuthStorage(): AuthStorage | undefined { + return this.authStorage; + } + private createSdkAccessTokenProvider() { if (this.accessTokenProvider) { return this.accessTokenProvider; @@ -46,6 +54,7 @@ export class ResourceFactory { this.accessTokenProvider = createAccessTokenProvider( this.createAuthResource(), + this.authStorage, ); return this.accessTokenProvider; } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 00696fe..7f8166b 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -7,5 +7,9 @@ export * from './resources/interfaces'; export * from './resources/auth'; export * from './resources/spend-request'; export * from './resources/payment-methods'; -export { MemoryStorage, storage } from './utils/storage'; -export type { AuthStorage, PendingDeviceAuth } from './utils/storage'; +export { MemoryStorage, Storage, storage } from './utils/storage'; +export type { + AuthStorage, + PendingDeviceAuth, + StorageOptions, +} from './utils/storage'; diff --git a/packages/sdk/src/utils/__tests__/storage.test.ts b/packages/sdk/src/utils/__tests__/storage.test.ts index 70dcc54..e04b946 100644 --- a/packages/sdk/src/utils/__tests__/storage.test.ts +++ b/packages/sdk/src/utils/__tests__/storage.test.ts @@ -104,6 +104,41 @@ describePosix('Storage (disk-backed) file permissions', () => { expect(fs.statSync(configPath).mode & 0o777).toBe(0o600); }); + it('configPath option writes to the specified file path', () => { + const customPath = path.join(tmpDir, 'custom-creds.json'); + const storage = new Storage({ configPath: customPath }); + + storage.setAuth({ + access_token: 'at_custom', + refresh_token: 'rt_custom', + expires_in: 3600, + token_type: 'Bearer', + }); + + expect(storage.getPath()).toBe(customPath); + expect(fs.existsSync(customPath)).toBe(true); + expect(storage.getAuth()?.access_token).toBe('at_custom'); + + const mode = fs.statSync(customPath).mode & 0o777; + expect(mode).toBe(0o600); + }); + + it('configPath takes precedence over cwd', () => { + const customPath = path.join(tmpDir, 'override.json'); + const otherDir = fs.mkdtempSync(path.join(os.tmpdir(), 'link-cli-other-')); + const storage = new Storage({ configPath: customPath, cwd: otherDir }); + + storage.setAuth({ + access_token: 'at_override', + refresh_token: 'rt_override', + expires_in: 3600, + token_type: 'Bearer', + }); + + expect(storage.getPath()).toBe(customPath); + fs.rmSync(otherDir, { recursive: true, force: true }); + }); + it('also restricts pendingDeviceAuth, which is written to the same file', () => { const storage = new Storage({ cwd: tmpDir }); diff --git a/packages/sdk/src/utils/storage.ts b/packages/sdk/src/utils/storage.ts index cf8ee53..65d897d 100644 --- a/packages/sdk/src/utils/storage.ts +++ b/packages/sdk/src/utils/storage.ts @@ -1,4 +1,5 @@ import fs from 'node:fs'; +import path from 'node:path'; import type { AuthTokens } from '@/types/index'; import Conf from 'conf'; @@ -48,6 +49,9 @@ export interface StorageOptions { // conf resolves to the platform user-config directory. Tests pass a temp // dir so they don't touch the real location. cwd?: string; + // Full file path for the credential file. When set, takes precedence over + // cwd. The file is split into directory + config name for conf. + configPath?: string; } export class Storage implements AuthStorage { @@ -60,10 +64,19 @@ export class Storage implements AuthStorage { private getConfig(): Conf { if (!this.config) { + let locationOverride: { cwd: string; configName?: string } | undefined; + if (this.options.configPath) { + const parsed = path.parse(path.resolve(this.options.configPath)); + const configName = parsed.ext === '.json' ? parsed.name : parsed.base; + locationOverride = { cwd: parsed.dir, configName }; + } else if (this.options.cwd) { + locationOverride = { cwd: this.options.cwd }; + } + this.config = new Conf({ projectName: 'link-cli', configFileMode: CONFIG_FILE_MODE, - ...(this.options.cwd ? { cwd: this.options.cwd } : {}), + ...locationOverride, defaults: { auth: null, pendingDeviceAuth: null, diff --git a/skills/create-payment-credential/SKILL.md b/skills/create-payment-credential/SKILL.md index e5ffcca..93fc329 100644 --- a/skills/create-payment-credential/SKILL.md +++ b/skills/create-payment-credential/SKILL.md @@ -64,6 +64,7 @@ Call `tools/list` to see all available MCP tools. - Multi-step commands return a `_next` action. For example, authenticating or creating a spend request returns a `_next.command` that must be run to complete the flow. - By default all output is in `toon` format. Pass `--format [json|md|yaml]` to change output format. - Some commands return a verification or approval URL. **These** must be presented to the user clearly for their action. +- `--auth ` — global flag to read/write auth credentials to a specific file instead of the default location. Useful when running multiple sessions or isolating credentials from the agent's context. Also available as `LINK_AUTH_FILE` env var. _Recommended_: Run `link-cli --llms` to understand all the available commands. The `--llms-full` output is the canonical reference for parameter names, types, and valid values. Pass `--schema` before invoking a command to understand its parameters and constraints. From cd92fa9e24703ab6d705c69c4b729a606549e7ab Mon Sep 17 00:00:00 2001 From: System Administrator Date: Thu, 7 May 2026 10:48:04 -0400 Subject: [PATCH 2/3] copy cleanup --- README.md | 8 ++------ skills/create-payment-credential/SKILL.md | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 705f899..15747d9 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,8 @@ When you provide `--client-name`, the Link app displays it when you approve the Set `NO_UPDATE_NOTIFIER=1` to suppress update checks (for example, in CI). +All commands accept `--auth ` to read/write auth credentials to a specific file instead of the default location. Useful for running multiple sessions with separate identities. + ### Spend request lifecycle A spend request moves through: **create** → **request approval** → **approved** (with credentials). @@ -226,12 +228,6 @@ link-cli mpp decode \ --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' ``` -### Global flags - -| Flag | Effect | -|------|--------| -| `--auth ` | Read/write auth credentials to a specific file instead of the default location. Useful for running multiple sessions with separate identities. | - ### Environment variables | Variable | Effect | diff --git a/skills/create-payment-credential/SKILL.md b/skills/create-payment-credential/SKILL.md index 93fc329..9fc35d4 100644 --- a/skills/create-payment-credential/SKILL.md +++ b/skills/create-payment-credential/SKILL.md @@ -64,7 +64,7 @@ Call `tools/list` to see all available MCP tools. - Multi-step commands return a `_next` action. For example, authenticating or creating a spend request returns a `_next.command` that must be run to complete the flow. - By default all output is in `toon` format. Pass `--format [json|md|yaml]` to change output format. - Some commands return a verification or approval URL. **These** must be presented to the user clearly for their action. -- `--auth ` — global flag to read/write auth credentials to a specific file instead of the default location. Useful when running multiple sessions or isolating credentials from the agent's context. Also available as `LINK_AUTH_FILE` env var. +- `--auth ` flag to read/write auth credentials to a specific file instead of the default location. Useful when running multiple sessions or isolating credentials. Example: `link-cli auth login --auth credentials.json` _Recommended_: Run `link-cli --llms` to understand all the available commands. The `--llms-full` output is the canonical reference for parameter names, types, and valid values. Pass `--schema` before invoking a command to understand its parameters and constraints. From 43002529ccfc9afec7231003dbb4fa00a40ffdb1 Mon Sep 17 00:00:00 2001 From: System Administrator Date: Fri, 8 May 2026 12:01:21 -0400 Subject: [PATCH 3/3] fix: make flag clearer --- CLAUDE.md | 2 +- README.md | 2 +- packages/sdk/src/utils/storage.ts | 1 + skills/create-payment-credential/SKILL.md | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b731c00..2180d42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,7 +101,7 @@ Key input field notes: | Flag | Effect | |------|--------| -| `--auth ` | Read/write auth credentials to a specific file instead of the default platform config location. Parsed from `process.argv` and stripped before incur processes flags. | +| `--auth ` | Store auth credentials in a specific file instead of the default platform config location. `auth login` writes to this file; all other commands read from it. Parsed from `process.argv` and stripped before incur processes flags. | ## Environment Variables diff --git a/README.md b/README.md index 15747d9..a0b6d17 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ When you provide `--client-name`, the Link app displays it when you approve the Set `NO_UPDATE_NOTIFIER=1` to suppress update checks (for example, in CI). -All commands accept `--auth ` to read/write auth credentials to a specific file instead of the default location. Useful for running multiple sessions with separate identities. +All commands accept `--auth ` to store auth credentials in a specific file instead of the default location. `auth login` writes to this file; all other commands read from it. Useful for running multiple sessions with separate identities. ### Spend request lifecycle diff --git a/packages/sdk/src/utils/storage.ts b/packages/sdk/src/utils/storage.ts index 65d897d..2d38cad 100644 --- a/packages/sdk/src/utils/storage.ts +++ b/packages/sdk/src/utils/storage.ts @@ -67,6 +67,7 @@ export class Storage implements AuthStorage { let locationOverride: { cwd: string; configName?: string } | undefined; if (this.options.configPath) { const parsed = path.parse(path.resolve(this.options.configPath)); + // conf appends `.json` to configName, so strip it to avoid double extension const configName = parsed.ext === '.json' ? parsed.name : parsed.base; locationOverride = { cwd: parsed.dir, configName }; } else if (this.options.cwd) { diff --git a/skills/create-payment-credential/SKILL.md b/skills/create-payment-credential/SKILL.md index 9fc35d4..d3814db 100644 --- a/skills/create-payment-credential/SKILL.md +++ b/skills/create-payment-credential/SKILL.md @@ -64,7 +64,7 @@ Call `tools/list` to see all available MCP tools. - Multi-step commands return a `_next` action. For example, authenticating or creating a spend request returns a `_next.command` that must be run to complete the flow. - By default all output is in `toon` format. Pass `--format [json|md|yaml]` to change output format. - Some commands return a verification or approval URL. **These** must be presented to the user clearly for their action. -- `--auth ` flag to read/write auth credentials to a specific file instead of the default location. Useful when running multiple sessions or isolating credentials. Example: `link-cli auth login --auth credentials.json` +- `--auth ` flag to store auth credentials in a specific file instead of the default location. `auth login` writes to this file; all other commands read from it. Example: `link-cli auth login --auth credentials.json` _Recommended_: Run `link-cli --llms` to understand all the available commands. The `--llms-full` output is the canonical reference for parameter names, types, and valid values. Pass `--schema` before invoking a command to understand its parameters and constraints.