@rewloy/node 0.2.0 → 0.2.2
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 +57 -0
- package/README.md +103 -14
- package/dist/client.d.ts +14 -1
- package/dist/client.js +48 -11
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +3 -0
- package/dist/generated/methods.d.ts +58 -34
- package/dist/generated/methods.js +61 -35
- package/dist/generated/operations.d.ts +1 -1
- package/dist/generated/operations.js +6 -2
- package/dist/generated/types.d.ts +193 -104
- package/dist/generated/types.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,63 @@ https://rewloy.com/gelistiriciler/degisiklikler
|
|
|
5
5
|
|
|
6
6
|
This library's releases. The API's own changes are listed at the link above.
|
|
7
7
|
|
|
8
|
+
## 0.2.2 (2026-10-05)
|
|
9
|
+
|
|
10
|
+
Rewloy 1.1.0'a (API sürümü) göre yeniden üretildi: 256 işlem (0.2.1'de 255). Kasa
|
|
11
|
+
için `reverseAction`, `recordSale`'de `occurredAt`, `passAction`'da `reference`;
|
|
12
|
+
yanıtlarda `RateLimit-*` başlıkları.
|
|
13
|
+
|
|
14
|
+
Regenerated from Rewloy 1.1.0 (the product version in `info.version`): 256
|
|
15
|
+
operations (255 in 0.2.1).
|
|
16
|
+
|
|
17
|
+
- **New operation: `reverseAction`** (`POST /v1/passes/{serial}/actions/reverse`).
|
|
18
|
+
Voids a till action made with `passAction` (`spend`, `spend-points`,
|
|
19
|
+
`redeem-stamps`, `redeem-reward`, `use`), found by its `actionKey` (the
|
|
20
|
+
`Idempotency-Key` it was sent with) or its `reference`. It needs no
|
|
21
|
+
`Idempotency-Key`: an action is voided once and a repeat answers
|
|
22
|
+
`duplicate: true`. New error codes `ACTION_NOT_FOUND`, `ACTION_AMBIGUOUS`,
|
|
23
|
+
`ACTION_NOT_REVERSIBLE`.
|
|
24
|
+
- **`recordSale` takes an optional `occurredAt`**: when the sale really happened
|
|
25
|
+
(ISO 8601 with offset), for a till that queues sales while offline.
|
|
26
|
+
- **`passAction` takes an optional `reference`**, and its answer is now a union
|
|
27
|
+
type: the balance-card answer (`balance`, `detail`, `promotion`) or the coupon /
|
|
28
|
+
discount-card answer (`status`, `uses`, `usesLeft`). Narrow with `'uses' in r`.
|
|
29
|
+
- **Rate limit headers.** `ApiResponse.rateLimit` (`{ limit, remaining, reset }`,
|
|
30
|
+
from `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`; `null` when the
|
|
31
|
+
answer has none) and `RewloyError.rateLimit` (including `RateLimitError`).
|
|
32
|
+
`parseRateLimit(headers)` is exported. Additive.
|
|
33
|
+
- Webhook-creation responses may carry `warnings` (a non-live installation whose
|
|
34
|
+
URL production would refuse); the `Idempotency-Key` parameter documents its
|
|
35
|
+
8–64 printable ASCII rule; the API's descriptions no longer contain internal
|
|
36
|
+
`ADR n` references. README: the till example has a void step and a note on
|
|
37
|
+
`occurredAt` for offline queues.
|
|
38
|
+
|
|
39
|
+
## 0.2.1 (2026-10-05)
|
|
40
|
+
|
|
41
|
+
Dışarıdan geliştiricilerin bulduğu üç sorun düzeltildi.
|
|
42
|
+
|
|
43
|
+
Three problems found by outside developers, fixed.
|
|
44
|
+
|
|
45
|
+
- **`Idempotency-Key` is checked before sending.** A key with non-ASCII
|
|
46
|
+
characters (`fiş-0042`) made `fetch` throw a bare `TypeError` about the header
|
|
47
|
+
value. Now the client refuses any key that is not printable ASCII
|
|
48
|
+
(0x21–0x7E), 8–64 characters, with a clear `TypeError` ("Idempotency-Key
|
|
49
|
+
yalnız ASCII karakterler içerebilir …") and sends nothing. The API will also
|
|
50
|
+
answer `400 VALIDATION` for such a key in its next release.
|
|
51
|
+
- **`baseUrl` takes the address with or without `/v1`.** The documentation and
|
|
52
|
+
the OpenAPI document show `https://app.rewloy.com/v1`; the client wanted the
|
|
53
|
+
origin only. Now both work; a trailing `/v1` or `/v1/` and trailing slashes
|
|
54
|
+
are stripped (`rewloy.baseUrl` is the origin).
|
|
55
|
+
- **`idempotencyKey` is required where the API requires it.** For `recordSale`,
|
|
56
|
+
`passAction`, `sendCampaign` and `refundShopRedemption` the OpenAPI document
|
|
57
|
+
marks the header required, but the client made up a random UUID when it was
|
|
58
|
+
missing, which does not survive a restart of your app. `idempotencyKey` is now
|
|
59
|
+
a required argument of those methods (a type error in TypeScript, a
|
|
60
|
+
`TypeError` before sending in JavaScript). Where the header is optional
|
|
61
|
+
(`issuePass`, …) a UUID is still generated and reused on every retry.
|
|
62
|
+
**Breaking for callers that relied on the generated key** (a small break,
|
|
63
|
+
taken in a patch release because the old behaviour could write a sale twice).
|
|
64
|
+
|
|
8
65
|
## 0.2.0 (2026-10-05)
|
|
9
66
|
|
|
10
67
|
Rewloy API 1.0.5'e göre yeniden üretildi: 255 işlem (0.1.0'da 237). Kasa için
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ işlemin gerektirdikleriyle:
|
|
|
52
52
|
- `query`: sorgu parametreleri;
|
|
53
53
|
- `body`: JSON gövde;
|
|
54
54
|
- `merchant`: `Rewloy-Merchant` başlığı;
|
|
55
|
-
- `idempotencyKey`: `Idempotency-Key` başlığı (satış, kasa işlemi ve
|
|
55
|
+
- `idempotencyKey`: `Idempotency-Key` başlığı (satış, kasa işlemi, kampanya ve mağaza iadesinde zorunlu);
|
|
56
56
|
- `signal`, `timeoutMs`, `maxRetries`.
|
|
57
57
|
|
|
58
58
|
Metot yanıttaki `data`yı döndürür. Sayfalı listelerde `{ data, meta }`,
|
|
@@ -86,7 +86,7 @@ Bir işlem istemcinin kimlik türünü kabul etmiyor ama kimliksiz de çalışı
|
|
|
86
86
|
bir kimliği reddeder (`CREDENTIAL_NOT_ALLOWED`).
|
|
87
87
|
|
|
88
88
|
Diğer seçenekler:
|
|
89
|
-
- `baseUrl` (varsayılan `https://app.rewloy.com`; `/v1`
|
|
89
|
+
- `baseUrl` (varsayılan `https://app.rewloy.com`; sonuna `/v1` eklemeniz ya da eklememeniz fark etmez: `https://app.rewloy.com/v1` de olur, kütüphane `/v1`i kendisi ekler);
|
|
90
90
|
- `timeoutMs` (60000);
|
|
91
91
|
- `maxRetries` (2);
|
|
92
92
|
- `fetch`: kendi `fetch`iniz;
|
|
@@ -100,7 +100,7 @@ API'nin başka bir kopyasına (kendi staging ortamınız ya da bir vekil sunucu)
|
|
|
100
100
|
```ts
|
|
101
101
|
const rewloy = new Rewloy({
|
|
102
102
|
apiKey: process.env.REWLOY_API_KEY!,
|
|
103
|
-
baseUrl: 'https://rewloy-staging.ornek.com', // /v1
|
|
103
|
+
baseUrl: 'https://rewloy-staging.ornek.com', // sonuna /v1 yazsanız da olur
|
|
104
104
|
});
|
|
105
105
|
```
|
|
106
106
|
|
|
@@ -120,6 +120,15 @@ const sonuc = await rewloy.passAction({
|
|
|
120
120
|
idempotencyKey: `kasa3-z0187-fis${fisNo}`, // aşağıya bakın
|
|
121
121
|
});
|
|
122
122
|
if (sonuc.duplicate) console.log('Bu işlem zaten yazılmış');
|
|
123
|
+
|
|
124
|
+
// Yanlışlıkla bir harcama mı yapıldı? Yaparken gönderdiğiniz anahtarla geri alın:
|
|
125
|
+
await rewloy.passAction({
|
|
126
|
+
params: { serial },
|
|
127
|
+
body: { action: 'spend', locationId, amountMinor: 2500 },
|
|
128
|
+
idempotencyKey: `kasa3-z0187-iptal${fisNo}`,
|
|
129
|
+
});
|
|
130
|
+
const iptal = await rewloy.reverseAction({ params: { serial }, body: { actionKey: `kasa3-z0187-iptal${fisNo}` } });
|
|
131
|
+
console.log(iptal.undone, iptal.restored, iptal.balance); // 'spend', 2500, kartın bakiyesi
|
|
123
132
|
```
|
|
124
133
|
|
|
125
134
|
### Satış: `recordSale`
|
|
@@ -173,13 +182,62 @@ Bir satış bir kez geri alınır (tekrar `duplicate: true` döner). Kazanılan
|
|
|
173
182
|
kullanılmışsa (ödüle ya da harcamaya gitmişse) `409 SALE_ALREADY_SPENT` gelir ve
|
|
174
183
|
hiçbir şey yazılmaz.
|
|
175
184
|
|
|
185
|
+
**Çevrimdışı kasa kuyruğu: `occurredAt`.** Bağlantı koptuğunda satışı sonra
|
|
186
|
+
yazıyorsanız `occurredAt` ile satışın gerçekten olduğu anı (ISO 8601, saat
|
|
187
|
+
dilimiyle) gönderin; geçmişte, kartın geçmişinde o anla görünür. Gelecekte
|
|
188
|
+
olamaz (2 dakikalık saat farkı kabul edilir). `idempotencyKey` kuyruktaki
|
|
189
|
+
kayıtla birlikte saklanır, tekrar gönderilince satış ikinci kez yazılmaz.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
await rewloy.recordSale({
|
|
193
|
+
params: { serial },
|
|
194
|
+
body: { locationId, amountMinor: 4550, reference: `fis-${fisNo}`, occurredAt: '2026-10-05T14:32:10+03:00' },
|
|
195
|
+
idempotencyKey: anahtar,
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Kasa işlemini iptal etmek: `reverseAction`.** `passAction` ile yapılan bir
|
|
200
|
+
harcama, ödül ya da kullanım yanlışlıkla yapıldıysa (`spend`, `spend-points`,
|
|
201
|
+
`redeem-stamps`, `redeem-reward`, `use`) `reverseAction` tamamını geri verir.
|
|
202
|
+
İşlemi, yaparken gönderdiğiniz `Idempotency-Key` (`actionKey`) ya da işlemin
|
|
203
|
+
`reference` değeriyle bulur (`passAction` artık isteğe bağlı bir `reference`
|
|
204
|
+
alır). `reverseAction` bir `Idempotency-Key` **istemez**: bir işlem bir kez geri
|
|
205
|
+
alınır, tekrar `duplicate: true` döner.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const geri = await rewloy.reverseAction({
|
|
209
|
+
params: { serial },
|
|
210
|
+
body: { actionKey: `kasa3-z0187-fis${fisNo}`, locationId }, // ya da { reference: `fis-${fisNo}` }
|
|
211
|
+
});
|
|
212
|
+
console.log(geri.undone, geri.restored, geri.balance, geri.reopened, geri.duplicate);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
`passAction`ın yanıtı kart türüne göre iki biçimdedir ve TypeScript'te bir
|
|
216
|
+
birleşim türüdür: bakiyeli kartlarda `balance` (damga, puan, VIP, cashback,
|
|
217
|
+
hediye kartı), kupon ve indirim kartında `status`, `uses` ve `usesLeft`
|
|
218
|
+
(`'uses' in sonuc` ile ayırın). Kazanımlar (`earn-stamps`, `earn-points`,
|
|
219
|
+
`visit`) `reverseAction`la değil `reverseSale`la geri alınır.
|
|
220
|
+
|
|
221
|
+
**İstek sınırı.** Kimlikli her yanıt `RateLimit-Limit`, `RateLimit-Remaining` ve
|
|
222
|
+
`RateLimit-Reset` başlıklarını taşır: `rewloy.request(...)` bunları
|
|
223
|
+
`res.rateLimit` (`{ limit, remaining, reset }`) olarak verir, `429` hatası da
|
|
224
|
+
(`RateLimitError`) `err.rateLimit` ve `err.retryAfter` taşır.
|
|
225
|
+
|
|
176
226
|
### `Idempotency-Key`
|
|
177
227
|
|
|
178
|
-
`recordSale`, `passAction
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
228
|
+
`recordSale`, `passAction`, `sendCampaign` ve `refundShopRedemption` bir
|
|
229
|
+
`Idempotency-Key` **ister**: API'nin tanımında (OpenAPI) bu başlık bu işlemlerde
|
|
230
|
+
zorunludur, bu yüzden `idempotencyKey` bu metotlarda zorunlu bir argümandır.
|
|
231
|
+
Verilmezse kütüphane istek göndermeden `TypeError` fırlatır; **sizin yerinize
|
|
232
|
+
anahtar üretmez**. Üretilmiş rastgele bir anahtar yalnızca tek çağrının yeniden
|
|
233
|
+
denemelerini korurdu: uygulama çöküp yeniden başlarsa yeni bir anahtar çıkar ve
|
|
234
|
+
satış ikinci kez yazılabilirdi. Anahtarı kendiniz üretip satışla birlikte
|
|
235
|
+
saklayın. Anahtar 8–64 karakterlik görünür ASCII olmalıdır (0x21–0x7E: harf,
|
|
236
|
+
rakam ve noktalama; boşluk, Türkçe harf ya da `fiş` gibi ASCII dışı karakter
|
|
237
|
+
olmaz); aksi halde kütüphane yine istek göndermeden `TypeError` fırlatır.
|
|
238
|
+
Başlığın isteğe bağlı olduğu işlemlerde (örneğin `issuePass`) anahtar verilmezse
|
|
239
|
+
kütüphane bir UUID üretir ve aynı çağrının her denemesinde aynısını gönderir.
|
|
240
|
+
|
|
183
241
|
- **Anahtar bir kimlik için kalıcı olarak tekildir** (8–64 karakter; defterden
|
|
184
242
|
hiç silinmez). Aynı anahtarla aynı isteğin tekrarı ikinci kez yazmaz ve
|
|
185
243
|
ilk sonucu `duplicate: true` ile döndürür. Aynı anahtar başka bir gövdeyle
|
|
@@ -515,6 +573,15 @@ const sale = await rewloy.recordSale({
|
|
|
515
573
|
body: { locationId, amountMinor: 4550, reference: `receipt-${receiptNo}` }, // amount in the card's currency, minor units
|
|
516
574
|
idempotencyKey: `till3-z0187-r${receiptNo}`,
|
|
517
575
|
});
|
|
576
|
+
|
|
577
|
+
// A gift-card spend rung up by mistake? Void it by the key it was sent with:
|
|
578
|
+
await rewloy.passAction({
|
|
579
|
+
params: { serial },
|
|
580
|
+
body: { action: 'spend', locationId, amountMinor: 2500 },
|
|
581
|
+
idempotencyKey: `till3-z0187-s${receiptNo}`,
|
|
582
|
+
});
|
|
583
|
+
const voided = await rewloy.reverseAction({ params: { serial }, body: { actionKey: `till3-z0187-s${receiptNo}` } });
|
|
584
|
+
console.log(voided.undone, voided.restored, voided.balance); // 'spend', 2500, the balance again
|
|
518
585
|
```
|
|
519
586
|
|
|
520
587
|
- **Till.** `recordSale` writes a completed sale to a card (the card type and
|
|
@@ -522,14 +589,34 @@ const sale = await rewloy.recordSale({
|
|
|
522
589
|
card's structured fields (`programName`, `currency`, `stamps`, `points`,
|
|
523
590
|
`money`, `customer`); `reverseSale` takes a refunded sale back:
|
|
524
591
|
`rewloy.reverseSale({ params: { serial }, body: { saleKey: key } })`.
|
|
525
|
-
|
|
526
|
-
`
|
|
592
|
+
A void is `reverseAction`: it takes back a `passAction` that was a mistake
|
|
593
|
+
(`spend`, `spend-points`, `redeem-stamps`, `redeem-reward`, `use`), found by
|
|
594
|
+
the `Idempotency-Key` you sent with it (`actionKey`) or its `reference`; it
|
|
595
|
+
needs no `Idempotency-Key` of its own, and a repeat answers `duplicate: true`:
|
|
596
|
+
`rewloy.reverseAction({ params: { serial }, body: { actionKey: key } })`.
|
|
597
|
+
A till that queues sales while offline sends `occurredAt` (ISO 8601 with the
|
|
598
|
+
UTC offset, not in the future) with `recordSale`, so the card's history shows
|
|
599
|
+
when the sale really happened; the queued `idempotencyKey` makes the resend
|
|
600
|
+
safe. `passAction` takes an optional `reference` too, and its answer is a union:
|
|
601
|
+
the balance-card answer (`balance`) or the coupon / discount-card answer
|
|
602
|
+
(`status`, `uses`, `usesLeft`).
|
|
603
|
+
- **Idempotency keys.** `recordSale`, `passAction`, `sendCampaign` and
|
|
604
|
+
`refundShopRedemption` need an `Idempotency-Key`: the API's OpenAPI document
|
|
605
|
+
marks the header required for them, so `idempotencyKey` is a required
|
|
606
|
+
argument and the client throws a `TypeError` before sending if it is missing.
|
|
607
|
+
It never makes one up for you (a generated key would not survive a restart of
|
|
608
|
+
your app). The key must be 8–64 printable ASCII characters (0x21–0x7E); a
|
|
609
|
+
non-ASCII key such as `fiş-0042` is refused client-side, with a `TypeError`,
|
|
610
|
+
before anything is sent. Where the header is optional (for example
|
|
611
|
+
`issuePass`) the client still generates a UUID and reuses it on every retry
|
|
612
|
+
of the call. A key is unique **for good per credential**: do not use the
|
|
527
613
|
receipt number alone (fiscal receipt numbers restart after the Z report) but
|
|
528
614
|
register + Z number + receipt number, or a UUID stored with the sale. The
|
|
529
|
-
receipt number goes in `reference`.
|
|
530
|
-
of one call, not a restart of your app.
|
|
615
|
+
receipt number goes in `reference`.
|
|
531
616
|
- **Base URL.** `new Rewloy({ apiKey, baseUrl: 'https://staging.example.com' })`
|
|
532
|
-
|
|
617
|
+
or `baseUrl: 'https://staging.example.com/v1'`: with or without a trailing
|
|
618
|
+
`/v1` (and trailing slashes), the client appends `/v1/...` itself. Default
|
|
619
|
+
`https://app.rewloy.com`.
|
|
533
620
|
- **Test mode.** Open the test environment (panel → Developer, or
|
|
534
621
|
`POST /v1/test/environment`) and use its `rwk_test_` key at the same address:
|
|
535
622
|
a separate test business that sends nothing and never reaches real
|
|
@@ -540,7 +627,9 @@ const sale = await rewloy.recordSale({
|
|
|
540
627
|
- **Results.** It resolves to the answer's `data`: `{ data, meta }` for paged
|
|
541
628
|
lists, `undefined` for 204, a `Blob` for files.
|
|
542
629
|
- **The whole answer.** `rewloy.request(id, args)` returns `status`,
|
|
543
|
-
`headers`, `requestId`, `
|
|
630
|
+
`headers`, `requestId`, `rateLimit` (`{ limit, remaining, reset }` from the
|
|
631
|
+
`RateLimit-*` headers, `null` when absent), `mode` (the `Rewloy-Mode` header: `live` or `test`) and `replayed` (`Idempotent-Replayed`).
|
|
632
|
+
A `RewloyError` carries `rateLimit` too; a `RateLimitError` also has `retryAfter`.
|
|
544
633
|
- **Pagination.** `rewloy.paginate('listCustomers', args)` iterates the items
|
|
545
634
|
of every page.
|
|
546
635
|
- **Streams.** `rewloy.liveFeed({ signal })` (or `rewloy.stream('liveFeed',
|
package/dist/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { RewloyMethods } from './generated/methods.js';
|
|
7
7
|
import type { OperationId, Operations, PagedOperationId, StreamOperationId } from './generated/types.js';
|
|
8
8
|
import { EventStream } from './sse.js';
|
|
9
|
-
import type { ApiResponse, AuthKind } from './types.js';
|
|
9
|
+
import type { ApiResponse, AuthKind, RateLimitInfo } from './types.js';
|
|
10
10
|
export declare const DEFAULT_BASE_URL = "https://app.rewloy.com";
|
|
11
11
|
type Credential = Exclude<AuthKind, 'public'>;
|
|
12
12
|
type Sleep = (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
@@ -58,8 +58,21 @@ type ArgsParam<A> = object extends A ? [args?: A] : [args: A];
|
|
|
58
58
|
type ItemOf<K extends PagedOperationId> = Operations[K]['data'] extends (infer T)[] ? T : never;
|
|
59
59
|
/** `Retry-After` in milliseconds: delta-seconds or an HTTP date. */
|
|
60
60
|
export declare function parseRetryAfter(value: string | null, now?: number): number | null;
|
|
61
|
+
/** The `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers; `null` unless all three are numbers. */
|
|
62
|
+
export declare function parseRateLimit(headers: Headers | null | undefined): RateLimitInfo | null;
|
|
61
63
|
/** Exponential backoff with jitter for the retry after attempt `attempt` (0-based). */
|
|
62
64
|
export declare function backoff(attempt: number, random?: () => number): number;
|
|
65
|
+
/**
|
|
66
|
+
* The base URL without trailing slashes and without a trailing `/v1`: the
|
|
67
|
+
* paths of the operations carry `/v1` themselves, and the documentation shows
|
|
68
|
+
* the address both ways (`https://app.rewloy.com` and `https://app.rewloy.com/v1`).
|
|
69
|
+
*/
|
|
70
|
+
export declare function normalizeBaseUrl(url: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* An `Idempotency-Key` is 8–64 printable ASCII characters (0x21–0x7E): an HTTP
|
|
73
|
+
* header value cannot carry anything else, and `fetch` would throw a bare TypeError.
|
|
74
|
+
*/
|
|
75
|
+
export declare function checkIdempotencyKey(key: unknown): string;
|
|
63
76
|
/**
|
|
64
77
|
* A client of the Rewloy API (`https://app.rewloy.com/v1`).
|
|
65
78
|
*
|
package/dist/client.js
CHANGED
|
@@ -51,11 +51,42 @@ export function parseRetryAfter(value, now = Date.now()) {
|
|
|
51
51
|
const at = Date.parse(v);
|
|
52
52
|
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
53
53
|
}
|
|
54
|
+
/** The `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers; `null` unless all three are numbers. */
|
|
55
|
+
export function parseRateLimit(headers) {
|
|
56
|
+
if (!headers)
|
|
57
|
+
return null;
|
|
58
|
+
const read = (name) => {
|
|
59
|
+
const v = headers.get(name)?.trim();
|
|
60
|
+
return v && /^\d+$/.test(v) ? Number(v) : null;
|
|
61
|
+
};
|
|
62
|
+
const limit = read('ratelimit-limit');
|
|
63
|
+
const remaining = read('ratelimit-remaining');
|
|
64
|
+
const reset = read('ratelimit-reset');
|
|
65
|
+
return limit === null || remaining === null || reset === null ? null : { limit, remaining, reset };
|
|
66
|
+
}
|
|
54
67
|
/** Exponential backoff with jitter for the retry after attempt `attempt` (0-based). */
|
|
55
68
|
export function backoff(attempt, random = Math.random) {
|
|
56
69
|
const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
57
70
|
return Math.round(cap / 2 + random() * (cap / 2));
|
|
58
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* The base URL without trailing slashes and without a trailing `/v1`: the
|
|
74
|
+
* paths of the operations carry `/v1` themselves, and the documentation shows
|
|
75
|
+
* the address both ways (`https://app.rewloy.com` and `https://app.rewloy.com/v1`).
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeBaseUrl(url) {
|
|
78
|
+
return url.replace(/\/+$/, '').replace(/\/v1$/, '').replace(/\/+$/, '');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* An `Idempotency-Key` is 8–64 printable ASCII characters (0x21–0x7E): an HTTP
|
|
82
|
+
* header value cannot carry anything else, and `fetch` would throw a bare TypeError.
|
|
83
|
+
*/
|
|
84
|
+
export function checkIdempotencyKey(key) {
|
|
85
|
+
if (typeof key !== 'string' || !/^[\x21-\x7e]{8,64}$/.test(key)) {
|
|
86
|
+
throw new TypeError('Rewloy: Idempotency-Key yalnız ASCII karakterler içerebilir (görünür karakterler, 8–64) / the Idempotency-Key must be printable ASCII (0x21–0x7E), 8–64 characters');
|
|
87
|
+
}
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
59
90
|
/** The URL a `Link` header gives for `rel="deprecation"` (else its first). */
|
|
60
91
|
function deprecationLink(link) {
|
|
61
92
|
if (!link)
|
|
@@ -135,7 +166,7 @@ export class Rewloy extends RewloyMethods {
|
|
|
135
166
|
if (options.merchant !== undefined && which !== 'staffSession')
|
|
136
167
|
throw new TypeError('Rewloy: `merchant` goes with a staffSession');
|
|
137
168
|
this.merchant = options.merchant ?? null;
|
|
138
|
-
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL)
|
|
169
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
139
170
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
140
171
|
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
141
172
|
const f = options.fetch ?? globalThis.fetch;
|
|
@@ -161,7 +192,7 @@ export class Rewloy extends RewloyMethods {
|
|
|
161
192
|
const { res, data, meta } = await this.#exchange(id, op, args[0]);
|
|
162
193
|
return {
|
|
163
194
|
data: data, meta, status: res.status, headers: res.headers,
|
|
164
|
-
requestId: res.headers.get('x-request-id'), mode: res.headers.get('rewloy-mode'),
|
|
195
|
+
requestId: res.headers.get('x-request-id'), rateLimit: parseRateLimit(res.headers), mode: res.headers.get('rewloy-mode'),
|
|
165
196
|
replayed: res.headers.get('idempotent-replayed') === 'true',
|
|
166
197
|
};
|
|
167
198
|
}
|
|
@@ -251,7 +282,7 @@ export class Rewloy extends RewloyMethods {
|
|
|
251
282
|
const qs = query.toString();
|
|
252
283
|
return `${this.baseUrl}${path}${qs ? `?${qs}` : ''}`;
|
|
253
284
|
}
|
|
254
|
-
#headers(op, a, lastEventId) {
|
|
285
|
+
#headers(id, op, a, lastEventId) {
|
|
255
286
|
const h = new Headers();
|
|
256
287
|
h.set('accept', op.stream ? 'text/event-stream' : op.response === 'json' || op.response === 'raw-json' ? 'application/json' : '*/*');
|
|
257
288
|
if (this.#userAgent)
|
|
@@ -264,15 +295,21 @@ export class Rewloy extends RewloyMethods {
|
|
|
264
295
|
const merchant = a.merchant ?? (this.credential === 'staff' ? this.merchant : null);
|
|
265
296
|
if (op.merchant && merchant)
|
|
266
297
|
h.set('rewloy-merchant', merchant);
|
|
267
|
-
if (op.idempotency)
|
|
268
|
-
|
|
298
|
+
if (op.idempotency) {
|
|
299
|
+
if (a.idempotencyKey === undefined && op.idempotency === 'required') {
|
|
300
|
+
throw new TypeError(`Rewloy: ${id} needs idempotencyKey: Idempotency-Key gerekli, kütüphane uydurmaz (8–64 ASCII karakter) / the Idempotency-Key is required and is never generated for you (8–64 printable ASCII characters)`);
|
|
301
|
+
}
|
|
302
|
+
h.set('idempotency-key', a.idempotencyKey === undefined ? randomUUID() : checkIdempotencyKey(a.idempotencyKey));
|
|
303
|
+
}
|
|
269
304
|
if (op.body)
|
|
270
305
|
h.set('content-type', 'application/json');
|
|
271
306
|
if (lastEventId)
|
|
272
307
|
h.set('last-event-id', lastEventId);
|
|
273
|
-
for (const [k, v] of Object.entries(a.headers ?? {}))
|
|
274
|
-
if (v
|
|
275
|
-
|
|
308
|
+
for (const [k, v] of Object.entries(a.headers ?? {})) {
|
|
309
|
+
if (v === undefined || v === null)
|
|
310
|
+
continue;
|
|
311
|
+
h.set(k, k.toLowerCase() === 'idempotency-key' ? checkIdempotencyKey(v) : String(v));
|
|
312
|
+
}
|
|
276
313
|
return h;
|
|
277
314
|
}
|
|
278
315
|
#notice(id, op, headers) {
|
|
@@ -290,7 +327,7 @@ export class Rewloy extends RewloyMethods {
|
|
|
290
327
|
async #exchange(id, op, args, stream) {
|
|
291
328
|
const a = args ?? {};
|
|
292
329
|
const url = this.#url(id, op, a);
|
|
293
|
-
const headers = this.#headers(op, a, stream?.lastEventId);
|
|
330
|
+
const headers = this.#headers(id, op, a, stream?.lastEventId);
|
|
294
331
|
const body = op.body ? JSON.stringify(a.body ?? {}) : undefined;
|
|
295
332
|
const retryable = IDEMPOTENT_METHODS.has(op.method) || headers.has('idempotency-key');
|
|
296
333
|
const maxRetries = Math.max(0, a.maxRetries ?? this.maxRetries);
|
|
@@ -369,7 +406,7 @@ export class Rewloy extends RewloyMethods {
|
|
|
369
406
|
#invalid(id, res, body) {
|
|
370
407
|
return new RewloyError({
|
|
371
408
|
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,
|
|
409
|
+
requestId: res.headers.get('x-request-id'), body, headers: res.headers, rateLimit: parseRateLimit(res.headers), operation: id,
|
|
373
410
|
});
|
|
374
411
|
}
|
|
375
412
|
#failure(id, res, text) {
|
|
@@ -386,7 +423,7 @@ export class Rewloy extends RewloyMethods {
|
|
|
386
423
|
detail: e && typeof e.message === 'string' ? e.message : res.statusText || `HTTP ${String(res.status)}`,
|
|
387
424
|
details: e?.details, docs: e && typeof e.docs === 'string' ? e.docs : null,
|
|
388
425
|
requestId: res.headers.get('x-request-id') ?? (e && typeof e.requestId === 'string' ? e.requestId : null),
|
|
389
|
-
body: parsed, headers: res.headers, operation: id,
|
|
426
|
+
body: parsed, headers: res.headers, rateLimit: parseRateLimit(res.headers), operation: id,
|
|
390
427
|
};
|
|
391
428
|
if (res.status === 429) {
|
|
392
429
|
const header = parseRetryAfter(res.headers.get('retry-after'));
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* answer that is not what the API documents, or no answer at all.
|
|
5
5
|
*/
|
|
6
6
|
import type { ErrorCode } from './generated/types.js';
|
|
7
|
+
import type { RateLimitInfo } from './types.js';
|
|
7
8
|
export interface RewloyErrorInit {
|
|
8
9
|
status: number;
|
|
9
10
|
code: string;
|
|
@@ -14,6 +15,7 @@ export interface RewloyErrorInit {
|
|
|
14
15
|
requestId?: string | null | undefined;
|
|
15
16
|
body?: unknown;
|
|
16
17
|
headers?: Headers | null | undefined;
|
|
18
|
+
rateLimit?: RateLimitInfo | null | undefined;
|
|
17
19
|
operation?: string | null | undefined;
|
|
18
20
|
cause?: unknown;
|
|
19
21
|
}
|
|
@@ -49,6 +51,8 @@ export declare class RewloyError extends Error {
|
|
|
49
51
|
/** The parsed answer body (or its text, when it is not JSON). */
|
|
50
52
|
readonly body: unknown;
|
|
51
53
|
readonly headers: Headers | null;
|
|
54
|
+
/** The `RateLimit-*` headers of the answer; `null` when it carried none. */
|
|
55
|
+
readonly rateLimit: RateLimitInfo | null;
|
|
52
56
|
/** The operationId of the call. */
|
|
53
57
|
readonly operation: string | null;
|
|
54
58
|
constructor(init: RewloyErrorInit);
|
package/dist/errors.js
CHANGED
|
@@ -35,6 +35,8 @@ export class RewloyError extends Error {
|
|
|
35
35
|
/** The parsed answer body (or its text, when it is not JSON). */
|
|
36
36
|
body;
|
|
37
37
|
headers;
|
|
38
|
+
/** The `RateLimit-*` headers of the answer; `null` when it carried none. */
|
|
39
|
+
rateLimit;
|
|
38
40
|
/** The operationId of the call. */
|
|
39
41
|
operation;
|
|
40
42
|
constructor(init) {
|
|
@@ -49,6 +51,7 @@ export class RewloyError extends Error {
|
|
|
49
51
|
this.requestId = init.requestId ?? null;
|
|
50
52
|
this.body = init.body;
|
|
51
53
|
this.headers = init.headers ?? null;
|
|
54
|
+
this.rateLimit = init.rateLimit ?? null;
|
|
52
55
|
this.operation = init.operation ?? null;
|
|
53
56
|
}
|
|
54
57
|
}
|