@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.
Files changed (37) hide show
  1. package/dist/cjs/browser/mount.js +308 -0
  2. package/dist/cjs/browser/payerClient.js +135 -0
  3. package/dist/cjs/checkout.js +17 -0
  4. package/dist/cjs/core/client.js +77 -0
  5. package/dist/cjs/core/config.js +38 -0
  6. package/dist/cjs/core/errors.js +112 -0
  7. package/dist/cjs/core/serviceToken.js +81 -0
  8. package/dist/cjs/index.js +41 -0
  9. package/dist/cjs/resources/checkoutSessions.js +26 -0
  10. package/dist/cjs/resources/paymentIntents.js +30 -0
  11. package/dist/cjs/resources/paymentLinks.js +23 -0
  12. package/dist/cjs/resources/webhooks.js +65 -0
  13. package/dist/esm/browser/mount.js +298 -0
  14. package/dist/esm/browser/payerClient.js +132 -0
  15. package/dist/esm/checkout.js +12 -0
  16. package/dist/esm/core/client.js +74 -0
  17. package/dist/esm/core/config.js +34 -0
  18. package/dist/esm/core/errors.js +102 -0
  19. package/dist/esm/core/serviceToken.js +78 -0
  20. package/dist/esm/index.js +28 -0
  21. package/dist/esm/resources/checkoutSessions.js +21 -0
  22. package/dist/esm/resources/paymentIntents.js +26 -0
  23. package/dist/esm/resources/paymentLinks.js +19 -0
  24. package/dist/esm/resources/webhooks.js +61 -0
  25. package/dist/types/browser/mount.d.ts +80 -0
  26. package/dist/types/browser/payerClient.d.ts +23 -0
  27. package/dist/types/checkout.d.ts +2 -0
  28. package/dist/types/core/client.d.ts +17 -0
  29. package/dist/types/core/config.d.ts +30 -0
  30. package/dist/types/core/errors.d.ts +54 -0
  31. package/dist/types/core/serviceToken.d.ts +11 -0
  32. package/dist/types/index.d.ts +22 -0
  33. package/dist/types/resources/checkoutSessions.d.ts +15 -0
  34. package/dist/types/resources/paymentIntents.d.ts +29 -0
  35. package/dist/types/resources/paymentLinks.d.ts +29 -0
  36. package/dist/types/resources/webhooks.d.ts +18 -0
  37. package/package.json +83 -0
