@rewloy/node 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,75 @@ 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.1 (2026-10-05)
9
+
10
+ Dışarıdan geliştiricilerin bulduğu üç sorun düzeltildi.
11
+
12
+ Three problems found by outside developers, fixed.
13
+
14
+ - **`Idempotency-Key` is checked before sending.** A key with non-ASCII
15
+ characters (`fiş-0042`) made `fetch` throw a bare `TypeError` about the header
16
+ value. Now the client refuses any key that is not printable ASCII
17
+ (0x21–0x7E), 8–64 characters, with a clear `TypeError` ("Idempotency-Key
18
+ yalnız ASCII karakterler içerebilir …") and sends nothing. The API will also
19
+ answer `400 VALIDATION` for such a key in its next release.
20
+ - **`baseUrl` takes the address with or without `/v1`.** The documentation and
21
+ the OpenAPI document show `https://app.rewloy.com/v1`; the client wanted the
22
+ origin only. Now both work; a trailing `/v1` or `/v1/` and trailing slashes
23
+ are stripped (`rewloy.baseUrl` is the origin).
24
+ - **`idempotencyKey` is required where the API requires it.** For `recordSale`,
25
+ `passAction`, `sendCampaign` and `refundShopRedemption` the OpenAPI document
26
+ marks the header required, but the client made up a random UUID when it was
27
+ missing, which does not survive a restart of your app. `idempotencyKey` is now
28
+ a required argument of those methods (a type error in TypeScript, a
29
+ `TypeError` before sending in JavaScript). Where the header is optional
30
+ (`issuePass`, …) a UUID is still generated and reused on every retry.
31
+ **Breaking for callers that relied on the generated key** (a small break,
32
+ taken in a patch release because the old behaviour could write a sale twice).
33
+
34
+ ## 0.2.0 (2026-10-05)
35
+
36
+ Rewloy API 1.0.5'e göre yeniden üretildi: 255 işlem (0.1.0'da 237). Kasa için
37
+ `recordSale` ve `reverseSale`; README'de yeni bir kasa örneği, test modu ve
38
+ `baseUrl`.
39
+
40
+ Regenerated from Rewloy API 1.0.5: 255 operations (237 in 0.1.0).
41
+
42
+ - **New operations (18).**
43
+ - *Till:* `recordSale` (`POST /v1/passes/{serial}/sale`: write a completed
44
+ sale to a card; the card type decides what is written) and `reverseSale`
45
+ (`POST /v1/passes/{serial}/sale/reverse`: take a refunded sale back).
46
+ - *Checkout codes and shop connections:* `quoteCheckoutCode`,
47
+ `holdCheckoutCode`, `captureCheckoutOrder`, `releaseCheckoutOrder`,
48
+ `refundCheckoutOrder`, `listOrderRedemptions`, `listShopRedemptions`,
49
+ `releaseShopRedemption`, `refundShopRedemption`, `setShopSettings`,
50
+ `setShopCeiling`, `setShopPluginAbilities`, and for the card holder
51
+ `holderCheckoutCodes`, `mintHolderCheckoutCode`, `cancelHolderCheckoutCode`.
52
+ - `getMeta` (`GET /v1/meta`): the API's version.
53
+ - **`getPass`** now also returns `programName`, `currency`, `stamps`
54
+ (`count`, `max`), `points`, `money` (`amountMinor`, `currency`), `customer`
55
+ (with `customers.read`), `actions` and `sale`.
56
+ - **Webhooks.** `webhooks.manage` API keys manage webhooks (`createWebhook`,
57
+ `listWebhooks`, `getWebhook`, `setWebhookStatus`, `testWebhook`,
58
+ `listWebhookDeliveries`, `webhookEvents`); a webhook reports `createdByKey`.
59
+ - **Other fields.** `issuePass` returns `created`; business lists and `me`
60
+ carry `currency`; programs carry `sale`; batches `onlineValue`; shops
61
+ `accepts`, `settings`, `shopName`, `unbacked` and the plugin key's
62
+ `abilities`.
63
+ - **Generator.** A doc comment holding an unbalanced bracket no longer
64
+ breaks an object union such as the answer of `me`.
65
+ - **README.**
66
+ - A till example with `recordSale`, the structured fields of `getPass` and
67
+ a refund with `reverseSale`.
68
+ - `Idempotency-Key`: a key is unique for good per credential. The
69
+ receipt number alone is not a key (fiscal receipt numbers restart after
70
+ the Z report): use register + Z number + receipt number, or a UUID
71
+ stored with the sale. The receipt number goes in `reference`.
72
+ - Test mode exists: `rwk_test_` keys and a test business. The "being
73
+ prepared" wording is gone.
74
+ - How to set a custom base URL (staging), and a link to the developer
75
+ docs, https://rewloy.com/gelistiriciler.
76
+
8
77
  ## 0.1.0 (2026-10-04)
