@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 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 kampanya);
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` olmadan, kütüphane ekler);
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 olmadan
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` ve `sendCampaign` bir `Idempotency-Key` ister.
179
- Verilmezse kütüphane bir UUID üretir ve aynı çağrının her denemesinde aynısını
180
- gönderir; ama uygulama çöküp yeniden başlarsa yeni bir anahtar üretilir ve
181
- satış ikinci kez yazılabilir. Kasada anahtarı kendiniz üretip satışla birlikte
182
- saklayın:
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
- - **Idempotency keys.** `recordSale`, `passAction` and `sendCampaign` need an
526
- `Idempotency-Key`. A key is unique **for good per credential**: do not use the
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`. A generated key only covers the retries
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
- (the origin, without `/v1`). Default `https://app.rewloy.com`.
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`, `mode` (the `Rewloy-Mode` header: `live` or `test`) and `replayed` (`Idempotent-Replayed`).
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).replace(/\/+$/, '');
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
- h.set('idempotency-key', a.idempotencyKey ?? randomUUID());
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 !== undefined && v !== null)
275
- h.set(k, String(v));
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
  }