@@ -0,0 +1,308 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PeableCheckout = void 0;
7
+ exports.deriveIntentId = deriveIntentId;
8
+ exports.buildPayDeepLink = buildPayDeepLink;
9
+ exports.isMobileUserAgent = isMobileUserAgent;
10
+ exports.createEmitter = createEmitter;
11
+ // `PeableCheckout.mount(...)` — the embeddable "Pay with Peable" button
12
+ // (Fase 2 SDK plan, Task 6). Plain DOM, no framework, so it embeds on any
13
+ // site. Renders a button into the host element, fetches + subscribes to the
14
+ // payment intent via the Task 5 payer client, drives a typed
15
+ // `settled`/`confirming`/`failed`/`error` event emitter, and on click either
16
+ // opens the `peable://pay` deep link (mobile) or shows a QR of it (desktop).
17
+ //
18
+ // Same `./core/*` boundary as `payerClient.ts`: the only import from `./core`
19
+ // is `core/errors.ts`, a pure status-mapper with no service-token/secret
20
+ // machinery, so it carries none of the merchant-authed code out of bounds for
21
+ // this browser bundle.
22
+ const qrcode_generator_1 = __importDefault(require("qrcode-generator"));
23
+ const payerClient_1 = require("./payerClient");
24
+ const errors_1 = require("../core/errors");
25
+ // ---------------------------------------------------------------------------
26
+ // Intent id derivation (pure, unit-tested independently of the DOM).
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * `pi_` followed by ≥24 lowercase hex chars — the Gateway's payment-intent id
30
+ * format. Mirrors `INTENT_ID_PATTERN` in the wallet's
31
+ * `packages/frontend/src/pay/payment-request.ts` byte-for-byte, and matches
32
+ * what `newId('pi')` in `packages/backend/src/lib/ids.ts` actually mints (12
33
+ * random bytes → 24 hex chars).
34
+ */
35
+ const INTENT_ID_PATTERN = /^pi_[0-9a-f]{24,}$/;
36
+ /**
37
+ * Derive and validate the payment intent id for `mount()`. When `intentId` is
38
+ * omitted, the id IS the prefix of `clientSecret` before `_secret_`
39
+ * (`clientSecretFor` in `packages/backend/src/lib/ids.ts` mints
40
+ * `${id}_secret_<hex>`, so `clientSecret.split('_secret_')[0]` recovers it).
41
+ * Either way the result must look like a real Gateway-minted id AND
42
+ * `clientSecret` must actually belong to it — a malformed or mismatched pair
43
+ * is a caller integration bug, and `mount()` must never render a button
44
+ * wired to a bogus id.
45
+ */
46
+ function deriveIntentId(clientSecret, intentId) {
47
+ const id = intentId ?? clientSecret.split('_secret_')[0] ?? '';
48
+ if (!INTENT_ID_PATTERN.test(id)) {
49
+ throw new errors_1.PeableInvalidRequestError(`PeableCheckout.mount: invalid or missing payment intent id ("${id}") — ` +
50
+ 'pass `intentId` explicitly or a well-formed `clientSecret`');
51
+ }
52
+ if (!clientSecret.startsWith(`${id}_secret_`)) {
53
+ throw new errors_1.PeableInvalidRequestError('PeableCheckout.mount: clientSecret does not belong to the given intentId');
54
+ }
55
+ return id;
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Deep link (pure, unit-tested independently of the DOM).
59
+ // ---------------------------------------------------------------------------
60
+ /** The `peable://pay` deep-link scheme + host — mirrors `PAYMENT_REQUEST_PREFIX`
61
+ * in the wallet's `packages/frontend/src/pay/payment-request.ts`. */
62
+ const PAYMENT_REQUEST_PREFIX = 'peable://pay';
63
+ /**
64
+ * Build an `peable://pay?...` deep link. Param names AND percent-encoding
65
+ * must match the wallet's parser byte-for-byte
66
+ * (`packages/frontend/src/pay/payment-request.ts`'s `parseQuery`, which
67
+ * splits on `&` then `=` and `decodeURIComponent`s each side raw) —
68
+ * deliberately built by hand with `encodeURIComponent` rather than
69
+ * `URLSearchParams`, whose `application/x-www-form-urlencoded` encoding
70
+ * (e.g. space → `+`) is a different scheme than plain `decodeURIComponent`
71
+ * expects on the other end.
72
+ */
73
+ function buildPayDeepLink(params) {
74
+ const pairs = [
75
+ ['intent', params.intentId],
76
+ ['secret', params.clientSecret],
77
+ ['address', params.address],
78
+ ['amount', params.amount],
79
+ ['network', params.network],
80
+ ];
81
+ const query = pairs
82
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
83
+ .join('&');
84
+ return `${PAYMENT_REQUEST_PREFIX}?${query}`;
85
+ }
86
+ /** Only a phone can plausibly have the Peable wallet app installed to catch
87
+ * the `peable://` deep link — desktop gets a QR to scan with it instead. */
88
+ const MOBILE_USER_AGENT_PATTERN = /Android|iPhone|iPad|iPod/i;
89
+ function isMobileUserAgent(userAgent) {
90
+ return MOBILE_USER_AGENT_PATTERN.test(userAgent);
91
+ }
92
+ /**
93
+ * A minimal typed pub/sub. The public `on`/`emit` are fully type-checked per
94
+ * event; the internal `Map` necessarily erases each handler's specific
95
+ * payload type (it stores handlers for every event key in one collection),
96
+ * so the single cast at the storage boundary is intentional, not a type-safety
97
+ * hole — nothing outside this function ever sees the erased type.
98
+ */
99
+ function createEmitter() {
100
+ const handlers = new Map();
101
+ return {
102
+ on(event, handler) {
103
+ const set = handlers.get(event) ?? new Set();
104
+ set.add(handler);
105
+ handlers.set(event, set);
106
+ },
107
+ emit(event, payload) {
108
+ const set = handlers.get(event);
109
+ if (!set)
110
+ return;
111
+ for (const handler of set) {
112
+ handler(payload);
113
+ }
114
+ },
115
+ removeAll() {
116
+ handlers.clear();
117
+ },
118
+ };
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // DOM rendering — exercised only by the real-browser Step 3 check (deferred;
122
+ // see the Task 6 report), not by the headless unit suite.
123
+ // ---------------------------------------------------------------------------
124
+ const BUTTON_CLASS = 'peable-checkout-button';
125
+ const OVERLAY_CLASS = 'peable-checkout-qr-overlay';
126
+ /** Statuses that still accept a payment — everything else disables the button. */
127
+ const PAYABLE_STATUSES = new Set([
128
+ 'created',
129
+ 'awaiting_approval',
130
+ 'approved',
131
+ ]);
132
+ function resolveTarget(target) {
133
+ const element = typeof target === 'string' ? document.querySelector(target) : target;
134
+ if (!(element instanceof Element)) {
135
+ throw new errors_1.PeableInvalidRequestError(typeof target === 'string'
136
+ ? `PeableCheckout.mount: no element matches selector "${target}"`
137
+ : 'PeableCheckout.mount: target is not a DOM Element');
138
+ }
139
+ return element;
140
+ }
141
+ function renderButton(container) {
142
+ const button = document.createElement('button');
143
+ button.type = 'button';
144
+ button.className = BUTTON_CLASS;
145
+ button.textContent = 'Pay with Peable';
146
+ button.disabled = true; // enabled once the initial snapshot loads
147
+ button.style.cssText =
148
+ 'padding:12px 24px;border-radius:8px;border:none;background:#5b21b6;' +
149
+ 'color:#fff;font:600 15px system-ui,sans-serif;cursor:pointer;';
150
+ container.replaceChildren(button);
151
+ return button;
152
+ }
153
+ function updateButton(button, intent) {
154
+ button.disabled = !PAYABLE_STATUSES.has(intent.status);
155
+ switch (intent.status) {
156
+ case 'settled':
157
+ button.textContent = 'Paid';
158
+ break;
159
+ case 'confirming':
160
+ button.textContent = 'Confirming…';
161
+ break;
162
+ case 'broadcast':
163
+ button.textContent = 'Payment received';
164
+ break;
165
+ case 'failed':
166
+ case 'expired':
167
+ case 'rejected':
168
+ button.textContent = 'Payment unavailable';
169
+ break;
170
+ default:
171
+ button.textContent = 'Pay with Peable';
172
+ }
173
+ }
174
+ /**
175
+ * Show a QR of the deep link for a desktop payer to scan with the wallet
176
+ * app. `qr.createSvgTag()` is `qrcode-generator`'s own trusted output — a
177
+ * grid of `<rect>` "modules" encoding `deepLink` as opaque data, never as
178
+ * literal SVG text — so assigning it via `innerHTML` carries no injection
179
+ * risk from `deepLink`'s content.
180
+ */
181
+ function showQrOverlay(deepLink) {
182
+ const qr = (0, qrcode_generator_1.default)(0, 'M'); // type 0 = auto-sized, level M = 15% error correction
183
+ qr.addData(deepLink);
184
+ qr.make();
185
+ const overlay = document.createElement('div');
186
+ overlay.className = OVERLAY_CLASS;
187
+ overlay.style.cssText =
188
+ 'position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;' +
189
+ 'justify-content:center;background:rgba(0,0,0,0.5);';
190
+ overlay.addEventListener('click', (event) => {
191
+ if (event.target === overlay)
192
+ overlay.remove();
193
+ });
194
+ const card = document.createElement('div');
195
+ card.style.cssText =
196
+ 'background:#fff;border-radius:12px;padding:24px;display:flex;' +
197
+ 'flex-direction:column;align-items:center;gap:12px;';
198
+ card.innerHTML = qr.createSvgTag({ cellSize: 4, margin: 4, scalable: true });
199
+ const caption = document.createElement('p');
200
+ caption.textContent = 'Scan with the Peable app';
201
+ caption.style.cssText = 'margin:0;font:500 14px system-ui,sans-serif;color:#111;';
202
+ const closeButton = document.createElement('button');
203
+ closeButton.type = 'button';
204
+ closeButton.textContent = 'Close';
205
+ closeButton.style.cssText =
206
+ 'padding:8px 16px;border-radius:6px;border:1px solid #d1d5db;background:#fff;' +
207
+ 'font:500 14px system-ui,sans-serif;cursor:pointer;';
208
+ closeButton.addEventListener('click', () => overlay.remove());
209
+ card.append(caption, closeButton);
210
+ overlay.appendChild(card);
211
+ document.body.appendChild(overlay);
212
+ }
213
+ function toPeableError(error) {
214
+ if (error instanceof errors_1.PeableError)
215
+ return error;
216
+ return new errors_1.PeableApiError(error instanceof Error ? error.message : String(error));
217
+ }
218
+ /**
219
+ * Render the "Pay with Peable" button into `target` and start tracking the
220
+ * payment intent. Returns synchronously — before any network call resolves —
221
+ * so a caller's `.on(...)` registrations (called immediately after `mount()`
222
+ * returns, in the same synchronous tick) are always in place before the
223
+ * first `settled`/`confirming`/`failed`/`error` event can fire on the
224
+ * following microtask.
225
+ */
226
+ function mount(target, opts) {
227
+ const element = resolveTarget(target);
228
+ const intentId = deriveIntentId(opts.clientSecret, opts.intentId);
229
+ const clientSecret = opts.clientSecret;
230
+ const emitter = createEmitter();
231
+ const payerClient = (0, payerClient_1.createPeableCheckout)({ gatewayUrl: opts.gatewayUrl });
232
+ const button = renderButton(element);
233
+ let latestIntent = null;
234
+ let subscription = null;
235
+ let destroyed = false;
236
+ function handleIntentUpdate(intent) {
237
+ latestIntent = intent;
238
+ updateButton(button, intent);
239
+ if (intent.status === 'settled') {
240
+ emitter.emit('settled', intent);
241
+ }
242
+ else if (intent.status === 'confirming') {
243
+ emitter.emit('confirming', intent);
244
+ }
245
+ else if (intent.status === 'failed' ||
246
+ intent.status === 'expired' ||
247
+ intent.status === 'rejected') {
248
+ emitter.emit('failed', intent);
249
+ }
250
+ }
251
+ button.addEventListener('click', () => {
252
+ if (!latestIntent)
253
+ return; // initial snapshot hasn't loaded yet
254
+ const deepLink = buildPayDeepLink({
255
+ intentId,
256
+ clientSecret,
257
+ address: latestIntent.address,
258
+ amount: latestIntent.amount,
259
+ network: latestIntent.network,
260
+ });
261
+ if (isMobileUserAgent(navigator.userAgent)) {
262
+ window.location.href = deepLink;
263
+ }
264
+ else {
265
+ showQrOverlay(deepLink);
266
+ }
267
+ });
268
+ // `destroyed` closes a race a caller CAN legitimately hit: calling
269
+ // `destroy()` while `getPaymentIntent`/`subscribe` is still in flight (e.g.
270
+ // the host page unmounts the widget immediately). Without this guard, a
271
+ // subscription created AFTER `destroy()` ran would overwrite `subscription`
272
+ // (still `null` at destroy time) and never get torn down — a leaked socket
273
+ // connection for the rest of the tab's life.
274
+ void payerClient
275
+ .getPaymentIntent(intentId, clientSecret)
276
+ .then((intent) => {
277
+ if (destroyed)
278
+ return null;
279
+ handleIntentUpdate(intent);
280
+ return payerClient.subscribe(intentId, clientSecret, handleIntentUpdate);
281
+ })
282
+ .then((sub) => {
283
+ if (!sub)
284
+ return;
285
+ if (destroyed) {
286
+ sub.unsubscribe();
287
+ return;
288
+ }
289
+ subscription = sub;
290
+ })
291
+ .catch((error) => {
292
+ if (destroyed)
293
+ return;
294
+ emitter.emit('error', toPeableError(error));
295
+ });
296
+ return {
297
+ on(event, handler) {
298
+ emitter.on(event, handler);
299
+ },
300
+ destroy() {
301
+ destroyed = true;
302
+ subscription?.unsubscribe();
303
+ emitter.removeAll();
304
+ button.remove();
305
+ },
306
+ };
307
+ }
308
+ exports.PeableCheckout = { mount };
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPeableCheckout = createPeableCheckout;
4
+ // The payer-side REST+socket client core — the "./checkout" entry's payer
5
+ // capability (Fase 2 SDK plan, Task 5). Mirrors the wallet's
6
+ // `gateway-client.ts`/`gateway-socket.ts` but standalone: no
7
+ // `@oxyhq/services`/`@oxyhq/core` — this is a third-party browser bundle, so
8
+ // it talks to the Gateway over bare `fetch` + `socket.io-client`.
9
+ //
10
+ // Holds NO service token and NO private key — only a public `client_secret`,
11
+ // the exact payer capability the wallet's own payer path already uses
12
+ // (non-custody invariant). `core/errors.ts` is the one `./core/*` import: it
13
+ // is a pure status-code→`PeableError` mapper with no service-token/secret
14
+ // machinery, so reusing it here (instead of throwing bare `Error`) does not
15
+ // carry any of the merchant-authed code `checkout.ts` warns against into the
16
+ // browser bundle.
17
+ const socket_io_client_1 = require("socket.io-client");
18
+ const errors_1 = require("../core/errors");
19
+ const DEFAULT_GATEWAY_URL = 'https://api.peable.to';
20
+ async function readJsonBody(response) {
21
+ const text = await response.text();
22
+ if (text.length === 0)
23
+ return undefined;
24
+ try {
25
+ return JSON.parse(text);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ /**
32
+ * Factory for the payer-side client core.
33
+ */
34
+ function createPeableCheckout(opts = {}) {
35
+ const baseUrl = (opts.gatewayUrl ?? DEFAULT_GATEWAY_URL).replace(/\/$/, '');
36
+ async function getPaymentIntent(id, clientSecret) {
37
+ let response;
38
+ try {
39
+ response = await fetch(`${baseUrl}/v1/payment_intents/${encodeURIComponent(id)}`, {
40
+ headers: { 'X-Peable-Client-Secret': clientSecret },
41
+ });
42
+ }
43
+ catch (cause) {
44
+ throw new errors_1.PeableApiError(`Failed to reach the Peable Gateway at ${baseUrl}: ${cause instanceof Error ? cause.message : String(cause)}`);
45
+ }
46
+ const body = await readJsonBody(response);
47
+ if (!response.ok)
48
+ throw (0, errors_1.errorFromResponse)(response.status, body);
49
+ return body;
50
+ }
51
+ async function submitTx(id, clientSecret, txid) {
52
+ let response;
53
+ try {
54
+ response = await fetch(`${baseUrl}/v1/payment_intents/${encodeURIComponent(id)}/submit_tx`, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({ client_secret: clientSecret, txid }),
58
+ });
59
+ }
60
+ catch (cause) {
61
+ throw new errors_1.PeableApiError(`Failed to reach the Peable Gateway at ${baseUrl}: ${cause instanceof Error ? cause.message : String(cause)}`);
62
+ }
63
+ const body = await readJsonBody(response);
64
+ if (!response.ok)
65
+ throw (0, errors_1.errorFromResponse)(response.status, body);
66
+ return body;
67
+ }
68
+ /**
69
+ * Opens its OWN socket per call (never a shared/module-level connection —
70
+ * this SDK has no session to key a singleton off) and connects with NO
71
+ * handshake auth token: the Gateway's connection auth is identity-optional
72
+ * (`optionalSocketAuth`, `packages/backend/src/realtime/socket.ts`) exactly
73
+ * so an anonymous checkout/embed payer can reach it. `subscribe`'s ack is
74
+ * the real authorization check — it requires this intent's `client_secret`
75
+ * — so nothing here needs an Oxy session.
76
+ */
77
+ async function subscribe(id, clientSecret, onUpdate) {
78
+ const socket = (0, socket_io_client_1.io)(baseUrl, {
79
+ transports: ['websocket'],
80
+ });
81
+ let ack;
82
+ try {
83
+ ack = await socket.emitWithAck('subscribe', { intentId: id, clientSecret });
84
+ }
85
+ catch (cause) {
86
+ socket.disconnect();
87
+ throw new errors_1.PeableApiError(`Failed to reach the Peable Gateway realtime layer at ${baseUrl}: ${cause instanceof Error ? cause.message : String(cause)}`);
88
+ }
89
+ if (!ack.ok) {
90
+ socket.disconnect();
91
+ // The Gateway's `subscribe` handler returns this SAME {ok:false} for
92
+ // an unknown intent AND a wrong client_secret (anti-enumeration — see
93
+ // `realtime/socket.ts`), so this is deliberately InvalidRequestError,
94
+ // not PermissionError: there is no signal here to tell the two apart.
95
+ throw new errors_1.PeableInvalidRequestError(`Gateway rejected subscription to payment intent ${id} ` +
96
+ '(unknown payment intent or invalid client_secret)');
97
+ }
98
+ // The socket only ever carries this one subscription, but filter by id
99
+ // anyway — matching the wallet's per-intent filter exactly means a future
100
+ // change there (e.g. a shared connection) can't silently leak another
101
+ // intent's updates into this caller's `onUpdate`.
102
+ const listener = (intent) => {
103
+ if (intent.id === id) {
104
+ onUpdate(intent);
105
+ }
106
+ };
107
+ socket.on('intent.updated', listener);
108
+ // socket.io-client's default reconnection restores the TRANSPORT, but the
109
+ // server spins up a fresh Socket with no room membership — so every
110
+ // reconnect (mobile network blip, backgrounded tab, LB cycle) must re-run
111
+ // the room-join handshake or `intent.updated` silently stops arriving for
112
+ // the rest of the subscription's life. A hosted checkout tab can sit open
113
+ // unattended for minutes while a payment settles, with no app-lifecycle
114
+ // trigger to notice and recover — unlike the wallet, this can't rely on
115
+ // the user reopening the screen. `reconnect` is a Manager-level event
116
+ // (`socket.io`, not `socket` itself) in socket.io-client.
117
+ const onReconnect = () => {
118
+ void socket.emitWithAck('subscribe', { intentId: id, clientSecret }).catch(() => {
119
+ // Fire-and-forget: `onUpdate` has no error channel to surface a
120
+ // failed resubscribe (e.g. the intent expired between drop and
121
+ // reconnect) — socket.io keeps retrying the transport regardless, so
122
+ // there is nothing actionable to do with this rejection here.
123
+ });
124
+ };
125
+ socket.io.on('reconnect', onReconnect);
126
+ return {
127
+ unsubscribe() {
128
+ socket.off('intent.updated', listener);
129
+ socket.io.off('reconnect', onReconnect);
130
+ socket.disconnect();
131
+ },
132
+ };
133
+ }
134
+ return { getPaymentIntent, subscribe, submitTx };
135
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PeableCheckout = exports.createPeableCheckout = void 0;
4
+ // Browser entry (`@peable.to/sdk/checkout`) — the anonymous payer-side client
5
+ // core (Task 5) plus the embeddable pay-button widget (Task 6). Deliberately
6
+ // imports nothing from `./core/*` beyond `core/errors.ts` (a pure
7
+ // status-mapper with no service-token/secret machinery): the browser bundle
8
+ // must never carry the merchant-authed code or a path to a secret.
9
+ //
10
+ // `packages/checkout` (the hosted checkout page, a separate package) can
11
+ // still consume `createPeableCheckout` directly instead of `PeableCheckout`
12
+ // when it wants to build its own UI over the payer client core (see the
13
+ // checkout+links plan's `intentClient.ts`).
14
+ var payerClient_1 = require("./browser/payerClient");
15
+ Object.defineProperty(exports, "createPeableCheckout", { enumerable: true, get: function () { return payerClient_1.createPeableCheckout; } });
16
+ var mount_1 = require("./browser/mount");
17
+ Object.defineProperty(exports, "PeableCheckout", { enumerable: true, get: function () { return mount_1.PeableCheckout; } });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ // `RestClient` — the thin HTTP layer every resource namespace calls through.
3
+ // Prefixes `baseURL`, attaches the bearer service token, sets
4
+ // `Idempotency-Key` when given, serializes query params, parses JSON, and
5
+ // maps every non-2xx response via `errors.ts`.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.createRestClient = createRestClient;
8
+ const errors_1 = require("./errors");
9
+ const HTTP_UNAUTHORIZED = 401;
10
+ function buildUrl(baseURL, path, query) {
11
+ const search = new URLSearchParams();
12
+ if (query) {
13
+ for (const [key, value] of Object.entries(query)) {
14
+ if (value === undefined)
15
+ continue;
16
+ search.set(key, String(value));
17
+ }
18
+ }
19
+ const queryString = search.toString();
20
+ return `${baseURL}${path}${queryString ? `?${queryString}` : ''}`;
21
+ }
22
+ async function readJsonBody(response) {
23
+ const text = await response.text();
24
+ if (text.length === 0)
25
+ return undefined;
26
+ try {
27
+ return JSON.parse(text);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ function createRestClient(config, tokenProvider, deps = {}) {
34
+ const fetchImpl = deps.fetch ?? fetch;
35
+ async function performRequest(method, path, opts, allowRetryOn401) {
36
+ const token = await tokenProvider.getToken();
37
+ const headers = { Authorization: `Bearer ${token}` };
38
+ if (opts.body !== undefined)
39
+ headers['Content-Type'] = 'application/json';
40
+ if (opts.idempotencyKey !== undefined)
41
+ headers['Idempotency-Key'] = opts.idempotencyKey;
42
+ if (opts.headers)
43
+ Object.assign(headers, opts.headers);
44
+ let response;
45
+ try {
46
+ response = await fetchImpl(buildUrl(config.baseURL, path, opts.query), {
47
+ method,
48
+ headers,
49
+ body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
50
+ });
51
+ }
52
+ catch (cause) {
53
+ throw new errors_1.PeableApiError(`Failed to reach the Peable Gateway at ${config.baseURL}: ${cause instanceof Error ? cause.message : String(cause)}`);
54
+ }
55
+ // A 401 here means the cached service token expired or was revoked
56
+ // mid-flight (it was valid when minted, or a prior request would have
57
+ // failed at `tokenProvider.getToken()` already). Invalidate the cache and
58
+ // retry exactly once with a freshly minted token before giving up.
59
+ if (response.status === HTTP_UNAUTHORIZED && allowRetryOn401) {
60
+ tokenProvider.invalidate();
61
+ return performRequest(method, path, opts, false);
62
+ }
63
+ // The Gateway echoes a date-based `Peable-Version` response header
64
+ // (Stripe parity). No SDK behavior depends on it yet, so it is
65
+ // deliberately not read here — nothing to do with it today.
66
+ const body = await readJsonBody(response);
67
+ if (!response.ok) {
68
+ throw (0, errors_1.errorFromResponse)(response.status, body);
69
+ }
70
+ return body;
71
+ }
72
+ return {
73
+ request(method, path, opts = {}) {
74
+ return performRequest(method, path, opts, true);
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ // Configuration for the server-side `Peable` client. The SDK is configured
3
+ // with a confidential `ApplicationCredential` (`{publicKey, secret}`, the
4
+ // SAME credential Oxy Console already issues for a `type:'service'`
5
+ // credential) — never a bespoke "Peable API key". See the SDK plan's
6
+ // "Architecture / auth" section.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.DEFAULT_OXY_API_URL = exports.DEFAULT_GATEWAY_BASE_URL = void 0;
9
+ exports.resolveConfig = resolveConfig;
10
+ /** Default Gateway host (`api.peable.to`) — the SDK never sends a `livemode`
11
+ * flag; the credential's `environment` rides in the minted service JWT and
12
+ * the Gateway resolves test/live from it. */
13
+ exports.DEFAULT_GATEWAY_BASE_URL = 'https://api.peable.to';
14
+ /** Default oxy-api host used to mint the short-lived service token. */
15
+ exports.DEFAULT_OXY_API_URL = 'https://api.oxy.so';
16
+ function stripTrailingSlash(url) {
17
+ return url.endsWith('/') ? url.slice(0, -1) : url;
18
+ }
19
+ /**
20
+ * Validate and normalize an `PeableConfig`. Throws a plain `Error` for a
21
+ * missing `publicKey`/`secret` — a programmer error caught at construction
22
+ * time, not an API-response failure, so it deliberately does not go through
23
+ * the `PeableError` hierarchy (which models mapped HTTP responses).
24
+ */
25
+ function resolveConfig(config) {
26
+ if (!config.publicKey) {
27
+ throw new Error('Peable: `publicKey` is required');
28
+ }
29
+ if (!config.secret) {
30
+ throw new Error('Peable: `secret` is required');
31
+ }
32
+ return {
33
+ publicKey: config.publicKey,
34
+ secret: config.secret,
35
+ baseURL: stripTrailingSlash(config.baseURL ?? exports.DEFAULT_GATEWAY_BASE_URL),
36
+ oxyApiUrl: stripTrailingSlash(config.oxyApiUrl ?? exports.DEFAULT_OXY_API_URL),
37
+ };
38
+ }