@peable.to/sdk 0.1.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/dist/cjs/browser/mount.js +308 -0
- package/dist/cjs/browser/payerClient.js +135 -0
- package/dist/cjs/checkout.js +17 -0
- package/dist/cjs/core/client.js +77 -0
- package/dist/cjs/core/config.js +38 -0
- package/dist/cjs/core/errors.js +112 -0
- package/dist/cjs/core/serviceToken.js +81 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/resources/checkoutSessions.js +26 -0
- package/dist/cjs/resources/paymentIntents.js +30 -0
- package/dist/cjs/resources/paymentLinks.js +23 -0
- package/dist/cjs/resources/webhooks.js +65 -0
- package/dist/esm/browser/mount.js +298 -0
- package/dist/esm/browser/payerClient.js +132 -0
- package/dist/esm/checkout.js +12 -0
- package/dist/esm/core/client.js +74 -0
- package/dist/esm/core/config.js +34 -0
- package/dist/esm/core/errors.js +102 -0
- package/dist/esm/core/serviceToken.js +78 -0
- package/dist/esm/index.js +28 -0
- package/dist/esm/resources/checkoutSessions.js +21 -0
- package/dist/esm/resources/paymentIntents.js +26 -0
- package/dist/esm/resources/paymentLinks.js +19 -0
- package/dist/esm/resources/webhooks.js +61 -0
- package/dist/types/browser/mount.d.ts +80 -0
- package/dist/types/browser/payerClient.d.ts +23 -0
- package/dist/types/checkout.d.ts +2 -0
- package/dist/types/core/client.d.ts +17 -0
- package/dist/types/core/config.d.ts +30 -0
- package/dist/types/core/errors.d.ts +54 -0
- package/dist/types/core/serviceToken.d.ts +11 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/resources/checkoutSessions.d.ts +15 -0
- package/dist/types/resources/paymentIntents.d.ts +29 -0
- package/dist/types/resources/paymentLinks.d.ts +29 -0
- package/dist/types/resources/webhooks.d.ts +18 -0
- package/package.json +83 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Mints a short-lived Oxy service token from an `ApplicationCredential`
|
|
2
|
+
// (`{publicKey, secret}`) by presenting it to oxy-api's `POST
|
|
3
|
+
// /auth/service-token` — the exact mechanism `oxyClient.serviceAuth()`
|
|
4
|
+
// validates at the Gateway. Caches the token until near expiry and re-mints
|
|
5
|
+
// reactively when `RestClient` sees a 401 (see `client.ts`).
|
|
6
|
+
import { resolveConfig } from './config.js';
|
|
7
|
+
import { errorFromResponse, PeableApiError } from './errors.js';
|
|
8
|
+
/** Path (relative to `oxyApiUrl`) of oxy-api's service-token mint endpoint. */
|
|
9
|
+
const SERVICE_TOKEN_PATH = '/auth/service-token';
|
|
10
|
+
/** Refresh proactively this long before the minted token's actual expiry. */
|
|
11
|
+
const REFRESH_MARGIN_MS = 60000;
|
|
12
|
+
function isServiceTokenMintEnvelope(body) {
|
|
13
|
+
if (typeof body !== 'object' || body === null || !('data' in body))
|
|
14
|
+
return false;
|
|
15
|
+
const { data } = body;
|
|
16
|
+
if (typeof data !== 'object' || data === null)
|
|
17
|
+
return false;
|
|
18
|
+
const candidate = data;
|
|
19
|
+
return typeof candidate.token === 'string' && typeof candidate.expiresIn === 'number';
|
|
20
|
+
}
|
|
21
|
+
async function readJsonBody(response) {
|
|
22
|
+
const text = await response.text();
|
|
23
|
+
if (text.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function createServiceTokenProvider(cfg, deps = {}) {
|
|
33
|
+
const resolved = resolveConfig(cfg);
|
|
34
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
35
|
+
let cachedToken = null;
|
|
36
|
+
let cachedExpiresAt = 0;
|
|
37
|
+
let inflight = null;
|
|
38
|
+
async function mint() {
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await fetchImpl(`${resolved.oxyApiUrl}${SERVICE_TOKEN_PATH}`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ apiKey: resolved.publicKey, apiSecret: resolved.secret }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (cause) {
|
|
48
|
+
throw new PeableApiError(`Failed to reach ${resolved.oxyApiUrl} to mint an Peable service token: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
49
|
+
}
|
|
50
|
+
const body = await readJsonBody(response);
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw errorFromResponse(response.status, body);
|
|
53
|
+
}
|
|
54
|
+
if (!isServiceTokenMintEnvelope(body)) {
|
|
55
|
+
throw new PeableApiError('Peable: the service-token mint response was missing `data.token`/`data.expiresIn`');
|
|
56
|
+
}
|
|
57
|
+
cachedToken = body.data.token;
|
|
58
|
+
cachedExpiresAt = Date.now() + body.data.expiresIn * 1000;
|
|
59
|
+
return cachedToken;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async getToken() {
|
|
63
|
+
if (cachedToken !== null && Date.now() < cachedExpiresAt - REFRESH_MARGIN_MS) {
|
|
64
|
+
return cachedToken;
|
|
65
|
+
}
|
|
66
|
+
if (inflight === null) {
|
|
67
|
+
inflight = mint().finally(() => {
|
|
68
|
+
inflight = null;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return inflight;
|
|
72
|
+
},
|
|
73
|
+
invalidate() {
|
|
74
|
+
cachedToken = null;
|
|
75
|
+
cachedExpiresAt = 0;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Server entry (`@peable.to/sdk`) — the merchant-authed SDK. Configured with a
|
|
2
|
+
// confidential `ApplicationCredential` (`{publicKey, secret}`); mints and
|
|
3
|
+
// caches an Oxy service token, and exposes Stripe-ergonomics resource
|
|
4
|
+
// namespaces over the Gateway REST contract.
|
|
5
|
+
//
|
|
6
|
+
// Deliberately never imports `socket.io-client` or `./browser/*` — the
|
|
7
|
+
// server bundle stays lean and holds no browser-only code.
|
|
8
|
+
import { resolveConfig } from './core/config.js';
|
|
9
|
+
import { createServiceTokenProvider } from './core/serviceToken.js';
|
|
10
|
+
import { createRestClient } from './core/client.js';
|
|
11
|
+
import { PaymentIntentsResource } from './resources/paymentIntents.js';
|
|
12
|
+
import { PaymentLinksResource } from './resources/paymentLinks.js';
|
|
13
|
+
import { CheckoutResource } from './resources/checkoutSessions.js';
|
|
14
|
+
import { WebhooksResource } from './resources/webhooks.js';
|
|
15
|
+
export class Peable {
|
|
16
|
+
constructor(config) {
|
|
17
|
+
const resolved = resolveConfig(config);
|
|
18
|
+
const tokenProvider = createServiceTokenProvider(config);
|
|
19
|
+
const client = createRestClient({ baseURL: resolved.baseURL }, tokenProvider);
|
|
20
|
+
this.paymentIntents = new PaymentIntentsResource(client);
|
|
21
|
+
this.paymentLinks = new PaymentLinksResource(client);
|
|
22
|
+
this.checkout = new CheckoutResource(client);
|
|
23
|
+
this.webhooks = new WebhooksResource();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export { DEFAULT_GATEWAY_BASE_URL, DEFAULT_OXY_API_URL } from './core/config.js';
|
|
27
|
+
export { PeableError, PeableAuthenticationError, PeableInvalidRequestError, PeablePermissionError, PeableApiError, PeableSignatureVerificationError, } from './core/errors.js';
|
|
28
|
+
export { WEBHOOK_SIGNATURE_HEADER } from './resources/webhooks.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class CheckoutSessionsResource {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
}
|
|
5
|
+
create(params) {
|
|
6
|
+
return this.client.request('POST', '/v1/checkout_sessions', {
|
|
7
|
+
body: params,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
retrieve(id) {
|
|
11
|
+
return this.client.request('GET', `/v1/checkout_sessions/${encodeURIComponent(id)}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** `peable.checkout.sessions.*` — a thin namespace wrapper so the public API
|
|
15
|
+
* reads `peable.checkout.sessions.create(...)` (Stripe Checkout Session
|
|
16
|
+
* parity), leaving room for future `peable.checkout.*` resources. */
|
|
17
|
+
export class CheckoutResource {
|
|
18
|
+
constructor(client) {
|
|
19
|
+
this.sessions = new CheckoutSessionsResource(client);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class PaymentIntentsResource {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
}
|
|
5
|
+
create(params, options) {
|
|
6
|
+
return this.client.request('POST', '/v1/payment_intents', {
|
|
7
|
+
body: params,
|
|
8
|
+
idempotencyKey: options.idempotencyKey,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
retrieve(id) {
|
|
12
|
+
return this.client.request('GET', `/v1/payment_intents/${encodeURIComponent(id)}`);
|
|
13
|
+
}
|
|
14
|
+
list(params = {}) {
|
|
15
|
+
return this.client.request('GET', '/v1/payment_intents', {
|
|
16
|
+
query: {
|
|
17
|
+
status: params.status,
|
|
18
|
+
limit: params.limit,
|
|
19
|
+
starting_after: params.starting_after,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
reject(id) {
|
|
24
|
+
return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class PaymentLinksResource {
|
|
2
|
+
constructor(client) {
|
|
3
|
+
this.client = client;
|
|
4
|
+
}
|
|
5
|
+
create(params) {
|
|
6
|
+
return this.client.request('POST', '/v1/payment_links', { body: params });
|
|
7
|
+
}
|
|
8
|
+
retrieve(id) {
|
|
9
|
+
return this.client.request('GET', `/v1/payment_links/${encodeURIComponent(id)}`);
|
|
10
|
+
}
|
|
11
|
+
list(params = {}) {
|
|
12
|
+
return this.client.request('GET', '/v1/payment_links', {
|
|
13
|
+
query: { limit: params.limit, starting_after: params.starting_after },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
update(id, params) {
|
|
17
|
+
return this.client.request('PATCH', `/v1/payment_links/${encodeURIComponent(id)}`, { body: params });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { verifyWebhook } from '@peable.to/shared-types';
|
|
2
|
+
import { PeableSignatureVerificationError } from '../core/errors.js';
|
|
3
|
+
/** Header the Gateway's `webhookDispatcher.deliver` signs with (`Peable-Signature`). */
|
|
4
|
+
export const WEBHOOK_SIGNATURE_HEADER = 'Peable-Signature';
|
|
5
|
+
/** Default replay-tolerance window, matching the backend's own tests/usage. */
|
|
6
|
+
const DEFAULT_TOLERANCE_SEC = 300;
|
|
7
|
+
const WEBHOOK_EVENT_TYPES = [
|
|
8
|
+
'payment_intent.confirming',
|
|
9
|
+
'payment_intent.settled',
|
|
10
|
+
'payment_intent.failed',
|
|
11
|
+
'payment_intent.rejected',
|
|
12
|
+
'payment_intent.expired',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Structural guard for the parsed payload — the signature already proves the
|
|
16
|
+
* bytes were produced by `signWebhook` on the SAME secret, but a caller could
|
|
17
|
+
* still pass an unrelated (validly HMAC'd under a shared secret) JSON blob;
|
|
18
|
+
* this keeps `constructEvent`'s return type honest rather than a blind cast.
|
|
19
|
+
*/
|
|
20
|
+
function isWebhookEventShape(value) {
|
|
21
|
+
if (typeof value !== 'object' || value === null)
|
|
22
|
+
return false;
|
|
23
|
+
const candidate = value;
|
|
24
|
+
return (typeof candidate.id === 'string' &&
|
|
25
|
+
candidate.object === 'event' &&
|
|
26
|
+
typeof candidate.type === 'string' &&
|
|
27
|
+
WEBHOOK_EVENT_TYPES.includes(candidate.type) &&
|
|
28
|
+
typeof candidate.created === 'string' &&
|
|
29
|
+
typeof candidate.data === 'object' &&
|
|
30
|
+
candidate.data !== null &&
|
|
31
|
+
'object' in candidate.data);
|
|
32
|
+
}
|
|
33
|
+
export class WebhooksResource {
|
|
34
|
+
/**
|
|
35
|
+
* Verify a webhook delivery and parse it into a typed `WebhookEvent`.
|
|
36
|
+
* Verifies via the SAME `verifyWebhook` the Gateway signs with
|
|
37
|
+
* (`@peable.to/shared-types`) so the algorithm can never drift between the two
|
|
38
|
+
* sides. Throws `PeableSignatureVerificationError` on a bad/stale/tampered
|
|
39
|
+
* signature or a malformed payload — never returns a partially-trusted
|
|
40
|
+
* event.
|
|
41
|
+
*/
|
|
42
|
+
constructEvent(rawBody, signatureHeader, endpointSecret, opts = {}) {
|
|
43
|
+
const toleranceSec = opts.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
|
|
44
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
45
|
+
const verified = verifyWebhook(endpointSecret, rawBody, signatureHeader, toleranceSec, nowSec);
|
|
46
|
+
if (!verified) {
|
|
47
|
+
throw new PeableSignatureVerificationError('Webhook signature verification failed — the payload, signature header, or secret do not match, or the timestamp is stale.');
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(rawBody);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new PeableSignatureVerificationError('Webhook signature verified, but the payload is not valid JSON');
|
|
55
|
+
}
|
|
56
|
+
if (!isWebhookEventShape(parsed)) {
|
|
57
|
+
throw new PeableSignatureVerificationError('Webhook signature verified, but the payload does not match the expected event shape');
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { PaymentIntent } from '@peable.to/shared-types';
|
|
2
|
+
import { PeableError } from '../core/errors';
|
|
3
|
+
/**
|
|
4
|
+
* Derive and validate the payment intent id for `mount()`. When `intentId` is
|
|
5
|
+
* omitted, the id IS the prefix of `clientSecret` before `_secret_`
|
|
6
|
+
* (`clientSecretFor` in `packages/backend/src/lib/ids.ts` mints
|
|
7
|
+
* `${id}_secret_<hex>`, so `clientSecret.split('_secret_')[0]` recovers it).
|
|
8
|
+
* Either way the result must look like a real Gateway-minted id AND
|
|
9
|
+
* `clientSecret` must actually belong to it — a malformed or mismatched pair
|
|
10
|
+
* is a caller integration bug, and `mount()` must never render a button
|
|
11
|
+
* wired to a bogus id.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deriveIntentId(clientSecret: string, intentId?: string): string;
|
|
14
|
+
export interface PayDeepLinkParams {
|
|
15
|
+
intentId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
address: string;
|
|
18
|
+
/** Base-unit integer string (m⊜) — `PaymentIntent['amount']`'s own shape. */
|
|
19
|
+
amount: string;
|
|
20
|
+
network: PaymentIntent['network'];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build an `peable://pay?...` deep link. Param names AND percent-encoding
|
|
24
|
+
* must match the wallet's parser byte-for-byte
|
|
25
|
+
* (`packages/frontend/src/pay/payment-request.ts`'s `parseQuery`, which
|
|
26
|
+
* splits on `&` then `=` and `decodeURIComponent`s each side raw) —
|
|
27
|
+
* deliberately built by hand with `encodeURIComponent` rather than
|
|
28
|
+
* `URLSearchParams`, whose `application/x-www-form-urlencoded` encoding
|
|
29
|
+
* (e.g. space → `+`) is a different scheme than plain `decodeURIComponent`
|
|
30
|
+
* expects on the other end.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildPayDeepLink(params: PayDeepLinkParams): string;
|
|
33
|
+
export declare function isMobileUserAgent(userAgent: string): boolean;
|
|
34
|
+
export interface PeableCheckoutEventMap {
|
|
35
|
+
settled: PaymentIntent;
|
|
36
|
+
confirming: PaymentIntent;
|
|
37
|
+
failed: PaymentIntent;
|
|
38
|
+
error: PeableError;
|
|
39
|
+
}
|
|
40
|
+
type CheckoutEventName = keyof PeableCheckoutEventMap;
|
|
41
|
+
type CheckoutEventHandler<K extends CheckoutEventName> = (payload: PeableCheckoutEventMap[K]) => void;
|
|
42
|
+
interface CheckoutEmitter {
|
|
43
|
+
on<K extends CheckoutEventName>(event: K, handler: CheckoutEventHandler<K>): void;
|
|
44
|
+
emit<K extends CheckoutEventName>(event: K, payload: PeableCheckoutEventMap[K]): void;
|
|
45
|
+
removeAll(): void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A minimal typed pub/sub. The public `on`/`emit` are fully type-checked per
|
|
49
|
+
* event; the internal `Map` necessarily erases each handler's specific
|
|
50
|
+
* payload type (it stores handlers for every event key in one collection),
|
|
51
|
+
* so the single cast at the storage boundary is intentional, not a type-safety
|
|
52
|
+
* hole — nothing outside this function ever sees the erased type.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createEmitter(): CheckoutEmitter;
|
|
55
|
+
export interface PeableCheckoutMountOptions {
|
|
56
|
+
/** The payment intent's `client_secret` — the payer's proof of possession. */
|
|
57
|
+
clientSecret: string;
|
|
58
|
+
/** Payment intent id. Derived from `clientSecret` when omitted — see {@link deriveIntentId}. */
|
|
59
|
+
intentId?: string;
|
|
60
|
+
/** Gateway base URL, forwarded to {@link createPeableCheckout}. Default `https://api.peable.to`. */
|
|
61
|
+
gatewayUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface PeableCheckoutInstance {
|
|
64
|
+
on<K extends CheckoutEventName>(event: K, handler: CheckoutEventHandler<K>): void;
|
|
65
|
+
/** Unsubscribes from realtime updates and removes the button from the DOM. */
|
|
66
|
+
destroy(): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Render the "Pay with Peable" button into `target` and start tracking the
|
|
70
|
+
* payment intent. Returns synchronously — before any network call resolves —
|
|
71
|
+
* so a caller's `.on(...)` registrations (called immediately after `mount()`
|
|
72
|
+
* returns, in the same synchronous tick) are always in place before the
|
|
73
|
+
* first `settled`/`confirming`/`failed`/`error` event can fire on the
|
|
74
|
+
* following microtask.
|
|
75
|
+
*/
|
|
76
|
+
declare function mount(target: string | Element, opts: PeableCheckoutMountOptions): PeableCheckoutInstance;
|
|
77
|
+
export declare const PeableCheckout: {
|
|
78
|
+
mount: typeof mount;
|
|
79
|
+
};
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PaymentIntent } from '@peable.to/shared-types';
|
|
2
|
+
export interface PeableCheckoutClient {
|
|
3
|
+
/**
|
|
4
|
+
* `GET /v1/payment_intents/:id` — the initial REST snapshot. Sends
|
|
5
|
+
* `clientSecret` via the `X-Peable-Client-Secret` header, never a query
|
|
6
|
+
* param, so the capability token never lands in server/CDN access logs.
|
|
7
|
+
*/
|
|
8
|
+
getPaymentIntent(id: string, clientSecret: string): Promise<PaymentIntent>;
|
|
9
|
+
/** Realtime status stream over the Gateway's socket contract, filtered to this intent. */
|
|
10
|
+
subscribe(id: string, clientSecret: string, onUpdate: (intent: PaymentIntent) => void): Promise<{
|
|
11
|
+
unsubscribe(): void;
|
|
12
|
+
}>;
|
|
13
|
+
/** `POST /v1/payment_intents/:id/submit_tx` — report a broadcast txid. */
|
|
14
|
+
submitTx(id: string, clientSecret: string, txid: string): Promise<PaymentIntent>;
|
|
15
|
+
}
|
|
16
|
+
export interface CreatePeableCheckoutOptions {
|
|
17
|
+
/** Gateway base URL. Default `https://api.peable.to`. */
|
|
18
|
+
gatewayUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Factory for the payer-side client core.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createPeableCheckout(opts?: CreatePeableCheckoutOptions): PeableCheckoutClient;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServiceTokenProvider } from './serviceToken';
|
|
2
|
+
export interface RestClientRequestOptions {
|
|
3
|
+
body?: unknown;
|
|
4
|
+
query?: Record<string, string | number | undefined>;
|
|
5
|
+
idempotencyKey?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface RestClient {
|
|
9
|
+
request<T>(method: string, path: string, opts?: RestClientRequestOptions): Promise<T>;
|
|
10
|
+
}
|
|
11
|
+
export interface RestClientConfig {
|
|
12
|
+
/** Gateway base URL, already stripped of a trailing slash. */
|
|
13
|
+
baseURL: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function createRestClient(config: RestClientConfig, tokenProvider: ServiceTokenProvider, deps?: {
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
}): RestClient;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Default Gateway host (`api.peable.to`) — the SDK never sends a `livemode`
|
|
2
|
+
* flag; the credential's `environment` rides in the minted service JWT and
|
|
3
|
+
* the Gateway resolves test/live from it. */
|
|
4
|
+
export declare const DEFAULT_GATEWAY_BASE_URL = "https://api.peable.to";
|
|
5
|
+
/** Default oxy-api host used to mint the short-lived service token. */
|
|
6
|
+
export declare const DEFAULT_OXY_API_URL = "https://api.oxy.so";
|
|
7
|
+
export interface PeableConfig {
|
|
8
|
+
/** ApplicationCredential publicKey (e.g. `oxy_dk_...`). */
|
|
9
|
+
publicKey: string;
|
|
10
|
+
/** ApplicationCredential plaintext secret. */
|
|
11
|
+
secret: string;
|
|
12
|
+
/** Gateway base URL. Default `https://api.peable.to`. */
|
|
13
|
+
baseURL?: string;
|
|
14
|
+
/** oxy-api host used to mint the service token. Default `https://api.oxy.so`. */
|
|
15
|
+
oxyApiUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/** `PeableConfig` with every optional field defaulted and normalized. */
|
|
18
|
+
export interface ResolvedPeableConfig {
|
|
19
|
+
publicKey: string;
|
|
20
|
+
secret: string;
|
|
21
|
+
baseURL: string;
|
|
22
|
+
oxyApiUrl: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Validate and normalize an `PeableConfig`. Throws a plain `Error` for a
|
|
26
|
+
* missing `publicKey`/`secret` — a programmer error caught at construction
|
|
27
|
+
* time, not an API-response failure, so it deliberately does not go through
|
|
28
|
+
* the `PeableError` hierarchy (which models mapped HTTP responses).
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveConfig(config: PeableConfig): ResolvedPeableConfig;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type PeableErrorType = 'authentication_error' | 'invalid_request_error' | 'permission_error' | 'api_error' | 'signature_verification_error';
|
|
2
|
+
export interface PeableErrorDetails {
|
|
3
|
+
/** The HTTP status code that produced this error, when applicable. */
|
|
4
|
+
statusCode?: number;
|
|
5
|
+
/**
|
|
6
|
+
* The raw upstream discriminator: `error.type` from the Gateway's own
|
|
7
|
+
* `{ error: { type, message } }` envelope, or the flat `error` code string
|
|
8
|
+
* from `@oxyhq/core`'s auth middleware (`{ error: 'INVALID_TOKEN', ... }`)
|
|
9
|
+
* — see `errorFromResponse`'s doc comment for why both shapes exist.
|
|
10
|
+
*/
|
|
11
|
+
code?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class PeableError extends Error {
|
|
14
|
+
readonly type: PeableErrorType;
|
|
15
|
+
readonly statusCode?: number;
|
|
16
|
+
readonly code?: string;
|
|
17
|
+
constructor(type: PeableErrorType, message: string, details?: PeableErrorDetails);
|
|
18
|
+
}
|
|
19
|
+
export declare class PeableAuthenticationError extends PeableError {
|
|
20
|
+
constructor(message: string, details?: PeableErrorDetails);
|
|
21
|
+
}
|
|
22
|
+
export declare class PeableInvalidRequestError extends PeableError {
|
|
23
|
+
constructor(message: string, details?: PeableErrorDetails);
|
|
24
|
+
}
|
|
25
|
+
export declare class PeablePermissionError extends PeableError {
|
|
26
|
+
constructor(message: string, details?: PeableErrorDetails);
|
|
27
|
+
}
|
|
28
|
+
export declare class PeableApiError extends PeableError {
|
|
29
|
+
constructor(message: string, details?: PeableErrorDetails);
|
|
30
|
+
}
|
|
31
|
+
/** Thrown by `webhooks.constructEvent` on a bad/stale/tampered signature. */
|
|
32
|
+
export declare class PeableSignatureVerificationError extends PeableError {
|
|
33
|
+
constructor(message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Map an HTTP status + parsed JSON body into a typed `PeableError`.
|
|
37
|
+
*
|
|
38
|
+
* The Oxy ecosystem returns TWO distinct error envelopes depending on WHERE a
|
|
39
|
+
* request was rejected:
|
|
40
|
+
* - The Gateway's own route handlers use the Stripe-style nested envelope
|
|
41
|
+
* `{ error: { type, message } }` (`lib/http.ts`'s `sendError`).
|
|
42
|
+
* - `@oxyhq/core`'s Express auth middleware (`oxyClient.serviceAuth()` /
|
|
43
|
+
* `requireScope()` — rejecting a missing/expired/insufficiently-scoped
|
|
44
|
+
* service token BEFORE a route handler ever runs) and oxy-api's own
|
|
45
|
+
* `ApiError.toJSON()` use a FLAT envelope: `{ error: <CODE_STRING>,
|
|
46
|
+
* message, code? }`.
|
|
47
|
+
*
|
|
48
|
+
* A caller of this SDK hits the flat shape on every auth failure (expired
|
|
49
|
+
* token, missing scope, bad credentials at mint time), so the HTTP status
|
|
50
|
+
* code — not `error.type` — is the primary classifier here; the raw
|
|
51
|
+
* upstream `type`/`code` string is still attached to the resulting error
|
|
52
|
+
* (`.code`) for callers who want it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function errorFromResponse(status: number, body: unknown): PeableError;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PeableConfig } from './config';
|
|
2
|
+
export interface ServiceTokenProvider {
|
|
3
|
+
/** Mints once, caches until near expiry, re-mints on expiry/401. */
|
|
4
|
+
getToken(): Promise<string>;
|
|
5
|
+
/** Drop the cached token so the next `getToken()` re-mints. Called by
|
|
6
|
+
* `RestClient` after a Gateway request comes back 401. */
|
|
7
|
+
invalidate(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function createServiceTokenProvider(cfg: PeableConfig, deps?: {
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}): ServiceTokenProvider;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type PeableConfig } from './core/config';
|
|
2
|
+
import { PaymentIntentsResource } from './resources/paymentIntents';
|
|
3
|
+
import { PaymentLinksResource } from './resources/paymentLinks';
|
|
4
|
+
import { CheckoutResource } from './resources/checkoutSessions';
|
|
5
|
+
import { WebhooksResource } from './resources/webhooks';
|
|
6
|
+
export declare class Peable {
|
|
7
|
+
readonly paymentIntents: PaymentIntentsResource;
|
|
8
|
+
readonly paymentLinks: PaymentLinksResource;
|
|
9
|
+
readonly checkout: CheckoutResource;
|
|
10
|
+
readonly webhooks: WebhooksResource;
|
|
11
|
+
constructor(config: PeableConfig);
|
|
12
|
+
}
|
|
13
|
+
export type { PeableConfig, ResolvedPeableConfig } from './core/config';
|
|
14
|
+
export { DEFAULT_GATEWAY_BASE_URL, DEFAULT_OXY_API_URL } from './core/config';
|
|
15
|
+
export { PeableError, PeableAuthenticationError, PeableInvalidRequestError, PeablePermissionError, PeableApiError, PeableSignatureVerificationError, } from './core/errors';
|
|
16
|
+
export type { PeableErrorType, PeableErrorDetails } from './core/errors';
|
|
17
|
+
export type { RestClient, RestClientRequestOptions } from './core/client';
|
|
18
|
+
export type { ServiceTokenProvider } from './core/serviceToken';
|
|
19
|
+
export type { CreatePaymentIntentOptions, PaymentIntentListParams, PaymentIntentList, } from './resources/paymentIntents';
|
|
20
|
+
export type { PaymentLinkListParams, PaymentLinkList, UpdatePaymentLinkParams, } from './resources/paymentLinks';
|
|
21
|
+
export { WEBHOOK_SIGNATURE_HEADER } from './resources/webhooks';
|
|
22
|
+
export type { ConstructEventOptions } from './resources/webhooks';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CheckoutSession, CreateCheckoutSessionParams } from '@peable.to/shared-types';
|
|
2
|
+
import type { RestClient } from '../core/client';
|
|
3
|
+
export declare class CheckoutSessionsResource {
|
|
4
|
+
private readonly client;
|
|
5
|
+
constructor(client: RestClient);
|
|
6
|
+
create(params: CreateCheckoutSessionParams): Promise<CheckoutSession>;
|
|
7
|
+
retrieve(id: string): Promise<CheckoutSession>;
|
|
8
|
+
}
|
|
9
|
+
/** `peable.checkout.sessions.*` — a thin namespace wrapper so the public API
|
|
10
|
+
* reads `peable.checkout.sessions.create(...)` (Stripe Checkout Session
|
|
11
|
+
* parity), leaving room for future `peable.checkout.*` resources. */
|
|
12
|
+
export declare class CheckoutResource {
|
|
13
|
+
readonly sessions: CheckoutSessionsResource;
|
|
14
|
+
constructor(client: RestClient);
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CreatePaymentIntentParams, PaymentIntent, PaymentIntentStatus } from '@peable.to/shared-types';
|
|
2
|
+
import type { RestClient } from '../core/client';
|
|
3
|
+
export interface PaymentIntentListParams {
|
|
4
|
+
status?: PaymentIntentStatus;
|
|
5
|
+
limit?: number;
|
|
6
|
+
starting_after?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface PaymentIntentList {
|
|
9
|
+
object: 'list';
|
|
10
|
+
data: PaymentIntent[];
|
|
11
|
+
has_more: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface CreatePaymentIntentOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Required — the Gateway mandates the `Idempotency-Key` header on
|
|
16
|
+
* `POST /v1/payment_intents` (`paymentIntents.ts`'s `idempotencyKey`
|
|
17
|
+
* check). Making this a required option (not optional) means a caller
|
|
18
|
+
* cannot forget it.
|
|
19
|
+
*/
|
|
20
|
+
idempotencyKey: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class PaymentIntentsResource {
|
|
23
|
+
private readonly client;
|
|
24
|
+
constructor(client: RestClient);
|
|
25
|
+
create(params: CreatePaymentIntentParams, options: CreatePaymentIntentOptions): Promise<PaymentIntent>;
|
|
26
|
+
retrieve(id: string): Promise<PaymentIntent>;
|
|
27
|
+
list(params?: PaymentIntentListParams): Promise<PaymentIntentList>;
|
|
28
|
+
reject(id: string): Promise<PaymentIntent>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CreatePaymentLinkParams, PaymentLink } from '@peable.to/shared-types';
|
|
2
|
+
import type { RestClient } from '../core/client';
|
|
3
|
+
export interface PaymentLinkListParams {
|
|
4
|
+
limit?: number;
|
|
5
|
+
starting_after?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PaymentLinkList {
|
|
8
|
+
object: 'list';
|
|
9
|
+
data: PaymentLink[];
|
|
10
|
+
has_more: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Fields mutable after a link is created. `amount`/`network` are
|
|
14
|
+
* deliberately absent — a link's price is immutable once shared (the
|
|
15
|
+
* Gateway's `PATCH /v1/payment_links/:id` whitelist enforces the same).
|
|
16
|
+
*/
|
|
17
|
+
export interface UpdatePaymentLinkParams {
|
|
18
|
+
active?: boolean;
|
|
19
|
+
metadata?: Record<string, string>;
|
|
20
|
+
successUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class PaymentLinksResource {
|
|
23
|
+
private readonly client;
|
|
24
|
+
constructor(client: RestClient);
|
|
25
|
+
create(params: CreatePaymentLinkParams): Promise<PaymentLink>;
|
|
26
|
+
retrieve(id: string): Promise<PaymentLink>;
|
|
27
|
+
list(params?: PaymentLinkListParams): Promise<PaymentLinkList>;
|
|
28
|
+
update(id: string, params: UpdatePaymentLinkParams): Promise<PaymentLink>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type WebhookEvent } from '@peable.to/shared-types';
|
|
2
|
+
/** Header the Gateway's `webhookDispatcher.deliver` signs with (`Peable-Signature`). */
|
|
3
|
+
export declare const WEBHOOK_SIGNATURE_HEADER = "Peable-Signature";
|
|
4
|
+
export interface ConstructEventOptions {
|
|
5
|
+
/** Max allowed drift (seconds) between the signed timestamp and now. */
|
|
6
|
+
toleranceSec?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class WebhooksResource {
|
|
9
|
+
/**
|
|
10
|
+
* Verify a webhook delivery and parse it into a typed `WebhookEvent`.
|
|
11
|
+
* Verifies via the SAME `verifyWebhook` the Gateway signs with
|
|
12
|
+
* (`@peable.to/shared-types`) so the algorithm can never drift between the two
|
|
13
|
+
* sides. Throws `PeableSignatureVerificationError` on a bad/stale/tampered
|
|
14
|
+
* signature or a malformed payload — never returns a partially-trusted
|
|
15
|
+
* event.
|
|
16
|
+
*/
|
|
17
|
+
constructEvent(rawBody: string, signatureHeader: string, endpointSecret: string, opts?: ConstructEventOptions): WebhookEvent;
|
|
18
|
+
}
|