diff --git a/CLAUDE.md b/CLAUDE.md index f1472b0..899dbb7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,6 +87,7 @@ Key input field notes: - `--approval-detail` — optional JSON object (MCP/agent) or JSON string (CLI) with approval details for delegated flows. Required fields: `approved_at` (unix timestamp int), `approval_method` (`click`|`programmatic`|`voice`), `app_name`, `external_user_id`. Optional: `ip_address`, `user_agent`, `device_type` (`mobile`|`web`), `agent_log_id`, `external_user_name`, `external_session_id`, `authentication_method` (`biometric_face`|`biometric_fingerprint`|`passkey`). Sent as `approval_details` in the API request body. - `card` credentials include `billing_address` (name, line1, line2, city, state, postal_code, country) and `valid_until` (ISO date string — when the card expires/stops working) - `--output-file ` on `retrieve` or `create` writes full card credentials to a local file (0600 permissions) and redacts card data in stdout. `--force` allows overwriting an existing file. +- `create` also accepts an undocumented `--expires-at ` to override the default 12-hour spend request expiration (3 hours to 7 days in the future). It's deliberately excluded from `--schema`/`--llms-full` output and from README/SKILL.md: it's gated to an allow-list of OAuth clients server-side, and most callers get a 400 (`"expires_at is not supported for this client"`) if they try it — don't document or suggest it to general agents. ### mpp pay diff --git a/packages/cli/src/__tests__/cli.test.ts b/packages/cli/src/__tests__/cli.test.ts index ea87ea0..e717186 100644 --- a/packages/cli/src/__tests__/cli.test.ts +++ b/packages/cli/src/__tests__/cli.test.ts @@ -556,6 +556,58 @@ describe('production mode', () => { expect(sentBody.metadata).toBeUndefined(); }); + it('sends expires_at in POST body when --expires-at is used', async () => { + setNextResponse(200, BASE_REQUEST); + + const result = await runProdCli( + 'spend-request', + 'create', + '--payment-method-id', + 'pd_prod_test', + '--merchant-name', + 'Test Merchant', + '--merchant-url', + 'https://example.com', + '--context', + VALID_CONTEXT, + '--amount', + '5000', + '--expires-at', + '1720100000', + '--no-request-approval', + '--json', + ); + + expect(result.exitCode).toBe(0); + const sentBody = JSON.parse(lastRequest.body); + expect(sentBody.expires_at).toBe(1720100000); + }); + + it('does not include expires_at in POST body when --expires-at is omitted', async () => { + setNextResponse(200, BASE_REQUEST); + + const result = await runProdCli( + 'spend-request', + 'create', + '--payment-method-id', + 'pd_prod_test', + '--merchant-name', + 'Test Merchant', + '--merchant-url', + 'https://example.com', + '--context', + VALID_CONTEXT, + '--amount', + '5000', + '--no-request-approval', + '--json', + ); + + expect(result.exitCode).toBe(0); + const sentBody = JSON.parse(lastRequest.body); + expect(sentBody.expires_at).toBeUndefined(); + }); + it('sends test flag in POST body when --test is used', async () => { setNextResponse(200, BASE_REQUEST); diff --git a/packages/cli/src/commands/spend-request/index.tsx b/packages/cli/src/commands/spend-request/index.tsx index 3b640c9..75fc35c 100644 --- a/packages/cli/src/commands/spend-request/index.tsx +++ b/packages/cli/src/commands/spend-request/index.tsx @@ -270,6 +270,7 @@ export function createSpendRequestCli( approve: opts.approve ? true : undefined, approval_details: approvalDetails, metadata, + expires_at: opts.expiresAt, }; const outputFile = opts.outputFile; diff --git a/packages/cli/src/commands/spend-request/schema.ts b/packages/cli/src/commands/spend-request/schema.ts index 360de8b..d050516 100644 --- a/packages/cli/src/commands/spend-request/schema.ts +++ b/packages/cli/src/commands/spend-request/schema.ts @@ -98,6 +98,11 @@ export const createOptions = z.object({ .describe( 'Metadata key:value pair (repeatable). Attaches arbitrary string data to the spend request. Max 50 keys, key <= 40 chars, value <= 500 chars. Example: "order_id:ord_123"', ), + expiresAt: z.coerce + .number() + .int() + .optional() + .describe('Unix timestamp (seconds).'), }); export const listOptions = z.object({ diff --git a/packages/sdk/src/resources/interfaces.ts b/packages/sdk/src/resources/interfaces.ts index cd69673..852d67f 100644 --- a/packages/sdk/src/resources/interfaces.ts +++ b/packages/sdk/src/resources/interfaces.ts @@ -69,6 +69,7 @@ export interface CreateSpendRequestParams { approve?: boolean; approval_details?: ApprovalDetail; metadata?: Record; + expires_at?: number; } export interface UpdateSpendRequestParams { diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index 8335a1b..f4b03c4 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -174,6 +174,7 @@ export interface SpendRequest { link_transaction_id?: string; activity_url?: string; metadata?: Record; + expires_at?: number; created_at: string; updated_at: string; }