@peable.to/sdk 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+
2
+ ### HTTP deadlines and existing operation keys
3
+
4
+ SDK0.2.1 accepts `requestTimeoutMs` on `new Peable(...)`. It bounds each token
5
+ mint and gateway HTTP attempt, including reading its body. Omitting it preserves
6
+ the previous timeout behavior. A consumer replacing a20s HTTP client should
7
+ pass `requestTimeoutMs: 20_000`. A401 may still trigger the existing single token
8
+ refresh/retry; timeouts and interrupted responses never retry a payment POST.
9
+ The remote outcome may be unknown: recover explicitly with the same durable key.
10
+
11
+ `paymentIntents.reject`, `refunds.create`, and `transfers.create` now accept an
12
+ optional second `{ idempotencyKey }` argument, forwarding the existing header.
13
+ Refund/transfer `externalRef` remains their durable business identity; the option
14
+ neither replaces it nor changes gateway financial semantics.
@@ -5,6 +5,7 @@
5
5
  // maps every non-2xx response via `errors.ts`.
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.createRestClient = createRestClient;
8
+ const config_1 = require("./config");
8
9
  const errors_1 = require("./errors");
9
10
  const HTTP_UNAUTHORIZED = 401;
10
11
  function buildUrl(baseURL, path, query) {
@@ -32,6 +33,7 @@ async function readJsonBody(response) {
32
33
  }
33
34
  function createRestClient(config, tokenProvider, deps = {}) {
34
35
  const fetchImpl = deps.fetch ?? fetch;
36
+ const timeout = (0, config_1.resolveRequestTimeout)(config.requestTimeoutMs);
35
37
  async function performRequest(method, path, opts, allowRetryOn401) {
36
38
  const token = await tokenProvider.getToken();
37
39
  const headers = { Authorization: `Bearer ${token}` };
@@ -46,6 +48,7 @@ function createRestClient(config, tokenProvider, deps = {}) {
46
48
  response = await fetchImpl(buildUrl(config.baseURL, path, opts.query), {
47
49
  method,
48
50
  headers,
51
+ ...(timeout === undefined ? {} : { signal: AbortSignal.timeout(timeout) }),
49
52
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
50
53
  });
51
54
  }
@@ -6,6 +6,7 @@
6
6
  // "Architecture / auth" section.
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.DEFAULT_OXY_API_URL = exports.DEFAULT_GATEWAY_BASE_URL = void 0;
9
+ exports.resolveRequestTimeout = resolveRequestTimeout;
9
10
  exports.resolveConfig = resolveConfig;
10
11
  /** Default Gateway host (`api.peable.to`) — the SDK never sends a `livemode`
11
12
  * flag; the credential's `environment` rides in the minted service JWT and
@@ -13,6 +14,15 @@ exports.resolveConfig = resolveConfig;
13
14
  exports.DEFAULT_GATEWAY_BASE_URL = 'https://api.peable.to';
14
15
  /** Default oxy-api host used to mint the short-lived service token. */
15
16
  exports.DEFAULT_OXY_API_URL = 'https://api.oxy.so';
17
+ function resolveRequestTimeout(value) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ const timeout = value;
21
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2147483647) {
22
+ throw new Error('Peable: requestTimeoutMs must be a positive32-bit integer');
23
+ }
24
+ return timeout;
25
+ }
16
26
  function stripTrailingSlash(url) {
17
27
  return url.endsWith('/') ? url.slice(0, -1) : url;
18
28
  }
@@ -30,6 +40,7 @@ function resolveConfig(config) {
30
40
  throw new Error('Peable: `secret` is required');
31
41
  }
32
42
  return {
43
+ requestTimeoutMs: resolveRequestTimeout(config.requestTimeoutMs),
33
44
  publicKey: config.publicKey,
34
45
  secret: config.secret,
35
46
  baseURL: stripTrailingSlash(config.baseURL ?? exports.DEFAULT_GATEWAY_BASE_URL),
@@ -43,6 +43,7 @@ function createServiceTokenProvider(cfg, deps = {}) {
43
43
  try {
44
44
  response = await fetchImpl(`${resolved.oxyApiUrl}${SERVICE_TOKEN_PATH}`, {
45
45
  method: 'POST',
46
+ ...(resolved.requestTimeoutMs === undefined ? {} : { signal: AbortSignal.timeout(resolved.requestTimeoutMs) }),
46
47
  headers: { 'Content-Type': 'application/json' },
47
48
  body: JSON.stringify({ apiKey: resolved.publicKey, apiSecret: resolved.secret }),
48
49
  });
@@ -50,7 +51,15 @@ function createServiceTokenProvider(cfg, deps = {}) {
50
51
  catch (cause) {
51
52
  throw new errors_1.PeableApiError(`Failed to reach ${resolved.oxyApiUrl} to mint an Peable service token: ${cause instanceof Error ? cause.message : String(cause)}`);
52
53
  }
53
- const body = await readJsonBody(response);
54
+ let body;
55
+ try {
56
+ body = await readJsonBody(response);
57
+ }
58
+ catch {
59
+ throw new errors_1.PeableApiError('The Oxy service-token response body could not be read', {
60
+ statusCode: response.status, code: 'invalid_response',
61
+ });
62
+ }
54
63
  if (!response.ok) {
55
64
  throw (0, errors_1.errorFromResponse)(response.status, body);
56
65
  }
package/dist/cjs/index.js CHANGED
@@ -40,7 +40,7 @@ class Peable {
40
40
  constructor(config) {
41
41
  const resolved = (0, config_1.resolveConfig)(config);
42
42
  const tokenProvider = (0, serviceToken_1.createServiceTokenProvider)(config);
43
- const client = (0, client_1.createRestClient)({ baseURL: resolved.baseURL }, tokenProvider);
43
+ const client = (0, client_1.createRestClient)({ baseURL: resolved.baseURL, requestTimeoutMs: resolved.requestTimeoutMs }, tokenProvider);
44
44
  this.billing = new billing_1.BillingResource(client);
45
45
  this.paymentIntents = new paymentIntents_1.PaymentIntentsResource(client);
46
46
  this.paymentLinks = new paymentLinks_1.PaymentLinksResource(client);
@@ -32,8 +32,8 @@ class PaymentIntentsResource {
32
32
  * and nothing was decided. Neither is a failure of the request — both are
33
33
  * facts about a payment that is still live.
34
34
  */
35
- reject(id) {
36
- return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`);
35
+ reject(id, options = {}) {
36
+ return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`, { idempotencyKey: options.idempotencyKey });
37
37
  }
38
38
  /**
39
39
  * What the PAYER's client has to do next — the resume operation.
@@ -18,8 +18,8 @@ class RefundsResource {
18
18
  * caller does not need a second read whose answer can move on before it
19
19
  * arrives.
20
20
  */
21
- create(params) {
22
- return this.client.request('POST', '/v1/refunds', { body: params });
21
+ create(params, options = {}) {
22
+ return this.client.request('POST', '/v1/refunds', { body: params, idempotencyKey: options.idempotencyKey });
23
23
  }
24
24
  /**
25
25
  * What this payment actually came to — gross, fee, net.
@@ -17,8 +17,8 @@ class TransfersResource {
17
17
  * would come from the platform's general balance, which is other merchants'
18
18
  * money in flight.
19
19
  */
20
- create(params) {
21
- return this.client.request('POST', '/v1/transfers', { body: params });
20
+ create(params, options = {}) {
21
+ return this.client.request('POST', '/v1/transfers', { body: params, idempotencyKey: options.idempotencyKey });
22
22
  }
23
23
  /**
24
24
  * Take some or all of a settlement back.
@@ -2,6 +2,7 @@
2
2
  // Prefixes `baseURL`, attaches the bearer service token, sets
3
3
  // `Idempotency-Key` when given, serializes query params, parses JSON, and
4
4
  // maps every non-2xx response via `errors.ts`.
5
+ import { resolveRequestTimeout } from './config.js';
5
6
  import { errorFromResponse, PeableApiError } from './errors.js';
6
7
  const HTTP_UNAUTHORIZED = 401;
7
8
  function buildUrl(baseURL, path, query) {
@@ -29,6 +30,7 @@ async function readJsonBody(response) {
29
30
  }
30
31
  export function createRestClient(config, tokenProvider, deps = {}) {
31
32
  const fetchImpl = deps.fetch ?? fetch;
33
+ const timeout = resolveRequestTimeout(config.requestTimeoutMs);
32
34
  async function performRequest(method, path, opts, allowRetryOn401) {
33
35
  const token = await tokenProvider.getToken();
34
36
  const headers = { Authorization: `Bearer ${token}` };
@@ -43,6 +45,7 @@ export function createRestClient(config, tokenProvider, deps = {}) {
43
45
  response = await fetchImpl(buildUrl(config.baseURL, path, opts.query), {
44
46
  method,
45
47
  headers,
48
+ ...(timeout === undefined ? {} : { signal: AbortSignal.timeout(timeout) }),
46
49
  body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
47
50
  });
48
51
  }
@@ -9,6 +9,15 @@
9
9
  export const DEFAULT_GATEWAY_BASE_URL = 'https://api.peable.to';
10
10
  /** Default oxy-api host used to mint the short-lived service token. */
11
11
  export const DEFAULT_OXY_API_URL = 'https://api.oxy.so';
12
+ export function resolveRequestTimeout(value) {
13
+ if (value === undefined)
14
+ return undefined;
15
+ const timeout = value;
16
+ if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 2147483647) {
17
+ throw new Error('Peable: requestTimeoutMs must be a positive32-bit integer');
18
+ }
19
+ return timeout;
20
+ }
12
21
  function stripTrailingSlash(url) {
13
22
  return url.endsWith('/') ? url.slice(0, -1) : url;
14
23
  }
@@ -26,6 +35,7 @@ export function resolveConfig(config) {
26
35
  throw new Error('Peable: `secret` is required');
27
36
  }
28
37
  return {
38
+ requestTimeoutMs: resolveRequestTimeout(config.requestTimeoutMs),
29
39
  publicKey: config.publicKey,
30
40
  secret: config.secret,
31
41
  baseURL: stripTrailingSlash(config.baseURL ?? DEFAULT_GATEWAY_BASE_URL),
@@ -40,6 +40,7 @@ export function createServiceTokenProvider(cfg, deps = {}) {
40
40
  try {
41
41
  response = await fetchImpl(`${resolved.oxyApiUrl}${SERVICE_TOKEN_PATH}`, {
42
42
  method: 'POST',
43
+ ...(resolved.requestTimeoutMs === undefined ? {} : { signal: AbortSignal.timeout(resolved.requestTimeoutMs) }),
43
44
  headers: { 'Content-Type': 'application/json' },
44
45
  body: JSON.stringify({ apiKey: resolved.publicKey, apiSecret: resolved.secret }),
45
46
  });
@@ -47,7 +48,15 @@ export function createServiceTokenProvider(cfg, deps = {}) {
47
48
  catch (cause) {
48
49
  throw new PeableApiError(`Failed to reach ${resolved.oxyApiUrl} to mint an Peable service token: ${cause instanceof Error ? cause.message : String(cause)}`);
49
50
  }
50
- const body = await readJsonBody(response);
51
+ let body;
52
+ try {
53
+ body = await readJsonBody(response);
54
+ }
55
+ catch {
56
+ throw new PeableApiError('The Oxy service-token response body could not be read', {
57
+ statusCode: response.status, code: 'invalid_response',
58
+ });
59
+ }
51
60
  if (!response.ok) {
52
61
  throw errorFromResponse(response.status, body);
53
62
  }
package/dist/esm/index.js CHANGED
@@ -37,7 +37,7 @@ export class Peable {
37
37
  constructor(config) {
38
38
  const resolved = resolveConfig(config);
39
39
  const tokenProvider = createServiceTokenProvider(config);
40
- const client = createRestClient({ baseURL: resolved.baseURL }, tokenProvider);
40
+ const client = createRestClient({ baseURL: resolved.baseURL, requestTimeoutMs: resolved.requestTimeoutMs }, tokenProvider);
41
41
  this.billing = new BillingResource(client);
42
42
  this.paymentIntents = new PaymentIntentsResource(client);
43
43
  this.paymentLinks = new PaymentLinksResource(client);
@@ -29,8 +29,8 @@ export class PaymentIntentsResource {
29
29
  * and nothing was decided. Neither is a failure of the request — both are
30
30
  * facts about a payment that is still live.
31
31
  */
32
- reject(id) {
33
- return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`);
32
+ reject(id, options = {}) {
33
+ return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`, { idempotencyKey: options.idempotencyKey });
34
34
  }
35
35
  /**
36
36
  * What the PAYER's client has to do next — the resume operation.
@@ -15,8 +15,8 @@ export class RefundsResource {
15
15
  * caller does not need a second read whose answer can move on before it
16
16
  * arrives.
17
17
  */
18
- create(params) {
19
- return this.client.request('POST', '/v1/refunds', { body: params });
18
+ create(params, options = {}) {
19
+ return this.client.request('POST', '/v1/refunds', { body: params, idempotencyKey: options.idempotencyKey });
20
20
  }
21
21
  /**
22
22
  * What this payment actually came to — gross, fee, net.
@@ -14,8 +14,8 @@ export class TransfersResource {
14
14
  * would come from the platform's general balance, which is other merchants'
15
15
  * money in flight.
16
16
  */
17
- create(params) {
18
- return this.client.request('POST', '/v1/transfers', { body: params });
17
+ create(params, options = {}) {
18
+ return this.client.request('POST', '/v1/transfers', { body: params, idempotencyKey: options.idempotencyKey });
19
19
  }
20
20
  /**
21
21
  * Take some or all of a settlement back.
@@ -11,6 +11,7 @@ export interface RestClient {
11
11
  export interface RestClientConfig {
12
12
  /** Gateway base URL, already stripped of a trailing slash. */
13
13
  baseURL: string;
14
+ requestTimeoutMs?: number;
14
15
  }
15
16
  export declare function createRestClient(config: RestClientConfig, tokenProvider: ServiceTokenProvider, deps?: {
16
17
  fetch?: typeof fetch;
@@ -13,6 +13,8 @@ export interface PeableConfig {
13
13
  baseURL?: string;
14
14
  /** oxy-api host used to mint the service token. Default `https://api.oxy.so`. */
15
15
  oxyApiUrl?: string;
16
+ /** Optional deadline per HTTP attempt, including body consumption. */
17
+ requestTimeoutMs?: number;
16
18
  }
17
19
  /** `PeableConfig` with every optional field defaulted and normalized. */
18
20
  export interface ResolvedPeableConfig {
@@ -20,7 +22,9 @@ export interface ResolvedPeableConfig {
20
22
  secret: string;
21
23
  baseURL: string;
22
24
  oxyApiUrl: string;
25
+ requestTimeoutMs?: number;
23
26
  }
27
+ export declare function resolveRequestTimeout(value: number | undefined): number | undefined;
24
28
  /**
25
29
  * Validate and normalize an `PeableConfig`. Throws a plain `Error` for a
26
30
  * missing `publicKey`/`secret` — a programmer error caught at construction
@@ -52,7 +52,9 @@ export declare class PaymentIntentsResource {
52
52
  * and nothing was decided. Neither is a failure of the request — both are
53
53
  * facts about a payment that is still live.
54
54
  */
55
- reject(id: string): Promise<PaymentIntent>;
55
+ reject(id: string, options?: {
56
+ idempotencyKey?: string;
57
+ }): Promise<PaymentIntent>;
56
58
  /**
57
59
  * What the PAYER's client has to do next — the resume operation.
58
60
  *
@@ -46,7 +46,9 @@ export declare class RefundsResource {
46
46
  * caller does not need a second read whose answer can move on before it
47
47
  * arrives.
48
48
  */
49
- create(params: CreateRefundParams): Promise<Refund>;
49
+ create(params: CreateRefundParams, options?: {
50
+ idempotencyKey?: string;
51
+ }): Promise<Refund>;
50
52
  /**
51
53
  * What this payment actually came to — gross, fee, net.
52
54
  *
@@ -57,7 +57,9 @@ export declare class TransfersResource {
57
57
  * would come from the platform's general balance, which is other merchants'
58
58
  * money in flight.
59
59
  */
60
- create(params: CreateTransferParams): Promise<Transfer>;
60
+ create(params: CreateTransferParams, options?: {
61
+ idempotencyKey?: string;
62
+ }): Promise<Transfer>;
61
63
  /**
62
64
  * Take some or all of a settlement back.
63
65
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peable.to/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Peable SDK — a Stripe-ergonomics TypeScript client over the Peable Gateway REST + realtime contract. Server entry (`@peable.to/sdk`) mints Oxy service tokens from an ApplicationCredential and exposes paymentIntents/paymentLinks/checkout.sessions/webhooks; browser entry (`@peable.to/sdk/checkout`) is the anonymous payer-side client core.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",