9
78
 
10
79
 
package/README.md CHANGED
@@ -13,15 +13,16 @@ kartı, kupon ve indirimdir:
13
13
 
14
14
  Kasada QR okutulur; bakiye, ödül ve kampanyalar kartın kendisinde güncellenir.
15
15
  Panelde yapılabilen her şey [Rewloy API v1](https://rewloy.com/gelistiriciler)
16
- ile de yapılabilir; bu kütüphane onu Node.js'ten kullanır:
16
+ ile de yapılabilir; bu kütüphane onu Node.js'ten kullanır. Geliştirici
17
+ belgeleri: **https://rewloy.com/gelistiriciler**.
17
18
 
18
19
  - **Tam tipli.** API'nin her işlemi, `operationId` adıyla bir metottur.
19
20
  Parametreler, gövdeler ve yanıtlar OpenAPI belgesinden
20
21
  ([`openapi.json`](https://app.rewloy.com/v1/openapi.json)) üretilen
21
22
  tiplerle gelir. CI belgeyi her gün okur ve değişince yeniden üretir.
22
23
  - **Bağımlılıksız.** Node 22 ve üstü; yerleşik `fetch` ve `node:crypto`.
23
- - **Güvenli tekrar.** Geçici hatalarda ölçülü yeniden deneme; kasa işleminde
24
- ve kampanyada `Idempotency-Key`.
24
+ - **Güvenli tekrar.** Geçici hatalarda ölçülü yeniden deneme; satışta, kasa
25
+ işleminde ve kampanyada `Idempotency-Key`.
25
26
  - **Ötesi:** sayfalama, canlı akış (SSE), webhook imzası doğrulama,
26
27
  kullanımdan kalkma uyarıları.
27
28
 
@@ -51,7 +52,7 @@ işlemin gerektirdikleriyle:
51
52
  - `query`: sorgu parametreleri;
52
53
  - `body`: JSON gövde;
53
54
  - `merchant`: `Rewloy-Merchant` başlığı;
54
- - `idempotencyKey`: `Idempotency-Key` başlığı (kasa işlemi ve kampanya);
55
+ - `idempotencyKey`: `Idempotency-Key` başlığı (satış, kasa işlemi, kampanya ve mağaza iadesinde zorunlu);
55
56
  - `signal`, `timeoutMs`, `maxRetries`.
56
57
 
57
58
  Metot yanıttaki `data`yı döndürür. Sayfalı listelerde `{ data, meta }`,
@@ -85,12 +86,27 @@ Bir işlem istemcinin kimlik türünü kabul etmiyor ama kimliksiz de çalışı
85
86
  bir kimliği reddeder (`CREDENTIAL_NOT_ALLOWED`).
86
87
 
87
88
  Diğer seçenekler:
88
- - `baseUrl` (varsayılan `https://app.rewloy.com`);
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);
89
90
  - `timeoutMs` (60000);
90
91
  - `maxRetries` (2);
91
92
  - `fetch`: kendi `fetch`iniz;
92
93
  - `userAgent`: gönderilen `User-Agent`a eklenir, örneğin `"KasaPOS/4.2"`.
93
94
 
95
+ ### Başka bir adres (staging)
96
+
97
+ API'nin başka bir kopyasına (kendi staging ortamınız ya da bir vekil sunucu)
98
+ `baseUrl` ile bağlanılır:
99
+
100
+ ```ts
101
+ const rewloy = new Rewloy({
102
+ apiKey: process.env.REWLOY_API_KEY!,
103
+ baseUrl: 'https://rewloy-staging.ornek.com', // sonuna /v1 yazsanız da olur
104
+ });
105
+ ```
106
+
107
+ Gerçek müşterilere dokunmadan denemek için adres değiştirmeniz gerekmez:
108
+ [test modu](#test-modu) aynı adreste, ayrı bir test ortamıyla çalışır.
109
+
94
110
  ## Kart vermek ve kasada işlem
95
111
 
96
112
  ```ts
@@ -101,16 +117,87 @@ const { serial, cardUrl } = await rewloy.issuePass({
101
117
  const sonuc = await rewloy.passAction({
102
118
  params: { serial },
103
119
  body: { action: 'earn-stamps', locationId, count: 1 },
104
- idempotencyKey: `fis-${fisNo}`,
120
+ idempotencyKey: `kasa3-z0187-fis${fisNo}`, // aşağıya bakın
121
+ });
122
+ if (sonuc.duplicate) console.log('Bu işlem zaten yazılmış');
123
+ ```
124
+
125
+ ### Satış: `recordSale`
126
+
127
+ Kasa ya da kendi yazılımınız için en kolay yol `recordSale`dir: "bu satış
128
+ oldu, sen yaz". Ödenen toplamı (kartın para biriminde, kuruş) gönderirsiniz;
129
+ ne yazılacağına kartın türü ve programın kendi kuralı karar verir. Kartın
130
+ türünü bilmeniz gerekmez.
131
+
132
+ ```ts
133
+ const kart = await rewloy.getPass({ params: { serial } });
134
+ // Kartın türüne özgü alanlar; `balance` yerine bunları okuyun.
135
+ if (kart.stamps) console.log(`${kart.stamps.count} / ${kart.stamps.max} damga`);
136
+ if (kart.points !== undefined) console.log(`${kart.points} puan`);
137
+ if (kart.money) console.log(`${kart.money.amountMinor / 100} ${kart.money.currency}`);
138
+ console.log(kart.programName, kart.customer?.name); // customer: yalnız customers.read yetkisiyle
139
+
140
+ // Fiş numarası anahtar olamaz: kasa + Z no + fiş no, ya da satışla saklanan bir UUID.
141
+ const anahtar = `kasa3-z0187-fis${fisNo}`;
142
+ const satis = await rewloy.recordSale({
143
+ params: { serial },
144
+ body: {
145
+ locationId,
146
+ amountMinor: 4550, // 45,50: kartın para biriminde (`kart.currency`), kuruş
147
+ currency: kart.currency, // isteğe bağlı güvence: uyuşmazsa 422 CURRENCY_MISMATCH
148
+ reference: `fis-${fisNo}`, // fiş numarası buraya yazılır
149
+ },
150
+ idempotencyKey: anahtar,
151
+ });
152
+ if (satis.applied === 'none') console.log('Yazılan bir şey yok:', satis.reason);
153
+ else console.log(`${satis.credited} ${satis.applied} yazıldı, bakiye ${satis.balance}`);
154
+ if (satis.rewardReady) console.log('Ödül hazır');
155
+ ```
156
+
157
+ `GET /v1/passes/{serial}` ayrıca `actions` (kartın aldığı kasa işlemleri ve
158
+ şimdi yapılıp yapılamayacakları) ve `sale` (bir satışın bu kartta ne
159
+ yazacağı) alanlarını verir.
160
+
161
+ **İade.** `reverseSale` bir satışın karta yazdığını geri alır; satışı
162
+ yazarken gönderdiğiniz anahtarla (`saleKey`) ya da `reference`la bulur:
163
+
164
+ ```ts
165
+ const geri = await rewloy.reverseSale({
166
+ params: { serial },
167
+ body: { saleKey: anahtar, locationId },
105
168
  });
106
- if (sonuc.duplicate) console.log('Bu fiş zaten işlenmiş');
169
+ console.log(geri.reversed, geri.applied, geri.balance, geri.duplicate);
107
170
  ```
108
171
 
109
- `passAction` ve `sendCampaign` bir `Idempotency-Key` ister. Verilmezse
172
+ Bir satış bir kez geri alınır (tekrar `duplicate: true` döner). Kazanılan
173
+ kullanılmışsa (ödüle ya da harcamaya gitmişse) `409 SALE_ALREADY_SPENT` gelir ve
174
+ hiçbir şey yazılmaz.
175
+
176
+ ### `Idempotency-Key`
177
+
178
+ `recordSale`, `passAction`, `sendCampaign` ve `refundShopRedemption` bir
179
+ `Idempotency-Key` **ister**: API'nin tanımında (OpenAPI) bu başlık bu işlemlerde
180
+ zorunludur, bu yüzden `idempotencyKey` bu metotlarda zorunlu bir argümandır.
181
+ Verilmezse kütüphane istek göndermeden `TypeError` fırlatır; **sizin yerinize
182
+ anahtar üretmez**. Üretilmiş rastgele bir anahtar yalnızca tek çağrının yeniden
183
+ denemelerini korurdu: uygulama çöküp yeniden başlarsa yeni bir anahtar çıkar ve
184
+ satış ikinci kez yazılabilirdi. Anahtarı kendiniz üretip satışla birlikte
185
+ saklayın. Anahtar 8–64 karakterlik görünür ASCII olmalıdır (0x21–0x7E: harf,
186
+ rakam ve noktalama; boşluk, Türkçe harf ya da `fiş` gibi ASCII dışı karakter
187
+ olmaz); aksi halde kütüphane yine istek göndermeden `TypeError` fırlatır.
188
+ Başlığın isteğe bağlı olduğu işlemlerde (örneğin `issuePass`) anahtar verilmezse
110
189
  kütüphane bir UUID üretir ve aynı çağrının her denemesinde aynısını gönderir.
111
- Kasada fiş numarası gibi kendi anahtarınızı vermek daha iyidir: uygulama
112
- çöküp yeniden başlasa bile aynı fiş ikinci kez işlenmez, aynı anahtarla tekrar
113
- ilk sonucu `duplicate: true` ile döndürür.
190
+
191
+ - **Anahtar bir kimlik için kalıcı olarak tekildir** (8–64 karakter; defterden
192
+ hiç silinmez). Aynı anahtarla aynı isteğin tekrarı ikinci kez yazmaz ve
193
+ ilk sonucu `duplicate: true` ile döndürür. Aynı anahtar başka bir gövdeyle
194
+ `422 IDEMPOTENCY_KEY_REUSED` alır.
195
+ - **Fiş numarası tek başına anahtar olamaz:** yazarkasa fiş numaraları Z
196
+ raporundan sonra yeniden başlar. Kasa + Z no + fiş no birleşimi
197
+ (`kasa3-z0187-fis0042`) ya da satışla birlikte saklanıp tekrarda yeniden
198
+ gönderilen bir UUID kullanın.
199
+ - **Fiş numarası `reference` alanına** yazılır; müşterinin geçmişinde ve işlem
200
+ dökümünde görünür.
114
201
 
115
202
  ## Sayfalama
116
203
 
@@ -164,6 +251,22 @@ gösterilen sırla (`whsec_…`) doğrular:
164
251
  - `t` şimdiden 300 saniyeden (`toleranceSeconds`) uzaksa reddeder;
165
252
  - gövdeyi ayrıştırılmış olarak döndürür.
166
253
 
254
+ Webhook'u panelden ya da API'den ekleyebilirsiniz. `webhooks.manage` yetkili
255
+ bir API anahtarı `createWebhook`, `listWebhooks`, `getWebhook`,
256
+ `setWebhookStatus`, `testWebhook` ve `listWebhookDeliveries`yi çağırabilir;
257
+ `webhookEvents` abone olunabilecek olayları söyler. Sır (`secret`) yalnız
258
+ `createWebhook` yanıtında gelir, saklayın:
259
+
260
+ ```ts
261
+ const { webhook, secret } = await rewloy.createWebhook({
262
+ body: { url: 'https://ornek.com/rewloy/webhook', events: ['pass.activity', 'pass.voided'] },
263
+ });
264
+ await rewloy.testWebhook({ params: { id: webhook.id } }); // webhook.test olayı gönderir
265
+ ```
266
+
267
+ Adres herkese açık bir `https` adresi olmalıdır (test ortamında da);
268
+ yerelde bir tünel kullanın.
269
+
167
270
  Tutmazsa `WebhookSignatureError` atar: 400 ile yanıtlayın ve hiçbir işlem
168
271
  yapmayın. Gövde mutlaka ham olmalıdır. JSON olarak ayrıştırılıp yeniden yazılan
169
272
  bir gövde imzayı tutturmaz.
@@ -238,7 +341,7 @@ try {
238
341
  await rewloy.passAction({
239
342
  params: { serial },
240
343
  body: { action: 'spend', locationId, amountMinor: 5000 },
241
- idempotencyKey: `fis-${fisNo}`,
344
+ idempotencyKey: `kasa3-z0187-fis${fisNo}`,
242
345
  });
243
346
  } catch (err) {
244
347
  if (err instanceof RateLimitError) console.log(`${err.retryAfter} saniye sonra yeniden deneyin`);
@@ -309,14 +412,35 @@ yanit.data; // kampanya
309
412
  `data`, sayfalı listede `meta`, `status`, `headers`, `requestId`, `mode` ve
310
413
  `replayed`.
311
414
 
312
- `mode`, yanıtın `Rewloy-Mode` başlığıdır. Platformda test modu hazırlanıyor:
313
- gerçek mesaj göndermeyen, gerçek kart vermeyen test anahtarları. Geldiğinde
314
- test yanıtları bunu bu başlıkla söyleyecek. Başlık yoksa `null`. Canlı akışta
315
- aynı bilgi `akis.mode`dadır.
415
+ `mode`, yanıtın `Rewloy-Mode` başlığıdır: `live` ya da `test`. Başlık yoksa
416
+ `null`. Canlı akışta aynı bilgi `akis.mode`dadır.
316
417
 
317
418
  İşlem tablosu da dışa açıktır: `OPERATIONS.passAction` →
318
419
  `{ method, path, auth, merchant, idempotency, paged, stream, deprecated, … }`.
319
420
 
421
+ ## Test modu
422
+
423
+ Gerçek müşterilere dokunmadan denemek için işletmenizin bir **test ortamı**
424
+ vardır: ona bağlı ayrı bir işletme (adı "· Test" ile biter); kendi
425
+ programları, müşterileri, kartları, anahtarları ve webhook'ları. Panel →
426
+ Geliştirici → "Test ortamını aç" ya da `POST /v1/test/environment`. Orada
427
+ oluşturulan anahtar `rwk_test_` ile başlar ve aynı adreste, aynı yollarla
428
+ çalışır:
429
+
430
+ ```ts
431
+ const rewloy = new Rewloy({ apiKey: process.env.REWLOY_TEST_KEY! }); // rwk_test_…
432
+ const yanit = await rewloy.request('getPass', { params: { serial } });
433
+ yanit.mode; // 'test'
434
+ ```
435
+
436
+ - Test ortamı hiçbir şey göndermez (e-posta, bildirim, SMS); kartlar
437
+ cüzdanlara eklenmez. Gönderilmeyenler `GET /v1/test/messages` ile okunur.
438
+ - Webhook'lar teslim edilir ve `Rewloy-Test: 1` başlığıyla `"test": true`
439
+ taşır.
440
+ - Gerçek müşteri verisini test ortamına girmeyin.
441
+
442
+ Ayrıntı: https://rewloy.com/gelistiriciler#test-ortamı
443
+
320
444
  ## Geliştirme
321
445
 
322
446
  ```sh
@@ -369,7 +493,8 @@ Bir güvenlik açığı bulursanız [SECURITY.md](SECURITY.md) dosyasındaki yol
369
493
  > **Status: preview (0.x), published on npm. The API is stable; the
370
494
  > library's interface may change until 1.0.**
371
495
 
372
- The documentation of the API itself is in Turkish (links above). In short:
496
+ The documentation of the API itself is in Turkish. Developer docs:
497
+ **https://rewloy.com/gelistiriciler**. In short:
373
498
 
374
499
  - Every operation of the API is a method named by its `operationId`, typed
375
500
  from the OpenAPI document, which CI reads daily and regenerates from.
@@ -393,21 +518,46 @@ import { Rewloy } from '@rewloy/node';
393
518
  const rewloy = new Rewloy({ apiKey: process.env.REWLOY_API_KEY! }); // or { staffSession, merchant } or { holderSession }
394
519
 
395
520
  const { serial } = await rewloy.issuePass({ body: { programId, email, kvkkConsent: true } });
396
- const result = await rewloy.passAction({
521
+ const sale = await rewloy.recordSale({
397
522
  params: { serial },
398
- body: { action: 'earn-stamps', locationId },
399
- idempotencyKey: `receipt-${receiptNo}`, // generated when omitted, reused across retries
523
+ body: { locationId, amountMinor: 4550, reference: `receipt-${receiptNo}` }, // amount in the card's currency, minor units
524
+ idempotencyKey: `till3-z0187-r${receiptNo}`,
400
525
  });
401
526
  ```
402
527
 
528
+ - **Till.** `recordSale` writes a completed sale to a card (the card type and
529
+ the programme's own rule decide what is written); `getPass` returns the
530
+ card's structured fields (`programName`, `currency`, `stamps`, `points`,
531
+ `money`, `customer`); `reverseSale` takes a refunded sale back:
532
+ `rewloy.reverseSale({ params: { serial }, body: { saleKey: key } })`.
533
+ - **Idempotency keys.** `recordSale`, `passAction`, `sendCampaign` and
534
+ `refundShopRedemption` need an `Idempotency-Key`: the API's OpenAPI document
535
+ marks the header required for them, so `idempotencyKey` is a required
536
+ argument and the client throws a `TypeError` before sending if it is missing.
537
+ It never makes one up for you (a generated key would not survive a restart of
538
+ your app). The key must be 8–64 printable ASCII characters (0x21–0x7E); a
539
+ non-ASCII key such as `fiş-0042` is refused client-side, with a `TypeError`,
540
+ before anything is sent. Where the header is optional (for example
541
+ `issuePass`) the client still generates a UUID and reuses it on every retry
542
+ of the call. A key is unique **for good per credential**: do not use the
543
+ receipt number alone (fiscal receipt numbers restart after the Z report) but
544
+ register + Z number + receipt number, or a UUID stored with the sale. The
545
+ receipt number goes in `reference`.
546
+ - **Base URL.** `new Rewloy({ apiKey, baseUrl: 'https://staging.example.com' })`
547
+ or `baseUrl: 'https://staging.example.com/v1'`: with or without a trailing
548
+ `/v1` (and trailing slashes), the client appends `/v1/...` itself. Default
549
+ `https://app.rewloy.com`.
550
+ - **Test mode.** Open the test environment (panel → Developer, or
551
+ `POST /v1/test/environment`) and use its `rwk_test_` key at the same address:
552
+ a separate test business that sends nothing and never reaches real
553
+ customers. Webhooks are delivered with `Rewloy-Test: 1`.
403
554
  - **Arguments.** Each method takes one object: `params`, `query` and `body` as
404
555
  the operation needs, plus `merchant`, `idempotencyKey`, `signal`, `timeoutMs`
405
556
  and `maxRetries`.
406
557
  - **Results.** It resolves to the answer's `data`: `{ data, meta }` for paged
407
558
  lists, `undefined` for 204, a `Blob` for files.
408
559
  - **The whole answer.** `rewloy.request(id, args)` returns `status`,
409
- `headers`, `requestId`, `mode` (the `Rewloy-Mode` header, for the coming
410
- test mode) and `replayed` (`Idempotent-Replayed`).
560
+ `headers`, `requestId`, `mode` (the `Rewloy-Mode` header: `live` or `test`) and `replayed` (`Idempotent-Replayed`).
411
561
  - **Pagination.** `rewloy.paginate('listCustomers', args)` iterates the items
412
562
  of every page.
413
563
  - **Streams.** `rewloy.liveFeed({ signal })` (or `rewloy.stream('liveFeed',
@@ -451,3 +601,9 @@ const event = verifyWebhook({ payload: req.body, header: req.get('Rewloy-Signatu
451
601
 
452
602
  Report vulnerabilities privately, as [SECURITY.md](SECURITY.md) says.
453
603
  [MIT](LICENSE) licensed.
604
+
605
+ ## Yeni sürüm yayımlamak / Releasing
606
+
607
+ `package.json`'daki sürümü ve CHANGELOG'u güncelleyin, commit'leyin, `v<sürüm>` etiketini gönderin (`git tag v0.2.0 && git push origin v0.2.0`). `release.yml` npm'e güvenilir yayıncı (trusted publishing) yoluyla, jetonsuz ve kaynak kanıtıyla (provenance) yayımlar.
608
+
609
+ Bump the version in `package.json` and the changelog, commit, and push a `v<version>` tag. `release.yml` publishes to npm through trusted publishing: no token, with provenance.
package/dist/client.d.ts CHANGED
@@ -60,6 +60,17 @@ type ItemOf<K extends PagedOperationId> = Operations[K]['data'] extends (infer T
60
60
  export declare function parseRetryAfter(value: string | null, now?: number): number | null;
61
61
  /** Exponential backoff with jitter for the retry after attempt `attempt` (0-based). */
62
62
  export declare function backoff(attempt: number, random?: () => number): number;
63
+ /**
64
+ * The base URL without trailing slashes and without a trailing `/v1`: the
65
+ * paths of the operations carry `/v1` themselves, and the documentation shows
66
+ * the address both ways (`https://app.rewloy.com` and `https://app.rewloy.com/v1`).
67
+ */
68
+ export declare function normalizeBaseUrl(url: string): string;
69
+ /**
70
+ * An `Idempotency-Key` is 8–64 printable ASCII characters (0x21–0x7E): an HTTP
71
+ * header value cannot carry anything else, and `fetch` would throw a bare TypeError.
72
+ */
73
+ export declare function checkIdempotencyKey(key: unknown): string;
63
74
  /**
64
75
  * A client of the Rewloy API (`https://app.rewloy.com/v1`).
65
76
  *
package/dist/client.js CHANGED
@@ -56,6 +56,24 @@ export function backoff(attempt, random = Math.random) {
56
56
  const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
57
57
  return Math.round(cap / 2 + random() * (cap / 2));
58
58
  }
59
+ /**
60
+ * The base URL without trailing slashes and without a trailing `/v1`: the
61
+ * paths of the operations carry `/v1` themselves, and the documentation shows
62
+ * the address both ways (`https://app.rewloy.com` and `https://app.rewloy.com/v1`).
63
+ */
64
+ export function normalizeBaseUrl(url) {
65
+ return url.replace(/\/+$/, '').replace(/\/v1$/, '').replace(/\/+$/, '');
66
+ }
67
+ /**
68
+ * An `Idempotency-Key` is 8–64 printable ASCII characters (0x21–0x7E): an HTTP
69
+ * header value cannot carry anything else, and `fetch` would throw a bare TypeError.
70
+ */
71
+ export function checkIdempotencyKey(key) {
72
+ if (typeof key !== 'string' || !/^[\x21-\x7e]{8,64}$/.test(key)) {
73
+ 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');
74
+ }
75
+ return key;
76
+ }
59
77
  /** The URL a `Link` header gives for `rel="deprecation"` (else its first). */
60
78
  function deprecationLink(link) {
61
79
  if (!link)
@@ -135,7 +153,7 @@ export class Rewloy extends RewloyMethods {
135
153
  if (options.merchant !== undefined && which !== 'staffSession')
136
154
  throw new TypeError('Rewloy: `merchant` goes with a staffSession');
137
155
  this.merchant = options.merchant ?? null;
138
- this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
156
+ this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
139
157
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
140
158
  this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
141
159
  const f = options.fetch ?? globalThis.fetch;
@@ -251,7 +269,7 @@ export class Rewloy extends RewloyMethods {
251
269
  const qs = query.toString();
252
270
  return `${this.baseUrl}${path}${qs ? `?${qs}` : ''}`;
253
271
  }
254
- #headers(op, a, lastEventId) {
272
+ #headers(id, op, a, lastEventId) {
255
273
  const h = new Headers();
256
274
  h.set('accept', op.stream ? 'text/event-stream' : op.response === 'json' || op.response === 'raw-json' ? 'application/json' : '*/*');
257
275
  if (this.#userAgent)
@@ -264,15 +282,21 @@ export class Rewloy extends RewloyMethods {
264
282
  const merchant = a.merchant ?? (this.credential === 'staff' ? this.merchant : null);
265
283
  if (op.merchant && merchant)
266
284
  h.set('rewloy-merchant', merchant);
267
- if (op.idempotency)
268
- h.set('idempotency-key', a.idempotencyKey ?? randomUUID());
285
+ if (op.idempotency) {
286
+ if (a.idempotencyKey === undefined && op.idempotency === 'required') {
287
+ 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)`);
288
+ }
289
+ h.set('idempotency-key', a.idempotencyKey === undefined ? randomUUID() : checkIdempotencyKey(a.idempotencyKey));
290
+ }
269
291
  if (op.body)
270
292
  h.set('content-type', 'application/json');
271
293
  if (lastEventId)
272
294
  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));
295
+ for (const [k, v] of Object.entries(a.headers ?? {})) {
296
+ if (v === undefined || v === null)
297
+ continue;
298
+ h.set(k, k.toLowerCase() === 'idempotency-key' ? checkIdempotencyKey(v) : String(v));
299
+ }
276
300
  return h;
277
301
  }
278
302
  #notice(id, op, headers) {
@@ -290,7 +314,7 @@ export class Rewloy extends RewloyMethods {
290
314
  async #exchange(id, op, args, stream) {
291
315
  const a = args ?? {};
292
316
  const url = this.#url(id, op, a);
293
- const headers = this.#headers(op, a, stream?.lastEventId);
317
+ const headers = this.#headers(id, op, a, stream?.lastEventId);
294
318
  const body = op.body ? JSON.stringify(a.body ?? {}) : undefined;
295
319
  const retryable = IDEMPOTENT_METHODS.has(op.method) || headers.has('idempotency-key');
296
320
  const maxRetries = Math.max(0, a.maxRetries ?? this.maxRetries);