From 6becbe5b788088db5334548a1b488208447181df Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:03:49 +0000 Subject: [PATCH 1/2] feat: Support Link recurring spend requests on vault Link cards Stainless-Generated-From: 74e9ad405f948c67e6203d5d776cd9157991803f --- api.md | 2 + src/core/pagination.ts | 8 +- src/resources/vaults/index.ts | 2 + src/resources/vaults/items.ts | 296 +++++++++++++++++++++-- src/resources/vaults/vaults.ts | 4 + tests/api-resources/vaults/items.test.ts | 1 + tests/pagination.test.ts | 26 +- 7 files changed, 319 insertions(+), 20 deletions(-) diff --git a/api.md b/api.md index e6269a01..3ed75618 100644 --- a/api.md +++ b/api.md @@ -545,6 +545,8 @@ Types: - KernelCredentialVaultItemState - KernelWalletState - KernelWalletVaultItemSpec +- LinkShippingAddress +- LinkUserInfo - ManagedAuthCredentialVaultField - ManagedAuthCredentialVaultItemSpec - ManagedAuthCredentialVaultItemSpecInput diff --git a/src/core/pagination.ts b/src/core/pagination.ts index 85e1d84d..cebdc295 100644 --- a/src/core/pagination.ts +++ b/src/core/pagination.ts @@ -200,7 +200,7 @@ export class OffsetPagination extends AbstractPage { return false; } - return super.hasNextPage(); + return this.nextPageRequestOptions() != null; } nextPageRequestOptions(): PageRequestOptions | null { @@ -221,6 +221,12 @@ export class OffsetPagination extends AbstractPage { if (offset === 0) { return null; } + // Cursors can move in either direction. Stop only when the server repeats + // the cursor and would re-request the same page. + const requestOffset = (this.options.query as OffsetPaginationParams | undefined)?.offset; + if (typeof requestOffset === 'number' && offset === requestOffset) { + return null; + } return { ...this.options, diff --git a/src/resources/vaults/index.ts b/src/resources/vaults/index.ts index b0de68f1..3b3ab789 100644 --- a/src/resources/vaults/index.ts +++ b/src/resources/vaults/index.ts @@ -35,6 +35,8 @@ export { type KernelCredentialVaultItemState, type KernelWalletState, type KernelWalletVaultItemSpec, + type LinkShippingAddress, + type LinkUserInfo, type ManagedAuthCredentialVaultField, type ManagedAuthCredentialVaultItemSpec, type ManagedAuthCredentialVaultItemSpecInput, diff --git a/src/resources/vaults/items.ts b/src/resources/vaults/items.ts index 4f0d67af..8cf6107e 100644 --- a/src/resources/vaults/items.ts +++ b/src/resources/vaults/items.ts @@ -368,7 +368,8 @@ export namespace CardVaultItemSpec { */ export interface LinkCardVaultItemSpec { /** - * Integer amount in minor currency units, at most 50000. + * Integer amount in minor currency units, at most 50000. With recurring, the + * spending limit per period, not a lifetime total. */ amount: number; @@ -398,6 +399,19 @@ export namespace CardVaultItemSpec { metadata?: { [key: string]: string }; + /** + * Recurring terms for a subscription or other repeated purchase. The amount + * becomes the spending limit for each period of interval_count intervals, for + * example every 2 weeks. The merchant schedules charges. The card stays ready + * after each charge until it expires or is deleted; do not create a new card each + * period. Deleting the card, its wallet or its vault first cancels the credential + * with Link, so later charges decline; if Link cannot be reached, deletion returns + * 409 provider_revocation_pending and should be retried. Deletion does not cancel + * the subscription with the merchant. Recurring terms cannot be changed once the + * card is authorized. + */ + recurring?: LinkCardVaultItemSpec.Recurring; + totals?: Array; } @@ -438,6 +452,26 @@ export namespace CardVaultItemSpec { } } + /** + * Recurring terms for a subscription or other repeated purchase. The amount + * becomes the spending limit for each period of interval_count intervals, for + * example every 2 weeks. The merchant schedules charges. The card stays ready + * after each charge until it expires or is deleted; do not create a new card each + * period. Deleting the card, its wallet or its vault first cancels the credential + * with Link, so later charges decline; if Link cannot be reached, deletion returns + * 409 provider_revocation_pending and should be retried. Deletion does not cancel + * the subscription with the merchant. Recurring terms cannot be changed once the + * card is authorized. + */ + export interface Recurring { + interval: 'day' | 'week' | 'month' | 'year'; + + /** + * Number of intervals per period. Link defaults it to 1. + */ + interval_count?: number; + } + export interface Total { /** * Total amount in minor currency units. @@ -713,7 +747,7 @@ export namespace CredentialAccountVaultItem { export interface AvailableExpansion { description: string; - type: 'payment_methods'; + type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses'; } /** @@ -1088,7 +1122,7 @@ export namespace CredentialVaultItem { export interface AvailableExpansion { description: string; - type: 'payment_methods'; + type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses'; } /** @@ -1291,7 +1325,8 @@ export interface FillVaultItemOperationRequest { page_url?: string; /** - * Total operation deadline in milliseconds, not a per-field timeout. + * @deprecated Ignored. Fill sets no operation deadline; typing pace adapts to the + * length of the values instead. Accepted for compatibility. */ timeout_ms?: number; } @@ -1327,8 +1362,11 @@ export interface KernelCardState { provider: 'kernel'; /** - * pending_authorization on a Visa purchase waits for the cardholder to approve it - * through the spend_approval action; an unused link expires after 30 minutes. + * pending_authorization waits for cardholder approval on Visa purchases or fresh + * security-code collection on alias-mode purchases through the spend_approval + * action; an unused link expires after 30 minutes. Alias-mode cards become ready + * after collection and expire at the temporary security-code alias deadline, by + * default five minutes later. An expired alias purchase requires a new card. * recovery_required means verification, approving the purchase or issuing the * one-time code has an unresolved outcome. A server-classified initial attestation * failure before intent dispatch may advertise restart_verification to prepare a @@ -1386,12 +1424,13 @@ export namespace KernelCardState { * spend_approval action: the cardholder approves it with a Visa passkey, and * Kernel registers a Visa intent for one transaction up to the amount before * issuing the code. For supported cards whose network cannot be enrolled for - * agentic tokens, authorize prepares checkout details without issuing a one-time - * code. The card can be filled into one checkout before its expires_at, one hour - * after authorization. Fill types those details on merchant_url's origin. The - * amount, merchant_url and merchant_country are recorded but do not restrict - * payment requests sent by the browser. Card updates are not supported; delete and - * create a new item instead. + * agentic tokens, authorize returns pending_authorization with a spend_approval + * action to collect a fresh security code in a hosted field. Once collected, the + * card is ready and can be filled before its expires_at, by default five minutes + * after collection. An expired card requires a new purchase. Fill types those + * details on merchant_url's origin. The amount, merchant_url and merchant_country + * are recorded but do not restrict payment requests sent by the browser. Card + * updates are not supported; delete and create a new item instead. */ export interface KernelCardVaultItemSpec { /** @@ -1527,6 +1566,196 @@ export interface KernelWalletVaultItemSpec { provider: 'kernel'; } +/** + * One entry of Link's `GET /shipping_addresses` response, passed through as Link + * reports it. Kernel never stores it. + */ +export interface LinkShippingAddress { + /** + * Link shipping address ID. + */ + id: string; + + is_default: boolean; + + nickname: string | null; + + /** + * Link's nullable postal address fields. Omitted from the shipping address when + * Link reports null. + */ + address?: LinkShippingAddress.Address; +} + +export namespace LinkShippingAddress { + /** + * Link's nullable postal address fields. Omitted from the shipping address when + * Link reports null. + */ + export interface Address { + /** + * State, province or region. + */ + administrative_area: string | null; + + country_code: string | null; + + dependent_locality: string | null; + + line_1: string | null; + + line_2: string | null; + + /** + * City or town. + */ + locality: string | null; + + name: string | null; + + postal_code: string | null; + + sorting_code: string | null; + } +} + +/** + * Link's `GET /userinfo` response for the connected wallet, passed through as Link + * reports it when the `link_user_info` expansion is requested. Kernel never stores + * it. Null fields are null at Link; omitted fields were not reported. Amounts are + * in the minor unit of `default_spend_request_currency`. + */ +export interface LinkUserInfo { + /** + * Stable Link user ID. + */ + id: string | null; + + email: string | null; + + first_name: string | null; + + last_name: string | null; + + name: string | null; + + phone: string | null; + + /** + * The Link user's postal address. Omitted when Link reports none. + */ + address?: LinkUserInfo.Address; + + /** + * Link's agent wallet spend limits, per transaction and over rolling periods. + */ + agent_wallet_spend_limits?: LinkUserInfo.AgentWalletSpendLimits; + + /** + * Verification Link requires from the user before the agent wallet can spend. + */ + agent_wallet_step_up?: LinkUserInfo.AgentWalletStepUp; + + /** + * Lowercase ISO currency Link applies to a spend request created without one. + */ + default_spend_request_currency?: string; + + /** + * Whether the user can fund purchases from a Link balance. + */ + eligible_for_balance?: boolean; +} + +export namespace LinkUserInfo { + /** + * The Link user's postal address. Omitted when Link reports none. + */ + export interface Address { + city: string | null; + + country: string | null; + + line1: string | null; + + line2: string | null; + + postal_code: string | null; + + state: string | null; + } + + /** + * Link's agent wallet spend limits, per transaction and over rolling periods. + */ + export interface AgentWalletSpendLimits { + daily: AgentWalletSpendLimits.Daily; + + per_transaction: AgentWalletSpendLimits.PerTransaction; + + thirty_day: AgentWalletSpendLimits.ThirtyDay; + } + + export namespace AgentWalletSpendLimits { + export interface Daily { + /** + * Null means unlimited. + */ + limit: number | null; + + /** + * Amount still available in the rolling period; null means unlimited. + */ + remaining: number | null; + + /** + * Amount already spent in the rolling period. + */ + used: number; + } + + export interface PerTransaction { + /** + * Null means unlimited. + */ + limit: number | null; + } + + export interface ThirtyDay { + /** + * Null means unlimited. + */ + limit: number | null; + + /** + * Amount still available in the rolling period; null means unlimited. + */ + remaining: number | null; + + /** + * Amount already spent in the rolling period. + */ + used: number; + } + } + + /** + * Verification Link requires from the user before the agent wallet can spend. + */ + export interface AgentWalletStepUp { + /** + * URL where the user completes verification; null when no action is available. + */ + action_url: string | null; + + /** + * Link status such as not_required, ssn_verification, identity_verification, + * contact_support or complete. + */ + status: string; + } +} + export interface ManagedAuthCredentialVaultField { /** * Text, email, and password have form inputs; totp and passkey do not and are @@ -2255,7 +2484,7 @@ export namespace VaultItem { export interface AvailableExpansion { description: string; - type: 'payment_methods'; + type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses'; } /** @@ -2284,6 +2513,20 @@ export namespace VaultItem { * Live, non-persisted data requested through the item GET expand parameter. */ export interface Expanded { + /** + * Link's saved shipping addresses, present when the `link_shipping_addresses` + * expansion is requested. + */ + link_shipping_addresses?: Array; + + /** + * Link's `GET /userinfo` response for the connected wallet, passed through as Link + * reports it when the `link_user_info` expansion is requested. Kernel never stores + * it. Null fields are null at Link; omitted fields were not reported. Amounts are + * in the minor unit of `default_spend_request_currency`. + */ + link_user_info?: ItemsAPI.LinkUserInfo; + payment_methods?: Array; } } @@ -2335,7 +2578,7 @@ export namespace VaultItem { export interface AvailableExpansion { description: string; - type: 'payment_methods'; + type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses'; } /** @@ -2506,7 +2749,7 @@ export namespace VaultItemOperationResponse { export interface AvailableExpansion { description: string; - type: 'payment_methods'; + type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses'; } /** @@ -2535,6 +2778,20 @@ export namespace VaultItemOperationResponse { * Live, non-persisted data requested through the item GET expand parameter. */ export interface Expanded { + /** + * Link's saved shipping addresses, present when the `link_shipping_addresses` + * expansion is requested. + */ + link_shipping_addresses?: Array; + + /** + * Link's `GET /userinfo` response for the connected wallet, passed through as Link + * reports it when the `link_user_info` expansion is requested. Kernel never stores + * it. Null fields are null at Link; omitted fields were not reported. Amounts are + * in the minor unit of `default_spend_request_currency`. + */ + link_user_info?: ItemsAPI.LinkUserInfo; + payment_methods?: Array; } } @@ -2586,7 +2843,7 @@ export namespace VaultItemOperationResponse { export interface AvailableExpansion { description: string; - type: 'payment_methods'; + type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses'; } /** @@ -2912,7 +3169,7 @@ export interface ItemRetrieveParams { * Query param: Live fields advertised by `available_expansions` to include in * `expanded`. */ - expand?: Array<'payment_methods'>; + expand?: Array<'payment_methods' | 'link_user_info' | 'link_shipping_addresses'>; /** * Query param: Hold for up to this many seconds while the item is pending @@ -3172,7 +3429,8 @@ export declare namespace ItemPerformOperationParams { page_url?: string; /** - * Body param: Total operation deadline in milliseconds, not a per-field timeout. + * @deprecated Body param: Ignored. Fill sets no operation deadline; typing pace + * adapts to the length of the values instead. Accepted for compatibility. */ timeout_ms?: number; } @@ -3624,6 +3882,8 @@ export declare namespace Items { type KernelCredentialVaultItemState as KernelCredentialVaultItemState, type KernelWalletState as KernelWalletState, type KernelWalletVaultItemSpec as KernelWalletVaultItemSpec, + type LinkShippingAddress as LinkShippingAddress, + type LinkUserInfo as LinkUserInfo, type ManagedAuthCredentialVaultField as ManagedAuthCredentialVaultField, type ManagedAuthCredentialVaultItemSpec as ManagedAuthCredentialVaultItemSpec, type ManagedAuthCredentialVaultItemSpecInput as ManagedAuthCredentialVaultItemSpecInput, diff --git a/src/resources/vaults/vaults.ts b/src/resources/vaults/vaults.ts index a2cc8814..f51a3e63 100644 --- a/src/resources/vaults/vaults.ts +++ b/src/resources/vaults/vaults.ts @@ -45,6 +45,8 @@ import { KernelCredentialVaultItemState, KernelWalletState, KernelWalletVaultItemSpec, + LinkShippingAddress, + LinkUserInfo, ManagedAuthCredentialVaultField, ManagedAuthCredentialVaultItemSpec, ManagedAuthCredentialVaultItemSpecInput, @@ -299,6 +301,8 @@ export declare namespace Vaults { type KernelCredentialVaultItemState as KernelCredentialVaultItemState, type KernelWalletState as KernelWalletState, type KernelWalletVaultItemSpec as KernelWalletVaultItemSpec, + type LinkShippingAddress as LinkShippingAddress, + type LinkUserInfo as LinkUserInfo, type ManagedAuthCredentialVaultField as ManagedAuthCredentialVaultField, type ManagedAuthCredentialVaultItemSpec as ManagedAuthCredentialVaultItemSpec, type ManagedAuthCredentialVaultItemSpecInput as ManagedAuthCredentialVaultItemSpecInput, diff --git a/tests/api-resources/vaults/items.test.ts b/tests/api-resources/vaults/items.test.ts index 8f128c86..f2381ea9 100644 --- a/tests/api-resources/vaults/items.test.ts +++ b/tests/api-resources/vaults/items.test.ts @@ -88,6 +88,7 @@ describe('resource items', () => { }, ], metadata: { foo: 'string' }, + recurring: { interval: 'day', interval_count: 1 }, totals: [ { amount: 0, diff --git a/tests/pagination.test.ts b/tests/pagination.test.ts index 3e5fadee..33917873 100644 --- a/tests/pagination.test.ts +++ b/tests/pagination.test.ts @@ -46,8 +46,32 @@ describe('OffsetPagination', () => { expect(page.hasNextPage()).toBe(false); }); - test('stops on an empty page', () => { + test('continues past an empty page while X-Has-More is true', () => { + // A server-side filter can leave a page empty with more records ahead. const page = pageWith({ 'x-next-offset': '300', 'x-has-more': 'true' }, [], 200); + expect(page.hasNextPage()).toBe(true); + expect(page.nextPageRequestOptions()?.query).toEqual({ offset: 300 }); + }); + + test('stops on an empty page when X-Has-More is false', () => { + const page = pageWith({ 'x-next-offset': '0', 'x-has-more': 'false' }, [], 200); + expect(page.hasNextPage()).toBe(false); + }); + + test('stops when X-Next-Offset repeats the requested offset', () => { + const page = pageWith({ 'x-next-offset': '200', 'x-has-more': 'true' }, [], 200); + expect(page.nextPageRequestOptions()).toBeNull(); + expect(page.hasNextPage()).toBe(false); + }); + + test('continues past an empty page with a descending cursor', () => { + const page = pageWith({ 'x-next-offset': '200', 'x-has-more': 'true' }, [], 300); + expect(page.hasNextPage()).toBe(true); + expect(page.nextPageRequestOptions()?.query).toEqual({ offset: 200 }); + }); + + test('stops on an empty first page without pagination headers', () => { + const page = pageWith({}, []); expect(page.hasNextPage()).toBe(false); }); From d4366c9520faa200c3030560b14ca46338e51006 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:08:53 +0000 Subject: [PATCH 2/2] release: 0.130.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ package.json | 2 +- src/version.ts | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3eebe245..90ea33ab 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.129.0" + ".": "0.130.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 02064e31..74e9fd9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.130.0](https://github.com/kernel/kernel-node-sdk/compare/v0.129.0...v0.130.0) (2026-10-24) + + +### Features + +* Support Link recurring spend requests on vault Link cards ([6becbe5](https://github.com/kernel/kernel-node-sdk/commit/6becbe5b788088db5334548a1b488208447181df)) + ## [0.129.0](https://github.com/kernel/kernel-node-sdk/compare/v0.128.0...v0.129.0) (2026-10-21) diff --git a/package.json b/package.json index bbbe93b8..18ae683a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@onkernel/sdk", - "version": "0.129.0", + "version": "0.130.0", "description": "The official TypeScript library for the Kernel API", "author": "Kernel <>", "types": "dist/index.d.ts", diff --git a/src/version.ts b/src/version.ts index 54ba18a0..bfde34bc 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.129.0'; // x-release-please-version +export const VERSION = '0.130.0'; // x-release-please-version