@peable.to/sdk 0.1.1 → 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 +14 -0
- package/dist/cjs/browser/mount.js +55 -8
- package/dist/cjs/browser/payerClient.js +58 -8
- package/dist/cjs/checkout.js +9 -1
- package/dist/cjs/core/client.js +25 -1
- package/dist/cjs/core/config.js +11 -0
- package/dist/cjs/core/errors.js +1 -1
- package/dist/cjs/core/serviceToken.js +11 -2
- package/dist/cjs/index.js +28 -1
- package/dist/cjs/resources/billing.js +25 -0
- package/dist/cjs/resources/checkoutSessions.js +2 -1
- package/dist/cjs/resources/connectedAccounts.js +63 -0
- package/dist/cjs/resources/merchants.js +51 -0
- package/dist/cjs/resources/paymentIntents.js +30 -2
- package/dist/cjs/resources/refunds.js +54 -0
- package/dist/cjs/resources/transfers.js +49 -0
- package/dist/esm/browser/mount.js +52 -7
- package/dist/esm/browser/payerClient.js +58 -8
- package/dist/esm/checkout.js +5 -0
- package/dist/esm/core/client.js +25 -1
- package/dist/esm/core/config.js +10 -0
- package/dist/esm/core/errors.js +1 -1
- package/dist/esm/core/serviceToken.js +11 -2
- package/dist/esm/index.js +28 -1
- package/dist/esm/resources/billing.js +21 -0
- package/dist/esm/resources/checkoutSessions.js +2 -1
- package/dist/esm/resources/connectedAccounts.js +59 -0
- package/dist/esm/resources/merchants.js +46 -0
- package/dist/esm/resources/paymentIntents.js +30 -2
- package/dist/esm/resources/refunds.js +50 -0
- package/dist/esm/resources/transfers.js +45 -0
- package/dist/types/browser/mount.d.ts +23 -1
- package/dist/types/browser/payerClient.d.ts +56 -2
- package/dist/types/checkout.d.ts +2 -1
- package/dist/types/core/client.d.ts +1 -0
- package/dist/types/core/config.d.ts +4 -0
- package/dist/types/core/errors.d.ts +2 -2
- package/dist/types/index.d.ts +40 -1
- package/dist/types/resources/billing.d.ts +15 -0
- package/dist/types/resources/checkoutSessions.d.ts +15 -1
- package/dist/types/resources/connectedAccounts.d.ts +84 -0
- package/dist/types/resources/merchants.d.ts +113 -0
- package/dist/types/resources/paymentIntents.d.ts +47 -1
- package/dist/types/resources/refunds.d.ts +77 -0
- package/dist/types/resources/transfers.d.ts +75 -0
- package/package.json +2 -2
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.
|
|
@@ -3,8 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.PeableCheckout = void 0;
|
|
6
|
+
exports.PeableCheckout = exports.PeableRailUnsupportedError = void 0;
|
|
7
7
|
exports.deriveIntentId = deriveIntentId;
|
|
8
|
+
exports.payDeepLinkFor = payDeepLinkFor;
|
|
8
9
|
exports.buildPayDeepLink = buildPayDeepLink;
|
|
9
10
|
exports.isMobileUserAgent = isMobileUserAgent;
|
|
10
11
|
exports.createEmitter = createEmitter;
|
|
@@ -60,6 +61,47 @@ function deriveIntentId(clientSecret, intentId) {
|
|
|
60
61
|
/** The `peable://pay` deep-link scheme + host — mirrors `PAYMENT_REQUEST_PREFIX`
|
|
61
62
|
* in the wallet's `packages/frontend/src/pay/payment-request.ts`. */
|
|
62
63
|
const PAYMENT_REQUEST_PREFIX = 'peable://pay';
|
|
64
|
+
/**
|
|
65
|
+
* Thrown when a deep link is asked for on an intent that has no chain payment
|
|
66
|
+
* behind it.
|
|
67
|
+
*
|
|
68
|
+
* `address` and `network` became nullable when the card rail landed (gateway
|
|
69
|
+
* ADR 0001 D6), and the tempting fix was a `?? ''` at the call site. That would
|
|
70
|
+
* mint `peable://pay?...&address=&network=` — a link the wallet's parser
|
|
71
|
+
* accepts structurally and which sends a payer to an empty address. Failing
|
|
72
|
+
* here is the only safe direction.
|
|
73
|
+
*/
|
|
74
|
+
class PeableRailUnsupportedError extends errors_1.PeableError {
|
|
75
|
+
constructor(rail) {
|
|
76
|
+
// Extends `PeableError`, not `Error`: the widget reports everything through
|
|
77
|
+
// its `error` event, whose payload type is `PeableError`, and a bare `Error`
|
|
78
|
+
// would arrive there only after being flattened into a `PeableApiError`
|
|
79
|
+
// that a caller cannot tell apart from a network failure.
|
|
80
|
+
// `invalid_request_error`, because that is what it is: the caller asked
|
|
81
|
+
// this widget for a surface the intent does not have. Nothing went wrong
|
|
82
|
+
// upstream and retrying cannot help.
|
|
83
|
+
super('invalid_request_error', `the '${rail}' rail has no wallet deep link; it is not a chain payment`);
|
|
84
|
+
this.name = 'PeableRailUnsupportedError';
|
|
85
|
+
Object.setPrototypeOf(this, PeableRailUnsupportedError.prototype);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.PeableRailUnsupportedError = PeableRailUnsupportedError;
|
|
89
|
+
/**
|
|
90
|
+
* Build the deep link for an intent, refusing any intent that is not a chain
|
|
91
|
+
* payment. The narrowing is what lets `buildPayDeepLink` keep non-null fields.
|
|
92
|
+
*/
|
|
93
|
+
function payDeepLinkFor(intent) {
|
|
94
|
+
if (intent.rail !== 'faircoin' || intent.address === null || intent.network === null) {
|
|
95
|
+
throw new PeableRailUnsupportedError(intent.rail);
|
|
96
|
+
}
|
|
97
|
+
return buildPayDeepLink({
|
|
98
|
+
intentId: intent.id,
|
|
99
|
+
clientSecret: intent.clientSecret,
|
|
100
|
+
address: intent.address,
|
|
101
|
+
amount: intent.amount,
|
|
102
|
+
network: intent.network,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
63
105
|
/**
|
|
64
106
|
* Build an `peable://pay?...` deep link. Param names AND percent-encoding
|
|
65
107
|
* must match the wallet's parser byte-for-byte
|
|
@@ -251,13 +293,18 @@ function mount(target, opts) {
|
|
|
251
293
|
button.addEventListener('click', () => {
|
|
252
294
|
if (!latestIntent)
|
|
253
295
|
return; // initial snapshot hasn't loaded yet
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
296
|
+
// A card intent has no wallet to deep-link into. `error` rather than a
|
|
297
|
+
// throw into an event handler nobody is listening to: this widget's whole
|
|
298
|
+
// contract is that a host page learns what happened through `on(...)`, and
|
|
299
|
+
// an uncaught exception here would leave the button silently dead.
|
|
300
|
+
let deepLink;
|
|
301
|
+
try {
|
|
302
|
+
deepLink = payDeepLinkFor(latestIntent);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
emitter.emit('error', toPeableError(err));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
261
308
|
if (isMobileUserAgent(navigator.userAgent)) {
|
|
262
309
|
window.location.href = deepLink;
|
|
263
310
|
}
|
|
@@ -4,7 +4,7 @@ exports.createPeableCheckout = createPeableCheckout;
|
|
|
4
4
|
// The payer-side REST+socket client core — the "./checkout" entry's payer
|
|
5
5
|
// capability (Fase 2 SDK plan, Task 5). Mirrors the wallet's
|
|
6
6
|
// `gateway-client.ts`/`gateway-socket.ts` but standalone: no
|
|
7
|
-
// `@
|
|
7
|
+
// `@oxy.so/services`/`@oxy.so/core` — this is a third-party browser bundle, so
|
|
8
8
|
// it talks to the Gateway over bare `fetch` + `socket.io-client`.
|
|
9
9
|
//
|
|
10
10
|
// Holds NO service token and NO private key — only a public `client_secret`,
|
|
@@ -65,6 +65,25 @@ function createPeableCheckout(opts = {}) {
|
|
|
65
65
|
throw (0, errors_1.errorFromResponse)(response.status, body);
|
|
66
66
|
return body;
|
|
67
67
|
}
|
|
68
|
+
async function getClientAction(id, clientSecret) {
|
|
69
|
+
let response;
|
|
70
|
+
try {
|
|
71
|
+
response = await fetch(`${baseUrl}/v1/payment_intents/${encodeURIComponent(id)}/client_action`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
// The HEADER, never a query param, for the same reason the snapshot
|
|
74
|
+
// read uses one: a capability token in a URL lands in every access
|
|
75
|
+
// log between here and the gateway.
|
|
76
|
+
headers: { 'X-Peable-Client-Secret': clientSecret },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch (cause) {
|
|
80
|
+
throw new errors_1.PeableApiError(`Failed to reach the Peable Gateway at ${baseUrl}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
81
|
+
}
|
|
82
|
+
const body = await readJsonBody(response);
|
|
83
|
+
if (!response.ok)
|
|
84
|
+
throw (0, errors_1.errorFromResponse)(response.status, body);
|
|
85
|
+
return body;
|
|
86
|
+
}
|
|
68
87
|
/**
|
|
69
88
|
* Opens its OWN socket per call (never a shared/module-level connection —
|
|
70
89
|
* this SDK has no session to key a singleton off) and connects with NO
|
|
@@ -74,7 +93,7 @@ function createPeableCheckout(opts = {}) {
|
|
|
74
93
|
* the real authorization check — it requires this intent's `client_secret`
|
|
75
94
|
* — so nothing here needs an Oxy session.
|
|
76
95
|
*/
|
|
77
|
-
async function subscribe(id, clientSecret, onUpdate) {
|
|
96
|
+
async function subscribe(id, clientSecret, onUpdate, onConnectionChange) {
|
|
78
97
|
const socket = (0, socket_io_client_1.io)(baseUrl, {
|
|
79
98
|
transports: ['websocket'],
|
|
80
99
|
});
|
|
@@ -105,6 +124,22 @@ function createPeableCheckout(opts = {}) {
|
|
|
105
124
|
}
|
|
106
125
|
};
|
|
107
126
|
socket.on('intent.updated', listener);
|
|
127
|
+
// A drop AFTER this point used to be invisible to the caller. `subscribe`
|
|
128
|
+
// had already resolved, so a `.catch` on it could not fire again, and the
|
|
129
|
+
// reconnect below restores DELIVERY but replays nothing — socket.io has no
|
|
130
|
+
// buffer for frames emitted while the socket was gone. A payment that
|
|
131
|
+
// settled during a thirty-second blip therefore never reached the page, and
|
|
132
|
+
// the payer watched a snapshot that would never update again.
|
|
133
|
+
//
|
|
134
|
+
// Reported rather than handled here: the SDK cannot know what a host wants
|
|
135
|
+
// to do about it. The hosted checkout turns REST polling on and re-reads
|
|
136
|
+
// the intent before turning it off again.
|
|
137
|
+
let closedByUs = false;
|
|
138
|
+
const onDisconnect = () => {
|
|
139
|
+
if (!closedByUs)
|
|
140
|
+
onConnectionChange?.('lost');
|
|
141
|
+
};
|
|
142
|
+
socket.on('disconnect', onDisconnect);
|
|
108
143
|
// socket.io-client's default reconnection restores the TRANSPORT, but the
|
|
109
144
|
// server spins up a fresh Socket with no room membership — so every
|
|
110
145
|
// reconnect (mobile network blip, backgrounded tab, LB cycle) must re-run
|
|
@@ -115,21 +150,36 @@ function createPeableCheckout(opts = {}) {
|
|
|
115
150
|
// the user reopening the screen. `reconnect` is a Manager-level event
|
|
116
151
|
// (`socket.io`, not `socket` itself) in socket.io-client.
|
|
117
152
|
const onReconnect = () => {
|
|
118
|
-
void socket
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
//
|
|
122
|
-
//
|
|
153
|
+
void socket
|
|
154
|
+
.emitWithAck('subscribe', { intentId: id, clientSecret })
|
|
155
|
+
.then((ack) => {
|
|
156
|
+
// `'live'` only once the ROOM is rejoined, never on the transport
|
|
157
|
+
// alone: a socket that is connected and in no room delivers nothing,
|
|
158
|
+
// and telling the host it is live there would have them stand their
|
|
159
|
+
// fallback down into permanent silence.
|
|
160
|
+
if (ack.ok)
|
|
161
|
+
onConnectionChange?.('live');
|
|
162
|
+
})
|
|
163
|
+
.catch(() => {
|
|
164
|
+
// The subscription is gone for good (the intent expired between drop
|
|
165
|
+
// and reconnect, say). Staying `lost` is the honest report: socket.io
|
|
166
|
+
// keeps retrying the transport, but nothing will arrive on it.
|
|
123
167
|
});
|
|
124
168
|
};
|
|
125
169
|
socket.io.on('reconnect', onReconnect);
|
|
126
170
|
return {
|
|
127
171
|
unsubscribe() {
|
|
172
|
+
// Set BEFORE `disconnect()`: a deliberate close fires `disconnect` like
|
|
173
|
+
// any other, and reporting `'lost'` there would have a host spin up a
|
|
174
|
+
// fallback for a subscription it just tore down — on an unmounting
|
|
175
|
+
// component, at that.
|
|
176
|
+
closedByUs = true;
|
|
128
177
|
socket.off('intent.updated', listener);
|
|
178
|
+
socket.off('disconnect', onDisconnect);
|
|
129
179
|
socket.io.off('reconnect', onReconnect);
|
|
130
180
|
socket.disconnect();
|
|
131
181
|
},
|
|
132
182
|
};
|
|
133
183
|
}
|
|
134
|
-
return { getPaymentIntent, subscribe, submitTx };
|
|
184
|
+
return { getPaymentIntent, subscribe, submitTx, getClientAction };
|
|
135
185
|
}
|
package/dist/cjs/checkout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PeableCheckout = exports.createPeableCheckout = void 0;
|
|
3
|
+
exports.PeableRailUnsupportedError = exports.buildPayDeepLink = exports.payDeepLinkFor = exports.PeableCheckout = exports.createPeableCheckout = void 0;
|
|
4
4
|
// Browser entry (`@peable.to/sdk/checkout`) — the anonymous payer-side client
|
|
5
5
|
// core (Task 5) plus the embeddable pay-button widget (Task 6). Deliberately
|
|
6
6
|
// imports nothing from `./core/*` beyond `core/errors.ts` (a pure
|
|
@@ -15,3 +15,11 @@ var payerClient_1 = require("./browser/payerClient");
|
|
|
15
15
|
Object.defineProperty(exports, "createPeableCheckout", { enumerable: true, get: function () { return payerClient_1.createPeableCheckout; } });
|
|
16
16
|
var mount_1 = require("./browser/mount");
|
|
17
17
|
Object.defineProperty(exports, "PeableCheckout", { enumerable: true, get: function () { return mount_1.PeableCheckout; } });
|
|
18
|
+
// The rail-aware deep-link builder and its refusal. Exported because
|
|
19
|
+
// `packages/checkout` renders its own pay button over the payer client core
|
|
20
|
+
// and must make the same decision this widget makes — one definition of "which
|
|
21
|
+
// intents have a wallet deep link", not two.
|
|
22
|
+
var mount_2 = require("./browser/mount");
|
|
23
|
+
Object.defineProperty(exports, "payDeepLinkFor", { enumerable: true, get: function () { return mount_2.payDeepLinkFor; } });
|
|
24
|
+
Object.defineProperty(exports, "buildPayDeepLink", { enumerable: true, get: function () { return mount_2.buildPayDeepLink; } });
|
|
25
|
+
Object.defineProperty(exports, "PeableRailUnsupportedError", { enumerable: true, get: function () { return mount_2.PeableRailUnsupportedError; } });
|
package/dist/cjs/core/client.js
CHANGED
|
@@ -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
|
}
|
|
@@ -63,10 +66,31 @@ function createRestClient(config, tokenProvider, deps = {}) {
|
|
|
63
66
|
// The Gateway echoes a date-based `Peable-Version` response header
|
|
64
67
|
// (Stripe parity). No SDK behavior depends on it yet, so it is
|
|
65
68
|
// deliberately not read here — nothing to do with it today.
|
|
66
|
-
|
|
69
|
+
let body;
|
|
70
|
+
try {
|
|
71
|
+
body = await readJsonBody(response);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Headers can arrive before the connection fails during body consumption.
|
|
75
|
+
// Preserve the known status, but do not infer whether a mutation committed
|
|
76
|
+
// or retry it automatically. Recovery keeps the caller's original key.
|
|
77
|
+
throw new errors_1.PeableApiError('The Peable Gateway response body could not be read; remote outcome is unknown', {
|
|
78
|
+
statusCode: response.status,
|
|
79
|
+
code: 'invalid_response',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
67
82
|
if (!response.ok) {
|
|
68
83
|
throw (0, errors_1.errorFromResponse)(response.status, body);
|
|
69
84
|
}
|
|
85
|
+
// Only explicit No Content can satisfy a void response. A missing or
|
|
86
|
+
// malformed JSON success is indeterminate, not a typed resource. Do not
|
|
87
|
+
// retry a mutation automatically: callers retain the original intent key.
|
|
88
|
+
if (body === undefined && response.status !== 204) {
|
|
89
|
+
throw new errors_1.PeableApiError('The Peable Gateway returned an invalid JSON response', {
|
|
90
|
+
statusCode: response.status,
|
|
91
|
+
code: 'invalid_response',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
70
94
|
return body;
|
|
71
95
|
}
|
|
72
96
|
return {
|
package/dist/cjs/core/config.js
CHANGED
|
@@ -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),
|
package/dist/cjs/core/errors.js
CHANGED
|
@@ -78,7 +78,7 @@ function isFlatGatewayErrorBody(body) {
|
|
|
78
78
|
* request was rejected:
|
|
79
79
|
* - The Gateway's own route handlers use the Stripe-style nested envelope
|
|
80
80
|
* `{ error: { type, message } }` (`lib/http.ts`'s `sendError`).
|
|
81
|
-
* - `@
|
|
81
|
+
* - `@oxy.so/core`'s Express auth middleware (`oxy.middleware.service()` /
|
|
82
82
|
* `requireScope()` — rejecting a missing/expired/insufficiently-scoped
|
|
83
83
|
* service token BEFORE a route handler ever runs) and oxy-api's own
|
|
84
84
|
* `ApiError.toJSON()` use a FLAT envelope: `{ error: <CODE_STRING>,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Mints a short-lived Oxy service token from an `ApplicationCredential`
|
|
3
3
|
// (`{publicKey, secret}`) by presenting it to oxy-api's `POST
|
|
4
|
-
// /auth/service-token` — the exact mechanism `
|
|
4
|
+
// /auth/service-token` — the exact mechanism `oxy.middleware.service()`
|
|
5
5
|
// validates at the Gateway. Caches the token until near expiry and re-mints
|
|
6
6
|
// reactively when `RestClient` sees a 401 (see `client.ts`).
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -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
|
-
|
|
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WEBHOOK_SIGNATURE_HEADER = exports.PeableSignatureVerificationError = exports.PeableApiError = exports.PeablePermissionError = exports.PeableInvalidRequestError = exports.PeableAuthenticationError = exports.PeableError = exports.DEFAULT_OXY_API_URL = exports.DEFAULT_GATEWAY_BASE_URL = exports.Peable = void 0;
|
|
4
|
+
const billing_1 = require("./resources/billing");
|
|
4
5
|
// Server entry (`@peable.to/sdk`) — the merchant-authed SDK. Configured with a
|
|
5
6
|
// confidential `ApplicationCredential` (`{publicKey, secret}`); mints and
|
|
6
7
|
// caches an Oxy service token, and exposes Stripe-ergonomics resource
|
|
@@ -15,15 +16,41 @@ const paymentIntents_1 = require("./resources/paymentIntents");
|
|
|
15
16
|
const paymentLinks_1 = require("./resources/paymentLinks");
|
|
16
17
|
const checkoutSessions_1 = require("./resources/checkoutSessions");
|
|
17
18
|
const webhooks_1 = require("./resources/webhooks");
|
|
19
|
+
const connectedAccounts_1 = require("./resources/connectedAccounts");
|
|
20
|
+
const merchants_1 = require("./resources/merchants");
|
|
21
|
+
const refunds_1 = require("./resources/refunds");
|
|
22
|
+
const transfers_1 = require("./resources/transfers");
|
|
23
|
+
/**
|
|
24
|
+
* The merchant-authed client.
|
|
25
|
+
*
|
|
26
|
+
* ## Why the namespaces below are not optional extras
|
|
27
|
+
*
|
|
28
|
+
* This class published four: intents, links, checkout and webhooks. The gateway
|
|
29
|
+
* has always served merchants, connected accounts, refunds, transfers and
|
|
30
|
+
* disputes as well — so every integrator reaching those wrote their own HTTP
|
|
31
|
+
* client and their own partial types for responses this package already knew.
|
|
32
|
+
* Mercaria did exactly that. Two descriptions of one wire format, in two
|
|
33
|
+
* repositories, with nothing comparing them: a field renamed in the gateway is
|
|
34
|
+
* a runtime failure over there, found by a settlement that does not happen.
|
|
35
|
+
*
|
|
36
|
+
* An SDK that covers less than the API it wraps does not reduce surface, it
|
|
37
|
+
* relocates it.
|
|
38
|
+
*/
|
|
18
39
|
class Peable {
|
|
19
40
|
constructor(config) {
|
|
20
41
|
const resolved = (0, config_1.resolveConfig)(config);
|
|
21
42
|
const tokenProvider = (0, serviceToken_1.createServiceTokenProvider)(config);
|
|
22
|
-
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
|
+
this.billing = new billing_1.BillingResource(client);
|
|
23
45
|
this.paymentIntents = new paymentIntents_1.PaymentIntentsResource(client);
|
|
24
46
|
this.paymentLinks = new paymentLinks_1.PaymentLinksResource(client);
|
|
25
47
|
this.checkout = new checkoutSessions_1.CheckoutResource(client);
|
|
26
48
|
this.webhooks = new webhooks_1.WebhooksResource();
|
|
49
|
+
this.merchants = new merchants_1.MerchantsResource(client);
|
|
50
|
+
this.connectedAccounts = new connectedAccounts_1.ConnectedAccountsResource(client);
|
|
51
|
+
this.refunds = new refunds_1.RefundsResource(client);
|
|
52
|
+
this.transfers = new transfers_1.TransfersResource(client);
|
|
53
|
+
this.disputes = new merchants_1.DisputesResource(client);
|
|
27
54
|
}
|
|
28
55
|
}
|
|
29
56
|
exports.Peable = Peable;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BillingResource = void 0;
|
|
4
|
+
/** Recurring billing is separate from one-off checkout and marketplace transfers. */
|
|
5
|
+
class BillingResource {
|
|
6
|
+
constructor(client) {
|
|
7
|
+
this.client = client;
|
|
8
|
+
}
|
|
9
|
+
ensureCustomer(params, options) {
|
|
10
|
+
return this.client.request('POST', '/v1/billing/customers', { body: params, idempotencyKey: options.idempotencyKey });
|
|
11
|
+
}
|
|
12
|
+
createCheckoutSession(params, options) {
|
|
13
|
+
return this.client.request('POST', '/v1/billing/checkout_sessions', { body: params, idempotencyKey: options.idempotencyKey });
|
|
14
|
+
}
|
|
15
|
+
createPortalSession(params, options) {
|
|
16
|
+
return this.client.request('POST', '/v1/billing/portal_sessions', { body: params, idempotencyKey: options.idempotencyKey });
|
|
17
|
+
}
|
|
18
|
+
retrieveSubscription(ref) {
|
|
19
|
+
return this.client.request('GET', `/v1/billing/subscriptions/${encodeURIComponent(ref)}`);
|
|
20
|
+
}
|
|
21
|
+
cancelAtPeriodEnd(ref, options) {
|
|
22
|
+
return this.client.request('POST', `/v1/billing/subscriptions/${encodeURIComponent(ref)}/cancel_at_period_end`, { body: {}, idempotencyKey: options.idempotencyKey });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.BillingResource = BillingResource;
|
|
@@ -5,9 +5,10 @@ class CheckoutSessionsResource {
|
|
|
5
5
|
constructor(client) {
|
|
6
6
|
this.client = client;
|
|
7
7
|
}
|
|
8
|
-
create(params) {
|
|
8
|
+
create(params, options = {}) {
|
|
9
9
|
return this.client.request('POST', '/v1/checkout_sessions', {
|
|
10
10
|
body: params,
|
|
11
|
+
...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
retrieve(id) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConnectedAccountsResource = void 0;
|
|
4
|
+
class ConnectedAccountsResource {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Open the account for a seller, or return the one they already have.
|
|
10
|
+
*
|
|
11
|
+
* Converges rather than erroring, because the underlying object CANNOT BE
|
|
12
|
+
* DELETED: opening two for one seller leaves them with two forever, one of
|
|
13
|
+
* which nobody uses and which keeps generating requirement emails.
|
|
14
|
+
*/
|
|
15
|
+
create(params) {
|
|
16
|
+
return this.client.request('POST', '/v1/connected_accounts', {
|
|
17
|
+
body: params,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
list(params = {}) {
|
|
21
|
+
return this.client.request('GET', '/v1/connected_accounts', {
|
|
22
|
+
query: { limit: params.limit, starting_after: params.starting_after },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
retrieve(accountId) {
|
|
26
|
+
return this.client.request('GET', `/v1/connected_accounts/${encodeURIComponent(accountId)}`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* By the merchant's OWN reference.
|
|
30
|
+
*
|
|
31
|
+
* The recovery read: a merchant who lost the `ca_…` — a create whose response
|
|
32
|
+
* never arrived — still has the reference they chose, and this is what makes
|
|
33
|
+
* finding the account possible without a list scan.
|
|
34
|
+
*/
|
|
35
|
+
retrieveByRef(externalRef) {
|
|
36
|
+
return this.client.request('GET', `/v1/connected_accounts/by_ref/${encodeURIComponent(externalRef)}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Re-read this account from the provider now.
|
|
40
|
+
*
|
|
41
|
+
* Rarely needed: readiness arrives on its own through the gateway's event
|
|
42
|
+
* drain and a backstop sweep. This is for a seller staring at a dashboard who
|
|
43
|
+
* has just finished onboarding.
|
|
44
|
+
*/
|
|
45
|
+
refresh(accountId) {
|
|
46
|
+
return this.client.request('POST', `/v1/connected_accounts/${encodeURIComponent(accountId)}/refresh`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A hosted onboarding link.
|
|
50
|
+
*
|
|
51
|
+
* SINGLE-USE and short-lived at the provider, so it is minted on demand and
|
|
52
|
+
* must never be stored, emailed or put in a chat message — a stored link is
|
|
53
|
+
* one that has already expired by the time anyone follows it, and the failure
|
|
54
|
+
* looks like the seller's fault.
|
|
55
|
+
*
|
|
56
|
+
* A visit to `returnUrl` proves NOTHING about verification. Read the account
|
|
57
|
+
* back, or wait for the readiness event.
|
|
58
|
+
*/
|
|
59
|
+
createAccountLink(accountId, params) {
|
|
60
|
+
return this.client.request('POST', `/v1/connected_accounts/${encodeURIComponent(accountId)}/account_links`, { body: params });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.ConnectedAccountsResource = ConnectedAccountsResource;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DisputesResource = exports.MerchantsResource = void 0;
|
|
4
|
+
class MerchantsResource {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
register(params = {}) {
|
|
9
|
+
return this.client.request('POST', '/v1/merchants', { body: params });
|
|
10
|
+
}
|
|
11
|
+
retrieve() {
|
|
12
|
+
return this.client.request('GET', '/v1/merchants/me');
|
|
13
|
+
}
|
|
14
|
+
update(params) {
|
|
15
|
+
return this.client.request('PATCH', '/v1/merchants/me', { body: params });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.MerchantsResource = MerchantsResource;
|
|
19
|
+
/**
|
|
20
|
+
* Disputes: read them, and answer one.
|
|
21
|
+
*
|
|
22
|
+
* A dispute is initiated by the card NETWORK, not by you, so there is nothing
|
|
23
|
+
* to create — the only write is a response. This surface was read-only for a
|
|
24
|
+
* while, because the gateway had no way to deliver evidence and a route that
|
|
25
|
+
* accepted it and did nothing would have been worse than none: you would have
|
|
26
|
+
* believed you had responded.
|
|
27
|
+
*/
|
|
28
|
+
class DisputesResource {
|
|
29
|
+
constructor(client) {
|
|
30
|
+
this.client = client;
|
|
31
|
+
}
|
|
32
|
+
listForPaymentIntent(paymentIntentId) {
|
|
33
|
+
return this.client.request('GET', `/v1/payment_intents/${encodeURIComponent(paymentIntentId)}/disputes`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Answer a dispute. **One shot.**
|
|
37
|
+
*
|
|
38
|
+
* Submitting is one-way at the card network, so this cannot be revised and a
|
|
39
|
+
* second call does not send a second response — it answers 200 with the
|
|
40
|
+
* dispute as it stands. Send everything you have the first time.
|
|
41
|
+
*
|
|
42
|
+
* Refused (409) when the dispute is already answered, already decided, or
|
|
43
|
+
* past `evidenceDueAt`. That last one is checked by the gateway rather than
|
|
44
|
+
* by the acquirer, so you learn it was the clock and not your request — a
|
|
45
|
+
* late response is the one failure here that no retry fixes.
|
|
46
|
+
*/
|
|
47
|
+
submitEvidence(disputeId, evidence) {
|
|
48
|
+
return this.client.request('POST', `/v1/disputes/${encodeURIComponent(disputeId)}/evidence`, { body: evidence });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.DisputesResource = DisputesResource;
|
|
@@ -23,8 +23,36 @@ class PaymentIntentsResource {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Reject a payment the payer has not completed.
|
|
28
|
+
*
|
|
29
|
+
* The gateway CANCELS it at the provider before it announces anything, so
|
|
30
|
+
* this can lose a race and say so: a 409 means the payer confirmed first and
|
|
31
|
+
* the payment is settled, and a 502 means the provider could not be reached
|
|
32
|
+
* and nothing was decided. Neither is a failure of the request — both are
|
|
33
|
+
* facts about a payment that is still live.
|
|
34
|
+
*/
|
|
35
|
+
reject(id, options = {}) {
|
|
36
|
+
return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`, { idempotencyKey: options.idempotencyKey });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What the PAYER's client has to do next — the resume operation.
|
|
40
|
+
*
|
|
41
|
+
* The payment-intent shape carries no confirmation credential, because a
|
|
42
|
+
* credential on that shape would be handed out by every list and every
|
|
43
|
+
* re-read. This is the bounded way to get one: a single payment, named
|
|
44
|
+
* explicitly, for a caller who has proven they may pay it.
|
|
45
|
+
*
|
|
46
|
+
* Call it to resume an unpaid checkout rather than creating a second payment
|
|
47
|
+
* — which is what a buyer returning the next day would otherwise produce, and
|
|
48
|
+
* an idempotency key that has aged out at the provider will not prevent.
|
|
49
|
+
*
|
|
50
|
+
* @throws {PeableInvalidRequestError} (409) when the payment can no longer be
|
|
51
|
+
* paid, so a surface handed this can never render a card form over a
|
|
52
|
+
* settled payment.
|
|
53
|
+
*/
|
|
54
|
+
clientAction(id) {
|
|
55
|
+
return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/client_action`);
|
|
28
56
|
}
|
|
29
57
|
}
|
|
30
58
|
exports.PaymentIntentsResource = PaymentIntentsResource;
|