From a5ed847eb272d7190bdb4c8a4116a7aa096635c6 Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 19 Oct 2026 00:45:44 +0000 Subject: [PATCH 1/4] feat(core): expose card spending limits and refund receipts --- .changeset/card-spending-limits.md | 5 + packages/core/CHANGELOG.md | 4 + packages/core/README.md | 33 ++++++ .../core/examples/card-spending-limits.ts | 32 ++++++ .../core/src/client/PaymentCardsAPI.test.ts | 102 ++++++++++++++++++ packages/core/src/client/PaymentCardsAPI.ts | 29 +++++ packages/core/src/client/SapiomClient.ts | 3 + packages/core/src/client/TransactionAPI.ts | 16 +++ packages/core/src/index.ts | 11 ++ packages/core/src/types/payment-card.ts | 69 ++++++++++++ 10 files changed, 304 insertions(+) create mode 100644 .changeset/card-spending-limits.md create mode 100644 packages/core/examples/card-spending-limits.ts create mode 100644 packages/core/src/client/PaymentCardsAPI.test.ts create mode 100644 packages/core/src/client/PaymentCardsAPI.ts create mode 100644 packages/core/src/types/payment-card.ts diff --git a/.changeset/card-spending-limits.md b/.changeset/card-spending-limits.md new file mode 100644 index 000000000..33408ff22 --- /dev/null +++ b/.changeset/card-spending-limits.md @@ -0,0 +1,5 @@ +--- +"@sapiom/core": minor +--- + +Add card spending-limit management and trusted confirmed-refund recording. These methods require the corresponding backend API deployment and management permissions. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d90f39feb..1c346b8e9 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,9 @@ # @sapiom/core +## Unreleased + +- Add trusted-platform card spending-limit read/write methods and confirmed-refund recording, with versioned settings and explicit evidence/idempotency types. Requires the corresponding backend deployment; package publication is pending. + ## 0.5.0 ### Minor Changes diff --git a/packages/core/README.md b/packages/core/README.md index dfcd70a34..e58bef0b5 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -19,6 +19,39 @@ Core SDK for Sapiom API providing HTTP client adapters, transaction management, ## Installation +### Card spending limits (unreleased) + +The working-tree client adds management methods for platforms integrating saved-card purchases. +Use a backend credential with `org.read` to read limits and `org.write` to change them or record +verified refunds. Keep that credential outside the purchasing agent. These methods require the +matching backend deployment and SDK release. + +```typescript +const saved = await client.paymentCards.getSpendingLimits(cardId); +const updated = await client.paymentCards.setSpendingLimits(cardId, { + version: saved.version, + purchaseLimit: { amount: "100.00", currency: "USD" }, + recurringLimit: { amount: "500.00", currency: "USD", period: "month" }, + customLimit: null, +}); +console.log(updated.usage.recurringLimit?.remaining); +``` + +All three nullable slots are required. Custom limits use inclusive UTC `YYYY-MM-DD` start/end +dates; recurring periods are UTC calendar weeks (Monday start), months or years. An unchanged +save retains usage; a changed cumulative budget starts a fresh allowance. Handle a version +conflict by reading again and resolving the edit. Removing all limits blocks purchasing. + +`client.transactions.recordRefundConfirmation(transactionId, receipt)` records a refund your +platform has already verified. Supply the original payment ID, external refund ID, amount and +currency, confirmation time, idempotency key and source/order references. It never initiates +a merchant refund. A refund credits the purchase's original allowance; an older-period refund +does not increase today's allowance. A receipt awaiting purchase confirmation remains pending. + +The reusable example is [examples/card-spending-limits.ts](examples/card-spending-limits.ts). + +### Install the published package + ```bash npm install @sapiom/core # or diff --git a/packages/core/examples/card-spending-limits.ts b/packages/core/examples/card-spending-limits.ts new file mode 100644 index 000000000..938d23f3c --- /dev/null +++ b/packages/core/examples/card-spending-limits.ts @@ -0,0 +1,32 @@ +import { SapiomClient } from "@sapiom/core"; + +/** Run from a trusted platform backend after the matching API and SDK have been released. */ +async function configureCard() { + const apiKey = process.env.SAPIOM_MANAGEMENT_API_KEY; + const cardId = process.env.SAPIOM_CARD_ID; + if (!apiKey || !cardId) + throw new Error("Set SAPIOM_MANAGEMENT_API_KEY and SAPIOM_CARD_ID"); + const client = new SapiomClient({ + apiKey, + baseURL: process.env.SAPIOM_API_URL, + backgroundRefresh: false, + }); + const current = await client.paymentCards.getSpendingLimits(cardId); + const saved = await client.paymentCards.setSpendingLimits(cardId, { + version: current.version, + purchaseLimit: { amount: "100.00", currency: "USD" }, + recurringLimit: { amount: "500.00", currency: "USD", period: "month" }, + customLimit: null, + }); + console.log( + JSON.stringify( + { cardId, version: saved.version, usage: saved.usage }, + null, + 2, + ), + ); +} +void configureCard().catch((error: Error) => { + console.error(error.message); + process.exitCode = 1; +}); diff --git a/packages/core/src/client/PaymentCardsAPI.test.ts b/packages/core/src/client/PaymentCardsAPI.test.ts new file mode 100644 index 000000000..9189e31ba --- /dev/null +++ b/packages/core/src/client/PaymentCardsAPI.test.ts @@ -0,0 +1,102 @@ +import { SapiomClient } from "./SapiomClient"; +import type { + CardSpendingLimitsSettings, + RefundConfirmationRequest, +} from "../types/payment-card"; + +describe("Card-limit management through the public client", () => { + const cardId = "11111111-1111-4111-8111-111111111111"; + const transactionId = "22222222-2222-4222-8222-222222222222"; + let client: SapiomClient; + let fetchMock: jest.Mock; + const originalFetch = global.fetch; + beforeEach(() => { + fetchMock = jest.fn(); + global.fetch = fetchMock; + client = new SapiomClient({ + apiKey: "fixture-management-key", + baseURL: "https://api.test", + backgroundRefresh: false, + }); + }); + afterEach(() => { + global.fetch = originalFetch; + }); + const json = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); + const settings: CardSpendingLimitsSettings = { + version: 0, + purchaseLimit: { amount: "100.00", currency: "USD" }, + recurringLimit: { amount: "500.00", currency: "USD", period: "month" }, + customLimit: null, + }; + + it("uses versioned routes and preserves explicit slots and decimal amounts", async () => { + fetchMock.mockResolvedValueOnce(json({ ...settings, version: 1 })); + expect( + (await client.paymentCards.setSpendingLimits(cardId, settings)).version, + ).toBe(1); + const [url, request] = fetchMock.mock.calls[0]; + expect(String(url)).toBe( + `https://api.test/v1/payment-cards/${cardId}/spending-limits`, + ); + expect(request.method).toBe("PUT"); + expect(JSON.parse(request.body)).toEqual(settings); + expect(request.headers["x-api-key"]).toBe("fixture-management-key"); + fetchMock.mockResolvedValueOnce( + json({ + ...settings, + version: 1, + usage: { recurringLimit: { remaining: "450.00" } }, + }), + ); + expect( + (await client.paymentCards.getSpendingLimits(cardId)).usage.recurringLimit + ?.remaining, + ).toBe("450.00"); + }); + it("propagates a stale-version conflict without silently retrying a reset", async () => { + fetchMock.mockResolvedValue(json({ code: "RULE_VERSION_CONFLICT" }, 409)); + await expect( + client.paymentCards.setSpendingLimits(cardId, settings), + ).rejects.toThrow(/409.*RULE_VERSION_CONFLICT/); + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + it("preserves refund identity, evidence and idempotency through transport retries", async () => { + const receipt: RefundConfirmationRequest = { + paymentTransactionId: transactionId, + externalRefundId: "refund-1", + amount: "10.00", + currency: "USD", + confirmedAt: "2026-10-18T12:00:00Z", + idempotencyKey: "refund-1", + evidence: { + source: "trusted_platform", + sourceReference: "verified-receipt-1", + merchantOrderId: "order-1", + }, + }; + fetchMock + .mockResolvedValueOnce(json({ message: "temporary" }, 503)) + .mockResolvedValueOnce(json({ id: "receipt-1", state: "applied" })); + expect( + ( + await client.transactions.recordRefundConfirmation( + transactionId, + receipt, + ) + ).state, + ).toBe("applied"); + expect(fetchMock).toHaveBeenCalledTimes(2); + for (const [url, request] of fetchMock.mock.calls) { + expect(String(url)).toBe( + `https://api.test/v1/transactions/${transactionId}/refund-confirmations`, + ); + expect(request.method).toBe("POST"); + expect(JSON.parse(request.body)).toEqual(receipt); + } + }); +}); diff --git a/packages/core/src/client/PaymentCardsAPI.ts b/packages/core/src/client/PaymentCardsAPI.ts new file mode 100644 index 000000000..6d13a4ec4 --- /dev/null +++ b/packages/core/src/client/PaymentCardsAPI.ts @@ -0,0 +1,29 @@ +import type { + CardSpendingLimitsSettings, + CardSpendingLimitsSummary, +} from "../types/payment-card.js"; +import type { HttpClient } from "./HttpClient.js"; + +/** Management methods for a trusted platform backend; purchasing keys cannot change limits. */ +export class PaymentCardsAPI { + constructor(private readonly httpClient: HttpClient) {} + + async getSpendingLimits(cardId: string): Promise { + return this.httpClient.request({ + method: "GET", + url: `/payment-cards/${encodeURIComponent(cardId)}/spending-limits`, + }); + } + + /** All slots are explicit. Changed cumulative settings start fresh; unchanged saves preserve usage. */ + async setSpendingLimits( + cardId: string, + settings: CardSpendingLimitsSettings, + ): Promise { + return this.httpClient.request({ + method: "PUT", + url: `/payment-cards/${encodeURIComponent(cardId)}/spending-limits`, + body: settings, + }); + } +} diff --git a/packages/core/src/client/SapiomClient.ts b/packages/core/src/client/SapiomClient.ts index c4e2850fe..7bfa12098 100644 --- a/packages/core/src/client/SapiomClient.ts +++ b/packages/core/src/client/SapiomClient.ts @@ -3,6 +3,7 @@ import { HttpClient, HttpRequestConfig } from "./HttpClient.js"; import type { RetryConfig } from "./HttpClient.js"; import { IdentityManager } from "./IdentityManager.js"; import { TransactionAPI } from "./TransactionAPI.js"; +import { PaymentCardsAPI } from "./PaymentCardsAPI.js"; import { VaultAPI } from "./VaultAPI.js"; const DEFAULT_SERVICES_DOMAIN = "services.sapiom.ai"; @@ -92,6 +93,7 @@ export class SapiomClient { public readonly apiKeys: ApiKeyAPI; public readonly identity: IdentityManager; public readonly transactions: TransactionAPI; + public readonly paymentCards: PaymentCardsAPI; public readonly vault: VaultAPI; constructor(config: SapiomClientConfig) { @@ -128,6 +130,7 @@ export class SapiomClient { backgroundRefresh: config.backgroundRefresh, }); this.transactions = new TransactionAPI(this.httpClient); + this.paymentCards = new PaymentCardsAPI(this.httpClient); this.vault = new VaultAPI(this.vaultHttpClient); } diff --git a/packages/core/src/client/TransactionAPI.ts b/packages/core/src/client/TransactionAPI.ts index c5685af01..29401ff0b 100644 --- a/packages/core/src/client/TransactionAPI.ts +++ b/packages/core/src/client/TransactionAPI.ts @@ -10,10 +10,26 @@ import { TransactionCostResponse, } from "../types/transaction.js"; import type { HttpClient } from "./HttpClient.js"; +import type { + RefundConfirmationRequest, + RefundConfirmationReceipt, +} from "../types/payment-card.js"; export class TransactionAPI { constructor(private readonly httpClient: HttpClient) {} + /** Record a refund already verified by the trusted platform. Requires org.write; does not initiate a merchant refund. */ + async recordRefundConfirmation( + transactionId: string, + receipt: RefundConfirmationRequest, + ): Promise { + return this.httpClient.request({ + method: "POST", + url: `/transactions/${encodeURIComponent(transactionId)}/refund-confirmations`, + body: receipt, + }); + } + /** * Create a new transaction * @param data The transaction data diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 440bacb1b..2c2e9bc73 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -15,6 +15,17 @@ export { IdentityManager } from "./client/IdentityManager.js"; export type { IdentityManagerOptions } from "./client/IdentityManager.js"; export { ApiKeyAPI } from "./client/ApiKeyAPI.js"; export { TransactionAPI } from "./client/TransactionAPI.js"; +export { PaymentCardsAPI } from "./client/PaymentCardsAPI.js"; +export type { + CardMoneyLimit, + CardRecurringLimit, + CardCustomLimit, + CardSpendingLimitsSettings, + CardAllowanceUsage, + CardSpendingLimitsSummary, + RefundConfirmationRequest, + RefundConfirmationReceipt, +} from "./types/payment-card.js"; export { VaultAPI } from "./client/VaultAPI.js"; // Payment Error Detection diff --git a/packages/core/src/types/payment-card.ts b/packages/core/src/types/payment-card.ts new file mode 100644 index 000000000..61e112e13 --- /dev/null +++ b/packages/core/src/types/payment-card.ts @@ -0,0 +1,69 @@ +/** Positive USD amounts are decimal strings with at most two fractional digits. */ +export interface CardMoneyLimit { + amount: string; + currency: "USD"; +} +export interface CardRecurringLimit extends CardMoneyLimit { + period: "week" | "month" | "year"; +} +export interface CardCustomLimit extends CardMoneyLimit { + /** UTC YYYY-MM-DD; both supplied dates are inclusive. */ + startDate: string; + endDate: string; +} +export interface CardSpendingLimitsSettings { + /** Use the version returned by getSpendingLimits; 0 means no configuration exists yet. */ + version: number; + purchaseLimit: CardMoneyLimit | null; + recurringLimit: CardRecurringLimit | null; + customLimit: CardCustomLimit | null; +} +export interface CardAllowanceUsage { + available: boolean; + state: "active" | "future" | "expired" | "inactive" | "unavailable"; + currency: "USD"; + allowanceVersion: number; + startInclusive: string; + endExclusive: string; + resetAt: string | null; + effectiveStartInclusive: string | null; + /** Unavailable evidence produces null quantities, never an invented zero. */ + confirmedSpend: string | null; + pendingCommitted: string | null; + confirmedRefunds: string | null; + used: string | null; + remaining: string | null; +} +export interface CardSpendingLimitsSummary extends CardSpendingLimitsSettings { + ruleId: string | null; + status: "active" | "paused" | "removed"; + evaluatedAt: string; + /** Indicates effective configuration; each proposed purchase is still checked against all limits. */ + configurationActive: boolean; + usage: { + purchaseLimit?: null; + recurringLimit?: CardAllowanceUsage; + customLimit?: CardAllowanceUsage; + }; +} +export interface RefundConfirmationRequest { + paymentTransactionId: string; + externalRefundId: string; + amount: string; + currency: "USD"; + confirmedAt: string; + idempotencyKey: string; + evidence: { + source: "trusted_platform"; + sourceReference: string; + merchantOrderId: string; + }; +} +export interface RefundConfirmationReceipt { + id: string; + paymentTransactionId: string; + state: "applied" | "pending" | "conflicting" | "superseded"; + amount: string; + currency: "USD"; + confirmedAt: string; +} From 96e761c2685df9e10b7c82e01f9b4bf7bfaef90d Mon Sep 17 00:00:00 2001 From: Yash Date: Mon, 19 Oct 2026 06:14:34 +0000 Subject: [PATCH 2/4] docs(core): describe card limit read permissions --- packages/core/src/client/PaymentCardsAPI.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/client/PaymentCardsAPI.ts b/packages/core/src/client/PaymentCardsAPI.ts index 6d13a4ec4..496457093 100644 --- a/packages/core/src/client/PaymentCardsAPI.ts +++ b/packages/core/src/client/PaymentCardsAPI.ts @@ -8,6 +8,7 @@ import type { HttpClient } from "./HttpClient.js"; export class PaymentCardsAPI { constructor(private readonly httpClient: HttpClient) {} + /** Read a card's versioned spending limits and current usage. Requires org.read. */ async getSpendingLimits(cardId: string): Promise { return this.httpClient.request({ method: "GET", From d1a69a37dd63d9185bec08f9e0d83a2083e09744 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 19 Oct 2026 19:09:46 +0000 Subject: [PATCH 3/4] refactor(core): expose shared payment usage and remove refund confirmation --- packages/core/CHANGELOG.md | 2 +- packages/core/README.md | 116 ++++++++---------- .../core/src/client/PaymentCardsAPI.test.ts | 40 +----- packages/core/src/client/TransactionAPI.ts | 17 --- packages/core/src/index.ts | 3 +- packages/core/src/types/payment-card.ts | 41 +++---- 6 files changed, 71 insertions(+), 148 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 1c346b8e9..46741e87d 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Add trusted-platform card spending-limit read/write methods and confirmed-refund recording, with versioned settings and explicit evidence/idempotency types. Requires the corresponding backend deployment; package publication is pending. +- Add trusted-platform card spending-limit read/write helpers over ordinary payment Rules, with versioned settings, shared payment usage and additional applicable limits. Card spending is booked at credential release; no refund-confirmation API is provided. Requires the corresponding backend deployment; package publication is pending. ## 0.5.0 diff --git a/packages/core/README.md b/packages/core/README.md index e58bef0b5..cbc06a864 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -19,68 +19,50 @@ Core SDK for Sapiom API providing HTTP client adapters, transaction management, ## Installation -### Card spending limits (unreleased) +### Card spending limits -The working-tree client adds management methods for platforms integrating saved-card purchases. -Use a backend credential with `org.read` to read limits and `org.write` to change them or record -verified refunds. Keep that credential outside the purchasing agent. These methods require the -matching backend deployment and SDK release. +Use a trusted platform credential with `org.read` / `org.write` to manage a card's common spending rules. Keep write permissions outside the purchasing agent. These methods require the corresponding backend deployment. ```typescript -const saved = await client.paymentCards.getSpendingLimits(cardId); -const updated = await client.paymentCards.setSpendingLimits(cardId, { - version: saved.version, +const current = await client.paymentCards.getSpendingLimits(cardId); +const saved = await client.paymentCards.setSpendingLimits(cardId, { + version: current.version, purchaseLimit: { amount: "100.00", currency: "USD" }, recurringLimit: { amount: "500.00", currency: "USD", period: "month" }, customLimit: null, }); -console.log(updated.usage.recurringLimit?.remaining); +console.log(saved.usage.recurringLimit?.remaining); ``` -All three nullable slots are required. Custom limits use inclusive UTC `YYYY-MM-DD` start/end -dates; recurring periods are UTC calendar weeks (Monday start), months or years. An unchanged -save retains usage; a changed cumulative budget starts a fresh allowance. Handle a version -conflict by reading again and resolving the edit. Removing all limits blocks purchasing. +The helper owns one ordinary `payment_spending_limit` rule scoped to this card. Other Rules remain independent and appear in `additionalLimits`; transaction-specific conditions still require the full purchase context. A rule grouped by `paymentCardId` gives every card its own allowance. An ungrouped rule can share a budget across cards. -`client.transactions.recordRefundConfirmation(transactionId, receipt)` records a refund your -platform has already verified. Supply the original payment ID, external refund ID, amount and -currency, confirmation time, idempotency key and source/order references. It never initiates -a merchant refund. A refund credits the purchase's original allowance; an older-period refund -does not increase today's allowance. A receipt awaiting purchase confirmation remains pending. +Periods are UTC calendar weeks (Monday), months, years, or inclusive custom dates. A custom-date budget applies only within its dates. No rule means no spending-limit prerequisite. Other applicable Rules and card readiness checks still apply. -The reusable example is [examples/card-spending-limits.ts](examples/card-spending-limits.ts). +Pending payments reserve capacity. Releasing the raw card credential books the full payment amount once; later merchant failures or cancellation do not restore it. M0 does not infer charges or refunds from merchant outcomes and exposes no refund-confirmation API. -### Install the published package - -```bash -npm install @sapiom/core -# or -pnpm add @sapiom/core -# or -yarn add @sapiom/core -``` +Unchanged saves preserve version and usage. Changing a cumulative budget starts a fresh allowance for that slot; other slots retain usage. Submit the latest version to avoid conflicting updates. Quantities are null when accounting is unavailable. `configurationActive` describes this helper's configuration, not permission to purchase. ## Quick Start ### Direct API Client ```typescript -import { SapiomClient } from '@sapiom/core'; +import { SapiomClient } from "@sapiom/core"; const client = new SapiomClient({ apiKey: process.env.SAPIOM_API_KEY, - baseURL: 'https://api.sapiom.ai' // optional + baseURL: "https://api.sapiom.ai", // optional }); // Create a transaction const transaction = await client.transactions.create({ - service: 'api', - action: 'call', - resource: 'completion', + service: "api", + action: "call", + resource: "completion", qualifiers: { - model: 'gpt-4', - tokens: 1000 - } + model: "gpt-4", + tokens: 1000, + }, }); // Check transaction status @@ -96,17 +78,20 @@ npm install @sapiom/axios axios ``` ```typescript -import axios from 'axios'; -import { withSapiom } from '@sapiom/axios'; - -const client = withSapiom(axios.create({ - baseURL: 'https://api.example.com' -}), { - apiKey: process.env.SAPIOM_API_KEY -}); +import axios from "axios"; +import { withSapiom } from "@sapiom/axios"; + +const client = withSapiom( + axios.create({ + baseURL: "https://api.example.com", + }), + { + apiKey: process.env.SAPIOM_API_KEY, + }, +); // Automatically handles 402 payment flows -const response = await client.get('/premium-endpoint'); +const response = await client.get("/premium-endpoint"); ``` See [@sapiom/axios](../axios/README.md) for complete documentation. @@ -120,14 +105,14 @@ npm install @sapiom/fetch ``` ```typescript -import { createFetch } from '@sapiom/fetch'; +import { createFetch } from "@sapiom/fetch"; const safeFetch = createFetch({ - apiKey: process.env.SAPIOM_API_KEY + apiKey: process.env.SAPIOM_API_KEY, }); // Drop-in replacement for native fetch -const response = await safeFetch('https://api.example.com/data'); +const response = await safeFetch("https://api.example.com/data"); ``` See [@sapiom/fetch](../fetch/README.md) for complete documentation. @@ -141,15 +126,15 @@ npm install @sapiom/node-http ``` ```typescript -import { createClient } from '@sapiom/node-http'; +import { createClient } from "@sapiom/node-http"; const client = createClient({ - apiKey: process.env.SAPIOM_API_KEY + apiKey: process.env.SAPIOM_API_KEY, }); const response = await client.request({ - method: 'GET', - url: 'https://api.example.com/data' + method: "GET", + url: "https://api.example.com/data", }); ``` @@ -166,6 +151,7 @@ new SapiomClient(config: SapiomClientConfig) ``` **Config options:** + - `apiKey: string` - Required: Your Sapiom API key - `baseURL?: string` - Optional: API base URL (default: https://api.sapiom.ai) - `servicesDomain?: string` - Optional: Services domain for gateway clients (default: services.sapiom.ai) @@ -206,21 +192,21 @@ client.transactions.getPaymentDetails(transaction): PaymentDetails | null ```typescript // Store one secret under an external ref -await client.vault.set('agent-123', 'OPENAI_API_KEY', { - value: 'sk-...' +await client.vault.set("agent-123", "OPENAI_API_KEY", { + value: "sk-...", }); // Read one secret. Missing keys return null. -const openaiKey = await client.vault.get('agent-123', 'OPENAI_API_KEY'); +const openaiKey = await client.vault.get("agent-123", "OPENAI_API_KEY"); // Read a subset of secrets from the same ref -const secrets = await client.vault.getMany('agent-123', [ - 'OPENAI_API_KEY', - 'ANTHROPIC_API_KEY' +const secrets = await client.vault.getMany("agent-123", [ + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", ]); // Remove one key -await client.vault.deleteKey('agent-123', 'OPENAI_API_KEY'); +await client.vault.deleteKey("agent-123", "OPENAI_API_KEY"); ``` ## HTTP Integrations @@ -280,7 +266,11 @@ try { ### Custom HTTP Adapter ```typescript -import type { HttpClientAdapter, HttpRequest, HttpResponse } from '@sapiom/core'; +import type { + HttpClientAdapter, + HttpRequest, + HttpResponse, +} from "@sapiom/core"; class MyCustomAdapter implements HttpClientAdapter { async request(config: HttpRequest): Promise { @@ -292,11 +282,11 @@ class MyCustomAdapter implements HttpClientAdapter { ### Payment Error Detection ```typescript -import { isPaymentRequiredError, extractX402Response } from '@sapiom/core'; +import { isPaymentRequiredError, extractX402Response } from "@sapiom/core"; if (isPaymentRequiredError(error)) { const x402Response = extractX402Response(error); - console.log('Payment required:', x402Response); + console.log("Payment required:", x402Response); } ``` @@ -311,7 +301,7 @@ import type { HttpResponse, X402Response, X402PaymentRequirement, -} from '@sapiom/core'; +} from "@sapiom/core"; ``` ## License diff --git a/packages/core/src/client/PaymentCardsAPI.test.ts b/packages/core/src/client/PaymentCardsAPI.test.ts index 9189e31ba..a2ac2d009 100644 --- a/packages/core/src/client/PaymentCardsAPI.test.ts +++ b/packages/core/src/client/PaymentCardsAPI.test.ts @@ -1,12 +1,8 @@ import { SapiomClient } from "./SapiomClient"; -import type { - CardSpendingLimitsSettings, - RefundConfirmationRequest, -} from "../types/payment-card"; +import type { CardSpendingLimitsSettings } from "../types/payment-card"; describe("Card-limit management through the public client", () => { const cardId = "11111111-1111-4111-8111-111111111111"; - const transactionId = "22222222-2222-4222-8222-222222222222"; let client: SapiomClient; let fetchMock: jest.Mock; const originalFetch = global.fetch; @@ -65,38 +61,4 @@ describe("Card-limit management through the public client", () => { ).rejects.toThrow(/409.*RULE_VERSION_CONFLICT/); expect(fetchMock).toHaveBeenCalledTimes(1); }); - it("preserves refund identity, evidence and idempotency through transport retries", async () => { - const receipt: RefundConfirmationRequest = { - paymentTransactionId: transactionId, - externalRefundId: "refund-1", - amount: "10.00", - currency: "USD", - confirmedAt: "2026-10-18T12:00:00Z", - idempotencyKey: "refund-1", - evidence: { - source: "trusted_platform", - sourceReference: "verified-receipt-1", - merchantOrderId: "order-1", - }, - }; - fetchMock - .mockResolvedValueOnce(json({ message: "temporary" }, 503)) - .mockResolvedValueOnce(json({ id: "receipt-1", state: "applied" })); - expect( - ( - await client.transactions.recordRefundConfirmation( - transactionId, - receipt, - ) - ).state, - ).toBe("applied"); - expect(fetchMock).toHaveBeenCalledTimes(2); - for (const [url, request] of fetchMock.mock.calls) { - expect(String(url)).toBe( - `https://api.test/v1/transactions/${transactionId}/refund-confirmations`, - ); - expect(request.method).toBe("POST"); - expect(JSON.parse(request.body)).toEqual(receipt); - } - }); }); diff --git a/packages/core/src/client/TransactionAPI.ts b/packages/core/src/client/TransactionAPI.ts index 29401ff0b..de45d04a3 100644 --- a/packages/core/src/client/TransactionAPI.ts +++ b/packages/core/src/client/TransactionAPI.ts @@ -10,26 +10,9 @@ import { TransactionCostResponse, } from "../types/transaction.js"; import type { HttpClient } from "./HttpClient.js"; -import type { - RefundConfirmationRequest, - RefundConfirmationReceipt, -} from "../types/payment-card.js"; - export class TransactionAPI { constructor(private readonly httpClient: HttpClient) {} - /** Record a refund already verified by the trusted platform. Requires org.write; does not initiate a merchant refund. */ - async recordRefundConfirmation( - transactionId: string, - receipt: RefundConfirmationRequest, - ): Promise { - return this.httpClient.request({ - method: "POST", - url: `/transactions/${encodeURIComponent(transactionId)}/refund-confirmations`, - body: receipt, - }); - } - /** * Create a new transaction * @param data The transaction data diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2c2e9bc73..d4dfa75d1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,8 +23,7 @@ export type { CardSpendingLimitsSettings, CardAllowanceUsage, CardSpendingLimitsSummary, - RefundConfirmationRequest, - RefundConfirmationReceipt, + PaymentLimitSummary, } from "./types/payment-card.js"; export { VaultAPI } from "./client/VaultAPI.js"; diff --git a/packages/core/src/types/payment-card.ts b/packages/core/src/types/payment-card.ts index 61e112e13..b37e39f2b 100644 --- a/packages/core/src/types/payment-card.ts +++ b/packages/core/src/types/payment-card.ts @@ -22,15 +22,13 @@ export interface CardAllowanceUsage { available: boolean; state: "active" | "future" | "expired" | "inactive" | "unavailable"; currency: "USD"; - allowanceVersion: number; - startInclusive: string; - endExclusive: string; + startInclusive: string | null; + endExclusive: string | null; resetAt: string | null; - effectiveStartInclusive: string | null; /** Unavailable evidence produces null quantities, never an invented zero. */ - confirmedSpend: string | null; + /** Irreversibly consumed payments; card payments become spent on credential release. */ + spent: string | null; pendingCommitted: string | null; - confirmedRefunds: string | null; used: string | null; remaining: string | null; } @@ -40,30 +38,21 @@ export interface CardSpendingLimitsSummary extends CardSpendingLimitsSettings { evaluatedAt: string; /** Indicates effective configuration; each proposed purchase is still checked against all limits. */ configurationActive: boolean; + /** Other matching or conditional payment rules, edited through the Rules API. */ + additionalLimits: PaymentLimitSummary[]; usage: { purchaseLimit?: null; recurringLimit?: CardAllowanceUsage; customLimit?: CardAllowanceUsage; }; } -export interface RefundConfirmationRequest { - paymentTransactionId: string; - externalRefundId: string; - amount: string; - currency: "USD"; - confirmedAt: string; - idempotencyKey: string; - evidence: { - source: "trusted_platform"; - sourceReference: string; - merchantOrderId: string; - }; -} -export interface RefundConfirmationReceipt { - id: string; - paymentTransactionId: string; - state: "applied" | "pending" | "conflicting" | "superseded"; - amount: string; - currency: "USD"; - confirmedAt: string; +export interface PaymentLimitSummary { + ruleId: string; + constraintId: string; + name: string; + /** Conditional rules need the complete transaction context to establish applicability. */ + scope: "match" | "unavailable"; + limit: { kind: "money"; amount: string; currency: "USD" }; + groupBy: Array<"agent" | "trace" | "service" | "paymentCardId">; + usage: CardAllowanceUsage | null; } From 2d272fcab2573e91731e59b21c8c249c874a9798 Mon Sep 17 00:00:00 2001 From: Yash Date: Wed, 21 Oct 2026 00:32:37 +0000 Subject: [PATCH 4/4] ci: run SDK checks for branch-based stacks --- .github/workflows/test.yml | 2 +- scripts/pr-ci-security.test.mjs | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9f637a212..c37710544 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Test on: pull_request: types: [opened, synchronize, reopened, stacked] - branches: [main] + # Branch-based stacks need the same unprivileged checks as PRs to main. push: branches: [main] diff --git a/scripts/pr-ci-security.test.mjs b/scripts/pr-ci-security.test.mjs index 1a62a7720..875a52a5b 100644 --- a/scripts/pr-ci-security.test.mjs +++ b/scripts/pr-ci-security.test.mjs @@ -32,11 +32,19 @@ test("pull request CI uses unprivileged fork-safe triggers", () => { /^ types: \[opened, synchronize, reopened, stacked\]$/m, `${relativePath}: run existing PR events and native stack changes`, ); - assert.match( - pullRequest, - /^ branches: \[main\]$/m, - `${relativePath}: target main directly or through a native stack`, - ); + if (relativePath === ".github/workflows/test.yml") { + assert.doesNotMatch( + pullRequest, + /^ branches(?:-ignore)?:/m, + `${relativePath}: test branch-based stacks regardless of their base`, + ); + } else { + assert.match( + pullRequest, + /^ branches: \[main\]$/m, + `${relativePath}: target main directly or through a native stack`, + ); + } assert.doesNotMatch(workflow, /pull_request_target:/, relativePath); assert.match( workflow,