@rewloy/node 0.1.0
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/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/client.d.ts +120 -0
- package/dist/client.js +404 -0
- package/dist/errors.d.ts +73 -0
- package/dist/errors.js +81 -0
- package/dist/generated/methods.d.ts +3297 -0
- package/dist/generated/methods.js +3793 -0
- package/dist/generated/operations.d.ts +12 -0
- package/dist/generated/operations.js +368 -0
- package/dist/generated/types.d.ts +13164 -0
- package/dist/generated/types.js +3 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +16 -0
- package/dist/sse.d.ts +68 -0
- package/dist/sse.js +251 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.js +5 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/webhooks.d.ts +91 -0
- package/dist/webhooks.js +98 -0
- package/package.json +56 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client: credentials, the request (headers, retries, timeouts, errors,
|
|
3
|
+
* deprecation notices), pagination and streams. The operations themselves
|
|
4
|
+
* come from the generated RewloyMethods, one method per operationId.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { RateLimitError, RewloyConnectionError, RewloyError, RewloyTimeoutError } from "./errors.js";
|
|
8
|
+
import { RewloyMethods } from "./generated/methods.js";
|
|
9
|
+
import { ERROR_TITLES, OPERATIONS } from "./generated/operations.js";
|
|
10
|
+
import { EventStream } from "./sse.js";
|
|
11
|
+
import { VERSION } from "./version.js";
|
|
12
|
+
export const DEFAULT_BASE_URL = 'https://app.rewloy.com';
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
14
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
15
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
|
|
16
|
+
/** Backoff: 0.5 s, 1 s, 2 s… up to 8 s, each with jitter (between half and all of it). */
|
|
17
|
+
const BACKOFF_BASE_MS = 500;
|
|
18
|
+
const BACKOFF_MAX_MS = 8000;
|
|
19
|
+
/** A `Retry-After` longer than this is not waited for: the error goes to the caller. */
|
|
20
|
+
const MAX_RETRY_AFTER_MS = 60_000;
|
|
21
|
+
/** setTimeout's ceiling; a longer (or infinite) timeout means none. */
|
|
22
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
23
|
+
const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'PUT', 'DELETE']);
|
|
24
|
+
/** 502–504 and Cloudflare's 520–524 (the origin unreachable or too slow). */
|
|
25
|
+
const GATEWAY_STATUSES = new Set([502, 503, 504, 520, 521, 522, 523, 524]);
|
|
26
|
+
/** Operations already warned about: one warning per operation per process. */
|
|
27
|
+
const warned = new Set();
|
|
28
|
+
const defaultSleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
29
|
+
if (signal?.aborted) {
|
|
30
|
+
reject(signal.reason);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const onAbort = () => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
reject(signal.reason);
|
|
36
|
+
};
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
signal?.removeEventListener('abort', onAbort);
|
|
39
|
+
resolve();
|
|
40
|
+
}, ms);
|
|
41
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
42
|
+
});
|
|
43
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
44
|
+
/** `Retry-After` in milliseconds: delta-seconds or an HTTP date. */
|
|
45
|
+
export function parseRetryAfter(value, now = Date.now()) {
|
|
46
|
+
if (!value)
|
|
47
|
+
return null;
|
|
48
|
+
const v = value.trim();
|
|
49
|
+
if (/^\d+(\.\d+)?$/.test(v))
|
|
50
|
+
return Math.round(Number(v) * 1000);
|
|
51
|
+
const at = Date.parse(v);
|
|
52
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
53
|
+
}
|
|
54
|
+
/** Exponential backoff with jitter for the retry after attempt `attempt` (0-based). */
|
|
55
|
+
export function backoff(attempt, random = Math.random) {
|
|
56
|
+
const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
57
|
+
return Math.round(cap / 2 + random() * (cap / 2));
|
|
58
|
+
}
|
|
59
|
+
/** The URL a `Link` header gives for `rel="deprecation"` (else its first). */
|
|
60
|
+
function deprecationLink(link) {
|
|
61
|
+
if (!link)
|
|
62
|
+
return null;
|
|
63
|
+
let first = null;
|
|
64
|
+
for (const m of link.matchAll(/<([^>]*)>([^,]*)/g)) {
|
|
65
|
+
first ??= m[1];
|
|
66
|
+
if (/\brel\s*=\s*"?[^";]*\bdeprecation\b/i.test(m[2]))
|
|
67
|
+
return m[1];
|
|
68
|
+
}
|
|
69
|
+
return first;
|
|
70
|
+
}
|
|
71
|
+
function warn(message) {
|
|
72
|
+
const p = globalThis.process;
|
|
73
|
+
if (typeof p?.emitWarning !== 'function') {
|
|
74
|
+
console.warn(`DeprecationWarning: ${message}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
p.emitWarning(message, { type: 'DeprecationWarning', code: 'REWLOY_DEPRECATED' });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// `node --throw-deprecation` throws here: let it surface, not pass for a failed request.
|
|
82
|
+
queueMicrotask(() => { throw err; });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function userAgent(suffix) {
|
|
86
|
+
const node = globalThis.process?.versions?.node;
|
|
87
|
+
// A browser decides its own User-Agent, and the API's CORS allow-list has no room for it.
|
|
88
|
+
if (!node)
|
|
89
|
+
return null;
|
|
90
|
+
return [`rewloy-node/${VERSION}`, `node/${node}`, suffix?.trim()].filter(Boolean).join(' ');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A client of the Rewloy API (`https://app.rewloy.com/v1`).
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* const rewloy = new Rewloy({ apiKey: process.env.REWLOY_API_KEY! });
|
|
97
|
+
* const card = await rewloy.getPass({ params: { serial: 'ABCD-EFGH-JKLM' } });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Every operation of the API is a method named by its operationId; each takes
|
|
101
|
+
* one argument with `params`, `query` and `body` as the operation needs, and
|
|
102
|
+
* the options of {@link RequestOptions}.
|
|
103
|
+
*/
|
|
104
|
+
export class Rewloy extends RewloyMethods {
|
|
105
|
+
baseUrl;
|
|
106
|
+
timeoutMs;
|
|
107
|
+
maxRetries;
|
|
108
|
+
/** The kind of credential this client sends, or `null` for none. */
|
|
109
|
+
credential;
|
|
110
|
+
/** The default `Rewloy-Merchant` of a staff session. */
|
|
111
|
+
merchant;
|
|
112
|
+
#token;
|
|
113
|
+
#fetch;
|
|
114
|
+
#userAgent;
|
|
115
|
+
#sleep;
|
|
116
|
+
constructor(options = {}) {
|
|
117
|
+
super();
|
|
118
|
+
const given = ['apiKey', 'staffSession', 'holderSession'].filter((k) => options[k] !== undefined);
|
|
119
|
+
if (given.length > 1)
|
|
120
|
+
throw new TypeError(`Rewloy: give one credential, not ${given.join(' and ')}`);
|
|
121
|
+
const prefixes = { apiKey: ['rwk_', 'key'], staffSession: ['rws_', 'staff'], holderSession: ['rwh_', 'holder'] };
|
|
122
|
+
const which = given[0];
|
|
123
|
+
if (which) {
|
|
124
|
+
const token = options[which];
|
|
125
|
+
const [prefix, kind] = prefixes[which];
|
|
126
|
+
if (typeof token !== 'string' || !token.startsWith(prefix))
|
|
127
|
+
throw new TypeError(`Rewloy: ${which} must start with "${prefix}"`);
|
|
128
|
+
this.#token = token;
|
|
129
|
+
this.credential = kind;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
this.#token = null;
|
|
133
|
+
this.credential = null;
|
|
134
|
+
}
|
|
135
|
+
if (options.merchant !== undefined && which !== 'staffSession')
|
|
136
|
+
throw new TypeError('Rewloy: `merchant` goes with a staffSession');
|
|
137
|
+
this.merchant = options.merchant ?? null;
|
|
138
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
139
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
140
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
141
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
142
|
+
if (typeof f !== 'function')
|
|
143
|
+
throw new TypeError('Rewloy: no fetch (Node 22 or later has one)');
|
|
144
|
+
this.#fetch = options.fetch ? f : (input, init) => globalThis.fetch(input, init);
|
|
145
|
+
this.#userAgent = userAgent(options.userAgent);
|
|
146
|
+
this.#sleep = options.sleep ?? defaultSleep;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Calls an operation and returns the whole answer: `data`, `meta` on paged
|
|
150
|
+
* lists, the status, headers, `requestId`, `mode` and `replayed`.
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* const res = await rewloy.request('sendCampaign', { body: { body: 'Bu hafta kahveler 2 damga!' } });
|
|
154
|
+
* res.status; res.replayed; res.data.id;
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
async request(id, ...args) {
|
|
158
|
+
const op = this.#operation(id);
|
|
159
|
+
if (op.stream)
|
|
160
|
+
throw new TypeError(`Rewloy: ${id} is a stream; use stream('${id}')`);
|
|
161
|
+
const { res, data, meta } = await this.#exchange(id, op, args[0]);
|
|
162
|
+
return {
|
|
163
|
+
data: data, meta, status: res.status, headers: res.headers,
|
|
164
|
+
requestId: res.headers.get('x-request-id'), mode: res.headers.get('rewloy-mode'),
|
|
165
|
+
replayed: res.headers.get('idempotent-replayed') === 'true',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Walks a paged list item by item, asking for the next page (`page`) while
|
|
170
|
+
* `meta` says there is one. `query.page` sets where to start and
|
|
171
|
+
* `query.limit` the page size.
|
|
172
|
+
*
|
|
173
|
+
* ```ts
|
|
174
|
+
* for await (const customer of rewloy.paginate('listCustomers', { query: { consent: 'yes' } })) { … }
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
paginate(id, ...args) {
|
|
178
|
+
const op = this.#operation(id);
|
|
179
|
+
if (!op.paged)
|
|
180
|
+
throw new TypeError(`Rewloy: ${id} is not a paged list`);
|
|
181
|
+
const base = (args[0] ?? {});
|
|
182
|
+
const exchange = (page) => this.#exchange(id, op, { ...base, query: { ...base.query, page } });
|
|
183
|
+
return (async function* () {
|
|
184
|
+
let page = Number(base.query?.page ?? 1);
|
|
185
|
+
for (;;) {
|
|
186
|
+
const { data, meta } = await exchange(page);
|
|
187
|
+
const items = (Array.isArray(data) ? data : []);
|
|
188
|
+
yield* items;
|
|
189
|
+
if (!meta || items.length === 0 || items.length < meta.pageSize || meta.page * meta.pageSize >= meta.total)
|
|
190
|
+
return;
|
|
191
|
+
page = meta.page + 1;
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Opens a server-sent event stream (`liveFeed`, `holderCardEvents`) and
|
|
197
|
+
* iterates its events. It reconnects by itself unless `reconnect: false`;
|
|
198
|
+
* stop it with `signal`, `break` or `close()`.
|
|
199
|
+
*
|
|
200
|
+
* ```ts
|
|
201
|
+
* for await (const ev of rewloy.stream('liveFeed', { signal })) {
|
|
202
|
+
* if (ev.event === 'event') console.log(JSON.parse(ev.data));
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
stream(id, ...args) {
|
|
207
|
+
return this.#open(id, args[0]);
|
|
208
|
+
}
|
|
209
|
+
async _call(id, args) {
|
|
210
|
+
const op = this.#operation(id);
|
|
211
|
+
const { data, meta } = await this.#exchange(id, op, args);
|
|
212
|
+
return (op.paged ? { data, meta } : data);
|
|
213
|
+
}
|
|
214
|
+
_open(id, args) {
|
|
215
|
+
return this.#open(id, args);
|
|
216
|
+
}
|
|
217
|
+
#open(id, args) {
|
|
218
|
+
const op = this.#operation(id);
|
|
219
|
+
if (!op.stream)
|
|
220
|
+
throw new TypeError(`Rewloy: ${id} is not a stream; use request('${id}')`);
|
|
221
|
+
const a = args ?? {};
|
|
222
|
+
return new EventStream({
|
|
223
|
+
operation: id,
|
|
224
|
+
connect: async (lastEventId, controller) => (await this.#exchange(id, op, a, { lastEventId, controller })).res,
|
|
225
|
+
signal: a.signal,
|
|
226
|
+
reconnect: a.reconnect ?? true,
|
|
227
|
+
idleTimeoutMs: a.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
228
|
+
sleep: this.#sleep,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
#operation(id) {
|
|
232
|
+
const op = Object.hasOwn(OPERATIONS, id) ? OPERATIONS[id] : undefined;
|
|
233
|
+
if (!op)
|
|
234
|
+
throw new TypeError(`Rewloy: unknown operation "${String(id)}"`);
|
|
235
|
+
return op;
|
|
236
|
+
}
|
|
237
|
+
#url(id, op, a) {
|
|
238
|
+
const path = op.path.replace(/\{([^}]+)\}/g, (_m, name) => {
|
|
239
|
+
const v = a.params?.[name];
|
|
240
|
+
if (v === undefined || v === null || v === '')
|
|
241
|
+
throw new TypeError(`Rewloy: ${id} needs params.${name}`);
|
|
242
|
+
return encodeURIComponent(String(v));
|
|
243
|
+
});
|
|
244
|
+
const query = new URLSearchParams();
|
|
245
|
+
for (const [k, v] of Object.entries(a.query ?? {})) {
|
|
246
|
+
if (v === undefined || v === null)
|
|
247
|
+
continue;
|
|
248
|
+
for (const x of Array.isArray(v) ? v : [v])
|
|
249
|
+
query.append(k, String(x));
|
|
250
|
+
}
|
|
251
|
+
const qs = query.toString();
|
|
252
|
+
return `${this.baseUrl}${path}${qs ? `?${qs}` : ''}`;
|
|
253
|
+
}
|
|
254
|
+
#headers(op, a, lastEventId) {
|
|
255
|
+
const h = new Headers();
|
|
256
|
+
h.set('accept', op.stream ? 'text/event-stream' : op.response === 'json' || op.response === 'raw-json' ? 'application/json' : '*/*');
|
|
257
|
+
if (this.#userAgent)
|
|
258
|
+
h.set('user-agent', this.#userAgent);
|
|
259
|
+
// An operation that takes no credential of this kind but works without one is called without it:
|
|
260
|
+
// the API refuses a credential an operation does not accept (CREDENTIAL_NOT_ALLOWED).
|
|
261
|
+
if (this.#token && this.credential && (op.auth.includes(this.credential) || !op.auth.includes('public'))) {
|
|
262
|
+
h.set('authorization', `Bearer ${this.#token}`);
|
|
263
|
+
}
|
|
264
|
+
const merchant = a.merchant ?? (this.credential === 'staff' ? this.merchant : null);
|
|
265
|
+
if (op.merchant && merchant)
|
|
266
|
+
h.set('rewloy-merchant', merchant);
|
|
267
|
+
if (op.idempotency)
|
|
268
|
+
h.set('idempotency-key', a.idempotencyKey ?? randomUUID());
|
|
269
|
+
if (op.body)
|
|
270
|
+
h.set('content-type', 'application/json');
|
|
271
|
+
if (lastEventId)
|
|
272
|
+
h.set('last-event-id', lastEventId);
|
|
273
|
+
for (const [k, v] of Object.entries(a.headers ?? {}))
|
|
274
|
+
if (v !== undefined && v !== null)
|
|
275
|
+
h.set(k, String(v));
|
|
276
|
+
return h;
|
|
277
|
+
}
|
|
278
|
+
#notice(id, op, headers) {
|
|
279
|
+
if (!headers.has('deprecation') || warned.has(id))
|
|
280
|
+
return;
|
|
281
|
+
warned.add(id);
|
|
282
|
+
const sunset = headers.get('sunset');
|
|
283
|
+
const link = deprecationLink(headers.get('link'));
|
|
284
|
+
warn(`Rewloy API operation ${id} (${op.method} ${op.path}) is deprecated.${sunset ? ` Sunset: ${sunset}.` : ''}${link ? ` See ${link}` : ''}`);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* One call: attempts until an answer settles it. For a stream it resolves
|
|
288
|
+
* once the headers are in, the body unread; otherwise with the body read.
|
|
289
|
+
*/
|
|
290
|
+
async #exchange(id, op, args, stream) {
|
|
291
|
+
const a = args ?? {};
|
|
292
|
+
const url = this.#url(id, op, a);
|
|
293
|
+
const headers = this.#headers(op, a, stream?.lastEventId);
|
|
294
|
+
const body = op.body ? JSON.stringify(a.body ?? {}) : undefined;
|
|
295
|
+
const retryable = IDEMPOTENT_METHODS.has(op.method) || headers.has('idempotency-key');
|
|
296
|
+
const maxRetries = Math.max(0, a.maxRetries ?? this.maxRetries);
|
|
297
|
+
const timeoutMs = a.timeoutMs ?? this.timeoutMs;
|
|
298
|
+
const signal = a.signal;
|
|
299
|
+
for (let attempt = 0;; attempt++) {
|
|
300
|
+
signal?.throwIfAborted();
|
|
301
|
+
const attemptCtrl = new AbortController();
|
|
302
|
+
let timedOut = false;
|
|
303
|
+
const timer = timeoutMs > 0 && timeoutMs < MAX_TIMER_MS ? setTimeout(() => { timedOut = true; attemptCtrl.abort(); }, timeoutMs) : undefined;
|
|
304
|
+
const signals = [attemptCtrl.signal, ...(signal ? [signal] : []), ...(stream ? [stream.controller.signal] : [])];
|
|
305
|
+
let failure;
|
|
306
|
+
let wait = null;
|
|
307
|
+
try {
|
|
308
|
+
const res = await this.#fetch(url, { method: op.method, headers, body, signal: AbortSignal.any(signals) });
|
|
309
|
+
this.#notice(id, op, res.headers);
|
|
310
|
+
if (res.ok) {
|
|
311
|
+
if (stream)
|
|
312
|
+
return { res, data: undefined, meta: undefined };
|
|
313
|
+
return { res, ...(await this.#read(id, op, res)) };
|
|
314
|
+
}
|
|
315
|
+
failure = this.#failure(id, res, await res.text().catch(() => ''));
|
|
316
|
+
const retryAfter = parseRetryAfter(res.headers.get('retry-after'));
|
|
317
|
+
if (!retryable || attempt >= maxRetries || !this.#retryStatus(res.status, failure.code))
|
|
318
|
+
throw failure;
|
|
319
|
+
wait = retryAfter;
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
if (err instanceof RewloyError)
|
|
323
|
+
throw err;
|
|
324
|
+
if (signal?.aborted)
|
|
325
|
+
throw signal.reason;
|
|
326
|
+
if (stream?.controller.signal.aborted)
|
|
327
|
+
throw err;
|
|
328
|
+
failure = timedOut
|
|
329
|
+
? new RewloyTimeoutError({ detail: `no answer within ${String(timeoutMs)} ms`, operation: id, cause: err })
|
|
330
|
+
: new RewloyConnectionError({ detail: describe(err), operation: id, cause: err });
|
|
331
|
+
if (!retryable || attempt >= maxRetries)
|
|
332
|
+
throw failure;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
// For a stream the timer ends with the headers; the body is the stream's own business.
|
|
336
|
+
clearTimeout(timer);
|
|
337
|
+
}
|
|
338
|
+
const delay = wait ?? backoff(attempt);
|
|
339
|
+
if (delay > MAX_RETRY_AFTER_MS)
|
|
340
|
+
throw failure;
|
|
341
|
+
await this.#sleep(delay, signal);
|
|
342
|
+
signal?.throwIfAborted();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
#retryStatus(status, code) {
|
|
346
|
+
return status === 429 || GATEWAY_STATUSES.has(status) || (status === 409 && code === 'IDEMPOTENCY_IN_PROGRESS');
|
|
347
|
+
}
|
|
348
|
+
async #read(id, op, res) {
|
|
349
|
+
if (op.response === 'none' || res.status === 204) {
|
|
350
|
+
await res.arrayBuffer().catch(() => undefined);
|
|
351
|
+
return { data: undefined, meta: undefined };
|
|
352
|
+
}
|
|
353
|
+
if (op.response === 'blob')
|
|
354
|
+
return { data: await res.blob(), meta: undefined };
|
|
355
|
+
const text = await res.text();
|
|
356
|
+
let parsed;
|
|
357
|
+
try {
|
|
358
|
+
parsed = JSON.parse(text);
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
throw this.#invalid(id, res, text);
|
|
362
|
+
}
|
|
363
|
+
if (op.response === 'raw-json')
|
|
364
|
+
return { data: parsed, meta: undefined };
|
|
365
|
+
if (!isRecord(parsed) || !('data' in parsed))
|
|
366
|
+
throw this.#invalid(id, res, parsed);
|
|
367
|
+
return { data: parsed.data, meta: isRecord(parsed.meta) ? parsed.meta : undefined };
|
|
368
|
+
}
|
|
369
|
+
#invalid(id, res, body) {
|
|
370
|
+
return new RewloyError({
|
|
371
|
+
status: res.status, code: 'INVALID_RESPONSE', detail: `the answer is not the JSON the API documents (${res.headers.get('content-type') ?? 'no content type'})`,
|
|
372
|
+
requestId: res.headers.get('x-request-id'), body, headers: res.headers, operation: id,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
#failure(id, res, text) {
|
|
376
|
+
let parsed = text;
|
|
377
|
+
try {
|
|
378
|
+
parsed = text ? JSON.parse(text) : null;
|
|
379
|
+
}
|
|
380
|
+
catch { /* not JSON: a proxy's page */ }
|
|
381
|
+
const e = isRecord(parsed) && isRecord(parsed.error) ? parsed.error : null;
|
|
382
|
+
const code = e && typeof e.code === 'string' ? e.code : `HTTP_${String(res.status)}`;
|
|
383
|
+
const init = {
|
|
384
|
+
status: res.status, code,
|
|
385
|
+
title: Object.hasOwn(ERROR_TITLES, code) ? ERROR_TITLES[code] : null,
|
|
386
|
+
detail: e && typeof e.message === 'string' ? e.message : res.statusText || `HTTP ${String(res.status)}`,
|
|
387
|
+
details: e?.details, docs: e && typeof e.docs === 'string' ? e.docs : null,
|
|
388
|
+
requestId: res.headers.get('x-request-id') ?? (e && typeof e.requestId === 'string' ? e.requestId : null),
|
|
389
|
+
body: parsed, headers: res.headers, operation: id,
|
|
390
|
+
};
|
|
391
|
+
if (res.status === 429) {
|
|
392
|
+
const header = parseRetryAfter(res.headers.get('retry-after'));
|
|
393
|
+
const fromBody = isRecord(init.details) && typeof init.details.retryAfterSec === 'number' ? init.details.retryAfterSec : null;
|
|
394
|
+
return new RateLimitError({ ...init, retryAfter: header !== null ? Math.ceil(header / 1000) : fromBody });
|
|
395
|
+
}
|
|
396
|
+
return new RewloyError(init);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function describe(err) {
|
|
400
|
+
if (!(err instanceof Error))
|
|
401
|
+
return String(err);
|
|
402
|
+
const cause = err.cause instanceof Error ? err.cause.message : undefined;
|
|
403
|
+
return cause ? `${err.message}: ${cause}` : err.message;
|
|
404
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the client throws. Every failure to get an answer from Rewloy is a
|
|
3
|
+
* RewloyError: an error answer from the API (with its stable `code`), an
|
|
4
|
+
* answer that is not what the API documents, or no answer at all.
|
|
5
|
+
*/
|
|
6
|
+
import type { ErrorCode } from './generated/types.js';
|
|
7
|
+
export interface RewloyErrorInit {
|
|
8
|
+
status: number;
|
|
9
|
+
code: string;
|
|
10
|
+
detail: string;
|
|
11
|
+
title?: string | null | undefined;
|
|
12
|
+
details?: unknown;
|
|
13
|
+
docs?: string | null | undefined;
|
|
14
|
+
requestId?: string | null | undefined;
|
|
15
|
+
body?: unknown;
|
|
16
|
+
headers?: Headers | null | undefined;
|
|
17
|
+
operation?: string | null | undefined;
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The API answered with an error, or the call failed on the way.
|
|
22
|
+
*
|
|
23
|
+
* Act on `code`: it is stable, while `detail` is a human sentence in Turkish
|
|
24
|
+
* that may change. Besides the API's codes (https://rewloy.com/gelistiriciler/hatalar)
|
|
25
|
+
* the client uses:
|
|
26
|
+
* - `CONNECTION_ERROR` and `TIMEOUT` (status 0): no answer arrived;
|
|
27
|
+
* - `INVALID_RESPONSE`: a 2xx answer that is not the documented JSON;
|
|
28
|
+
* - `HTTP_<status>`: an error answer without Rewloy's error body (a proxy's 502 page).
|
|
29
|
+
*/
|
|
30
|
+
export declare class RewloyError extends Error {
|
|
31
|
+
/** The HTTP status; 0 when no answer arrived. */
|
|
32
|
+
readonly status: number;
|
|
33
|
+
/** The API's stable machine code, e.g. `INSUFFICIENT_BALANCE`. */
|
|
34
|
+
readonly code: ErrorCode | (string & {});
|
|
35
|
+
/** The code's one-line title in the catalogue, e.g. "Bakiye yetersiz". */
|
|
36
|
+
readonly title: string | null;
|
|
37
|
+
/** What happened, in the API's words (`error.message`). */
|
|
38
|
+
readonly detail: string;
|
|
39
|
+
/**
|
|
40
|
+
* The API's `error.details`, when it sent any: for `VALIDATION` a list of
|
|
41
|
+
* `{ field, rule, message }`, for others what the catalogue says (`left`,
|
|
42
|
+
* `channels`, `request`…).
|
|
43
|
+
*/
|
|
44
|
+
readonly details: unknown;
|
|
45
|
+
/** Where the catalogue explains the code (`error.docs`). */
|
|
46
|
+
readonly docs: string | null;
|
|
47
|
+
/** `x-request-id`: quote it to Rewloy support. */
|
|
48
|
+
readonly requestId: string | null;
|
|
49
|
+
/** The parsed answer body (or its text, when it is not JSON). */
|
|
50
|
+
readonly body: unknown;
|
|
51
|
+
readonly headers: Headers | null;
|
|
52
|
+
/** The operationId of the call. */
|
|
53
|
+
readonly operation: string | null;
|
|
54
|
+
constructor(init: RewloyErrorInit);
|
|
55
|
+
}
|
|
56
|
+
/** 429 `RATE_LIMITED`: too many requests for this credential or this action. */
|
|
57
|
+
export declare class RateLimitError extends RewloyError {
|
|
58
|
+
/** Seconds to wait before trying again (`Retry-After`), when the API said. */
|
|
59
|
+
readonly retryAfter: number | null;
|
|
60
|
+
constructor(init: RewloyErrorInit & {
|
|
61
|
+
retryAfter: number | null;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** No answer arrived: the connection failed or broke (`CONNECTION_ERROR`). */
|
|
65
|
+
export declare class RewloyConnectionError extends RewloyError {
|
|
66
|
+
constructor(init: Omit<RewloyErrorInit, 'status' | 'code'> & {
|
|
67
|
+
code?: string;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** No answer within `timeoutMs` (`TIMEOUT`), or a stream fell silent. */
|
|
71
|
+
export declare class RewloyTimeoutError extends RewloyConnectionError {
|
|
72
|
+
constructor(init: Omit<RewloyErrorInit, 'status' | 'code'>);
|
|
73
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the client throws. Every failure to get an answer from Rewloy is a
|
|
3
|
+
* RewloyError: an error answer from the API (with its stable `code`), an
|
|
4
|
+
* answer that is not what the API documents, or no answer at all.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The API answered with an error, or the call failed on the way.
|
|
8
|
+
*
|
|
9
|
+
* Act on `code`: it is stable, while `detail` is a human sentence in Turkish
|
|
10
|
+
* that may change. Besides the API's codes (https://rewloy.com/gelistiriciler/hatalar)
|
|
11
|
+
* the client uses:
|
|
12
|
+
* - `CONNECTION_ERROR` and `TIMEOUT` (status 0): no answer arrived;
|
|
13
|
+
* - `INVALID_RESPONSE`: a 2xx answer that is not the documented JSON;
|
|
14
|
+
* - `HTTP_<status>`: an error answer without Rewloy's error body (a proxy's 502 page).
|
|
15
|
+
*/
|
|
16
|
+
export class RewloyError extends Error {
|
|
17
|
+
/** The HTTP status; 0 when no answer arrived. */
|
|
18
|
+
status;
|
|
19
|
+
/** The API's stable machine code, e.g. `INSUFFICIENT_BALANCE`. */
|
|
20
|
+
code;
|
|
21
|
+
/** The code's one-line title in the catalogue, e.g. "Bakiye yetersiz". */
|
|
22
|
+
title;
|
|
23
|
+
/** What happened, in the API's words (`error.message`). */
|
|
24
|
+
detail;
|
|
25
|
+
/**
|
|
26
|
+
* The API's `error.details`, when it sent any: for `VALIDATION` a list of
|
|
27
|
+
* `{ field, rule, message }`, for others what the catalogue says (`left`,
|
|
28
|
+
* `channels`, `request`…).
|
|
29
|
+
*/
|
|
30
|
+
details;
|
|
31
|
+
/** Where the catalogue explains the code (`error.docs`). */
|
|
32
|
+
docs;
|
|
33
|
+
/** `x-request-id`: quote it to Rewloy support. */
|
|
34
|
+
requestId;
|
|
35
|
+
/** The parsed answer body (or its text, when it is not JSON). */
|
|
36
|
+
body;
|
|
37
|
+
headers;
|
|
38
|
+
/** The operationId of the call. */
|
|
39
|
+
operation;
|
|
40
|
+
constructor(init) {
|
|
41
|
+
const where = [init.operation, init.requestId ? `requestId ${init.requestId}` : null].filter(Boolean).join(', ');
|
|
42
|
+
super(`${init.status ? `${String(init.status)} ` : ''}${init.code}: ${init.detail}${where ? ` (${where})` : ''}`, init.cause === undefined ? undefined : { cause: init.cause });
|
|
43
|
+
this.status = init.status;
|
|
44
|
+
this.code = init.code;
|
|
45
|
+
this.title = init.title ?? null;
|
|
46
|
+
this.detail = init.detail;
|
|
47
|
+
this.details = init.details;
|
|
48
|
+
this.docs = init.docs ?? null;
|
|
49
|
+
this.requestId = init.requestId ?? null;
|
|
50
|
+
this.body = init.body;
|
|
51
|
+
this.headers = init.headers ?? null;
|
|
52
|
+
this.operation = init.operation ?? null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 429 `RATE_LIMITED`: too many requests for this credential or this action. */
|
|
56
|
+
export class RateLimitError extends RewloyError {
|
|
57
|
+
/** Seconds to wait before trying again (`Retry-After`), when the API said. */
|
|
58
|
+
retryAfter;
|
|
59
|
+
constructor(init) {
|
|
60
|
+
super(init);
|
|
61
|
+
this.retryAfter = init.retryAfter;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** No answer arrived: the connection failed or broke (`CONNECTION_ERROR`). */
|
|
65
|
+
export class RewloyConnectionError extends RewloyError {
|
|
66
|
+
constructor(init) {
|
|
67
|
+
super({ ...init, status: 0, code: init.code ?? 'CONNECTION_ERROR' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** No answer within `timeoutMs` (`TIMEOUT`), or a stream fell silent. */
|
|
71
|
+
export class RewloyTimeoutError extends RewloyConnectionError {
|
|
72
|
+
constructor(init) {
|
|
73
|
+
super({ ...init, code: 'TIMEOUT' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// On the prototypes, not as fields: the stack trace's first line is written
|
|
77
|
+
// while the base constructor runs, before a field would be set.
|
|
78
|
+
const names = [[RewloyError, 'RewloyError'], [RateLimitError, 'RateLimitError'],
|
|
79
|
+
[RewloyConnectionError, 'RewloyConnectionError'], [RewloyTimeoutError, 'RewloyTimeoutError']];
|
|
80
|
+
for (const [E, name] of names)
|
|
81
|
+
Object.defineProperty(E.prototype, 'name', { value: name, writable: true, configurable: true });
|