@invonetwork/web-sdk 0.3.0 → 0.4.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/dist/server.cjs CHANGED
@@ -100,11 +100,11 @@ var _Http = class _Http {
100
100
  * (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
101
101
  */
102
102
  async post(path, body, auth, opts) {
103
- return this.request("POST", path, body, auth, opts?.idempotent ?? false);
103
+ return this.request("POST", path, body, auth, opts?.idempotent ?? false, opts?.signal);
104
104
  }
105
105
  // GET is always idempotent → safe to retry.
106
- async get(path, auth) {
107
- return this.request("GET", path, void 0, auth, true);
106
+ async get(path, auth, opts) {
107
+ return this.request("GET", path, void 0, auth, true, opts?.signal);
108
108
  }
109
109
  authHeaders(auth) {
110
110
  switch (auth.kind) {
@@ -116,7 +116,7 @@ var _Http = class _Http {
116
116
  return {};
117
117
  }
118
118
  }
119
- async request(method, path, body, auth, idempotent) {
119
+ async request(method, path, body, auth, idempotent, signal) {
120
120
  const url = `${this.baseUrl}${path}`;
121
121
  const headers = {
122
122
  Accept: "application/json",
@@ -126,10 +126,13 @@ var _Http = class _Http {
126
126
  if (body !== void 0) headers["Content-Type"] = "application/json";
127
127
  const payload = body !== void 0 ? JSON.stringify(body) : void 0;
128
128
  for (let attempt = 0; ; attempt++) {
129
+ if (signal?.aborted) throw abortError(path);
129
130
  const start = Date.now();
130
131
  this.fire("onRequest", { method, url, attempt });
131
132
  const controller = new AbortController();
132
133
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
134
+ const onAbort = () => controller.abort();
135
+ signal?.addEventListener("abort", onAbort, { once: true });
133
136
  let res;
134
137
  let networkError;
135
138
  try {
@@ -142,12 +145,14 @@ var _Http = class _Http {
142
145
  });
143
146
  } finally {
144
147
  clearTimeout(timer);
148
+ signal?.removeEventListener("abort", onAbort);
145
149
  }
146
150
  if (networkError) {
151
+ if (signal?.aborted) throw abortError(path);
147
152
  const willRetry = idempotent && attempt < this.maxRetries;
148
153
  this.fire("onError", { method, url, attempt, error: networkError, willRetry });
149
154
  if (willRetry) {
150
- await sleep(this.backoff(attempt));
155
+ await sleep(this.backoff(attempt), signal);
151
156
  continue;
152
157
  }
153
158
  throw networkError;
@@ -183,7 +188,7 @@ var _Http = class _Http {
183
188
  }
184
189
  this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
185
190
  if (wait !== void 0) {
186
- await sleep(wait);
191
+ await sleep(wait, signal);
187
192
  continue;
188
193
  }
189
194
  throw err;
@@ -209,8 +214,21 @@ var _Http = class _Http {
209
214
  /** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
210
215
  _Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
211
216
  var Http = _Http;
212
- function sleep(ms) {
213
- return new Promise((resolve) => setTimeout(resolve, ms));
217
+ function sleep(ms, signal) {
218
+ return new Promise((resolve) => {
219
+ if (signal?.aborted) return resolve();
220
+ const timer = setTimeout(done, ms);
221
+ const onAbort = () => done();
222
+ function done() {
223
+ clearTimeout(timer);
224
+ signal?.removeEventListener("abort", onAbort);
225
+ resolve();
226
+ }
227
+ signal?.addEventListener("abort", onAbort, { once: true });
228
+ });
229
+ }
230
+ function abortError(path) {
231
+ return new InvoError({ message: `Request to ${path} was aborted`, code: "ABORTED", status: 0 });
214
232
  }
215
233
  function pickRequestId(headers) {
216
234
  if (!headers || typeof headers.get !== "function") return void 0;
@@ -265,7 +283,7 @@ function safeEqualHex(a, b) {
265
283
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
266
284
  return diff === 0;
267
285
  }
268
- function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
286
+ function prepareVerification(rawBody, signatureHeader, secret, opts) {
269
287
  if (!signatureHeader) {
270
288
  throw webhookError("Missing X-Invo-Signature header.", "WEBHOOK_SIGNATURE_MISSING");
271
289
  }
@@ -288,13 +306,9 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
288
306
  }
289
307
  const bodyBytes = toBytes(rawBody);
290
308
  const message = concatBytes(ENCODER.encode(`${t}.`), bodyBytes);
291
- const matched = secrets.some((s) => {
292
- const expected = hmacHex(s, message);
293
- return sigs.some((sig) => safeEqualHex(sig, expected));
294
- });
295
- if (!matched) {
296
- throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
297
- }
309
+ return { secrets, sigs, message, bodyBytes };
310
+ }
311
+ function finalizeEvent(bodyBytes) {
298
312
  let parsed;
299
313
  try {
300
314
  parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
@@ -306,9 +320,93 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
306
320
  }
307
321
  return parsed;
308
322
  }
323
+ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
324
+ const { secrets, sigs, message, bodyBytes } = prepareVerification(
325
+ rawBody,
326
+ signatureHeader,
327
+ secret,
328
+ opts
329
+ );
330
+ const matched = secrets.some((s) => {
331
+ const expected = hmacHex(s, message);
332
+ return sigs.some((sig) => safeEqualHex(sig, expected));
333
+ });
334
+ if (!matched) {
335
+ throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
336
+ }
337
+ return finalizeEvent(bodyBytes);
338
+ }
339
+ async function verifyWebhookAsync(rawBody, signatureHeader, secret, opts = {}) {
340
+ const { secrets, sigs, message, bodyBytes } = prepareVerification(
341
+ rawBody,
342
+ signatureHeader,
343
+ secret,
344
+ opts
345
+ );
346
+ for (const s of secrets) {
347
+ const expected = await hmacHexSubtle(s, message);
348
+ if (sigs.some((sig) => safeEqualHex(sig, expected))) {
349
+ return finalizeEvent(bodyBytes);
350
+ }
351
+ }
352
+ throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
353
+ }
354
+ function createWebhookHandler(opts) {
355
+ return async (request) => {
356
+ const raw = await request.text();
357
+ let event;
358
+ try {
359
+ event = await verifyWebhookAsync(raw, request.headers.get("x-invo-signature"), opts.secret, {
360
+ toleranceSec: opts.toleranceSec
361
+ });
362
+ } catch (e) {
363
+ const err = e instanceof InvoError ? e : new InvoError({ message: String(e), status: 0 });
364
+ if (opts.onError) {
365
+ try {
366
+ const r = await opts.onError(err, request);
367
+ if (r instanceof Response) return r;
368
+ } catch {
369
+ }
370
+ }
371
+ return new Response(err.code ?? "invalid_signature", { status: 400 });
372
+ }
373
+ try {
374
+ await opts.onEvent(event, {
375
+ // Prefer the signature-covered body field over the (unauthenticated, optional)
376
+ // header — the body value is HMAC-verified and always present.
377
+ idempotencyKey: event.idempotency_key ?? request.headers.get("x-invo-idempotency-key"),
378
+ request
379
+ });
380
+ } catch {
381
+ return new Response("handler_error", { status: 500 });
382
+ }
383
+ return new Response(null, { status: 200 });
384
+ };
385
+ }
386
+ async function hmacHexSubtle(secret, message) {
387
+ const subtle = globalThis.crypto?.subtle;
388
+ if (!subtle) {
389
+ throw webhookError(
390
+ "Web Crypto (crypto.subtle) is unavailable in this runtime.",
391
+ "WEBHOOK_UNSUPPORTED"
392
+ );
393
+ }
394
+ const key = await subtle.importKey(
395
+ "raw",
396
+ ENCODER.encode(secret),
397
+ { name: "HMAC", hash: "SHA-256" },
398
+ false,
399
+ ["sign"]
400
+ );
401
+ const sig = await subtle.sign("HMAC", key, message);
402
+ const bytes = new Uint8Array(sig);
403
+ let hex = "";
404
+ for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
405
+ return hex;
406
+ }
309
407
 
310
408
  // src/server.ts
311
- var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
409
+ var DEFAULT_UA = "invonetwork-web-sdk/0.4.0 (+https://invo.network)";
312
410
  var MAX_USD_AMOUNT = 999.99;
313
411
  var MAX_ITEM_PRICE = 999999.99;
314
412
  function invalidInput(label, value, why) {
@@ -348,6 +446,21 @@ function toOrderDetails(raw) {
348
446
  raw
349
447
  };
350
448
  }
449
+ function toInboundPendingItem(row) {
450
+ return {
451
+ transactionId: String(row["transaction_id"] ?? ""),
452
+ flow: String(row["flow"] ?? ""),
453
+ amount: String(row["amount"] ?? ""),
454
+ netAmount: String(row["net_amount"] ?? ""),
455
+ sourceGameId: row["source_game_id"] ?? "",
456
+ sourceGame: String(row["source_game"] ?? ""),
457
+ toPhone: String(row["to_phone"] ?? ""),
458
+ toIdentityId: row["to_identity_id"] ?? null,
459
+ createdAt: String(row["created_at"] ?? ""),
460
+ claimCodeExpiresAt: String(row["claim_code_expires_at"] ?? ""),
461
+ raw: row
462
+ };
463
+ }
351
464
  function toCurrencyBalance(row) {
352
465
  return {
353
466
  currencyId: row["currency_id"] ?? "",
@@ -390,7 +503,7 @@ var InvoServer = class {
390
503
  this.auth = { kind: "game-secret", secret: config.gameSecret };
391
504
  }
392
505
  /** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
393
- async mintPlayerToken(input) {
506
+ async mintPlayerToken(input, opts) {
394
507
  if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
395
508
  throw invalidInput("playerEmail", input.playerEmail, "is required");
396
509
  }
@@ -398,7 +511,7 @@ var InvoServer = class {
398
511
  "/api/sdk/player-token",
399
512
  { player_email: input.playerEmail },
400
513
  this.auth,
401
- { idempotent: true }
514
+ { idempotent: true, signal: opts?.signal }
402
515
  // safe to retry: re-mint just issues a fresh token
403
516
  );
404
517
  return {
@@ -408,7 +521,7 @@ var InvoServer = class {
408
521
  };
409
522
  }
410
523
  /** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
411
- async initiateSend(input) {
524
+ async initiateSend(input, opts) {
412
525
  const raw = await this.http.post(
413
526
  "/api/currency-sends/initiate-send",
414
527
  {
@@ -422,13 +535,13 @@ var InvoServer = class {
422
535
  amount: input.amount
423
536
  },
424
537
  this.auth,
425
- { idempotent: true }
538
+ { idempotent: true, signal: opts?.signal }
426
539
  // client_request_id makes this safe to retry (backend dedupes)
427
540
  );
428
541
  return this.toInitiateResult(raw);
429
542
  }
430
543
  /** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
431
- async initiateTransfer(input) {
544
+ async initiateTransfer(input, opts) {
432
545
  const raw = await this.http.post(
433
546
  "/api/transfers/initiate-transfer",
434
547
  {
@@ -442,7 +555,7 @@ var InvoServer = class {
442
555
  amount: input.amount
443
556
  },
444
557
  this.auth,
445
- { idempotent: true }
558
+ { idempotent: true, signal: opts?.signal }
446
559
  // client_request_id makes this safe to retry (backend dedupes)
447
560
  );
448
561
  return this.toInitiateResult(raw);
@@ -451,7 +564,7 @@ var InvoServer = class {
451
564
  * returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
452
565
  * handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
453
566
  * purchase.completed webhook. */
454
- async createCheckout(input) {
567
+ async createCheckout(input, opts) {
455
568
  if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
456
569
  throw invalidInput("playerEmail", input.playerEmail, "is required");
457
570
  }
@@ -467,7 +580,9 @@ var InvoServer = class {
467
580
  const raw = await this.http.post(
468
581
  "/api/checkout/sessions",
469
582
  body,
470
- this.auth
583
+ this.auth,
584
+ { signal: opts?.signal }
585
+ // not idempotent — never auto-retried
471
586
  );
472
587
  return {
473
588
  sessionId: String(raw["session_id"] ?? ""),
@@ -483,7 +598,7 @@ var InvoServer = class {
483
598
  * - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
484
599
  * - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
485
600
  */
486
- async purchaseCurrency(input) {
601
+ async purchaseCurrency(input, opts) {
487
602
  if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
488
603
  throw invalidInput("playerEmail", input.playerEmail, "is required");
489
604
  }
@@ -516,7 +631,7 @@ var InvoServer = class {
516
631
  "/api/currency-purchases/purchase-currency",
517
632
  body,
518
633
  this.auth,
519
- { idempotent: true }
634
+ { idempotent: true, signal: opts?.signal }
520
635
  // purchase_reference makes this safe to retry (backend dedupes)
521
636
  );
522
637
  return {
@@ -531,14 +646,14 @@ var InvoServer = class {
531
646
  };
532
647
  }
533
648
  /** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
534
- async confirmPayment(input) {
649
+ async confirmPayment(input, opts) {
535
650
  const body = { payment_intent_id: input.paymentIntentId };
536
651
  if (input.orderId) body["order_id"] = input.orderId;
537
652
  const raw = await this.http.post(
538
653
  "/api/currency-purchases/confirm-payment",
539
654
  body,
540
655
  this.auth,
541
- { idempotent: true }
656
+ { idempotent: true, signal: opts?.signal }
542
657
  // keyed by payment_intent_id — safe to retry
543
658
  );
544
659
  return {
@@ -549,7 +664,7 @@ var InvoServer = class {
549
664
  };
550
665
  }
551
666
  /** Fetch purchase status (order + financial summary + timeline). */
552
- async getOrderDetails(query) {
667
+ async getOrderDetails(query, opts) {
553
668
  if (!query.orderId && !query.transactionId) {
554
669
  throw new InvoError({
555
670
  message: "getOrderDetails requires an `orderId` or `transactionId`.",
@@ -562,7 +677,8 @@ var InvoServer = class {
562
677
  if (query.transactionId) q.set("transaction_id", query.transactionId);
563
678
  const raw = await this.http.get(
564
679
  `/api/currency-purchases/order-details?${q.toString()}`,
565
- this.auth
680
+ this.auth,
681
+ { signal: opts?.signal }
566
682
  );
567
683
  return toOrderDetails(raw);
568
684
  }
@@ -574,7 +690,7 @@ var InvoServer = class {
574
690
  * (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
575
691
  * item to your inventory off the `item.purchased` webhook, not just this response.
576
692
  */
577
- async purchaseItem(input) {
693
+ async purchaseItem(input, opts) {
578
694
  const required = [
579
695
  ["clientRequestId", input.clientRequestId],
580
696
  ["playerEmail", input.playerEmail],
@@ -616,7 +732,7 @@ var InvoServer = class {
616
732
  "/api/item-purchases/purchase-item",
617
733
  body,
618
734
  this.auth,
619
- { idempotent: true }
735
+ { idempotent: true, signal: opts?.signal }
620
736
  // client_request_id makes this safe to retry (dup → 409)
621
737
  );
622
738
  return {
@@ -631,7 +747,7 @@ var InvoServer = class {
631
747
  };
632
748
  }
633
749
  /** Paginated item-purchase history for a player (§4.8 companion read). */
634
- async getItemPurchaseHistory(query) {
750
+ async getItemPurchaseHistory(query, opts) {
635
751
  if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
636
752
  throw invalidInput("playerEmail", query.playerEmail, "is required");
637
753
  }
@@ -641,7 +757,8 @@ var InvoServer = class {
641
757
  if (query.offset != null) q.set("offset", String(query.offset));
642
758
  const raw = await this.http.get(
643
759
  `/api/item-purchases/player-purchase-history?${q.toString()}`,
644
- this.auth
760
+ this.auth,
761
+ { signal: opts?.signal }
645
762
  );
646
763
  return {
647
764
  history: Array.isArray(raw["item_purchase_history"]) ? raw["item_purchase_history"] : [],
@@ -651,7 +768,7 @@ var InvoServer = class {
651
768
  }
652
769
  /** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
653
770
  * (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
654
- async getItemOrderDetails(query) {
771
+ async getItemOrderDetails(query, opts) {
655
772
  const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
656
773
  (v) => typeof v === "string" && v.trim()
657
774
  );
@@ -668,12 +785,40 @@ var InvoServer = class {
668
785
  if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
669
786
  const raw = await this.http.get(
670
787
  `/api/item-purchases/order-details?${q.toString()}`,
671
- this.auth
788
+ this.auth,
789
+ { signal: opts?.signal }
672
790
  );
673
791
  return toOrderDetails(raw);
674
792
  }
793
+ /**
794
+ * Async iterator over a player's entire item-purchase history — pages through
795
+ * `getItemPurchaseHistory` (limit/offset) until exhausted, yielding one row at a time.
796
+ *
797
+ * ```ts
798
+ * for await (const row of invo.iterateItemPurchaseHistory({ playerEmail })) { … }
799
+ * ```
800
+ */
801
+ async *iterateItemPurchaseHistory(query, opts) {
802
+ if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
803
+ throw invalidInput("playerEmail", query.playerEmail, "is required");
804
+ }
805
+ const pageSize = typeof query.pageSize === "number" && query.pageSize > 0 ? Math.floor(query.pageSize) : 50;
806
+ let yielded = 0;
807
+ for (let offset = 0; ; offset += pageSize) {
808
+ const { history, pagination } = await this.getItemPurchaseHistory(
809
+ { playerEmail: query.playerEmail, limit: pageSize, offset },
810
+ opts
811
+ );
812
+ if (history.length === 0) return;
813
+ for (const row of history) yield row;
814
+ yielded += history.length;
815
+ if (history.length < pageSize) return;
816
+ const total = Number(pagination?.["total"]);
817
+ if (Number.isFinite(total) && yielded >= total) return;
818
+ }
819
+ }
675
820
  /** Read a player's currency balances, by email or playerId (game-secret). */
676
- async getPlayerBalance(query) {
821
+ async getPlayerBalance(query, opts) {
677
822
  let path;
678
823
  if (query.playerId != null && String(query.playerId).trim()) {
679
824
  path = `/api/player-balances/player/${encodeURIComponent(String(query.playerId))}`;
@@ -686,7 +831,7 @@ var InvoServer = class {
686
831
  status: 0
687
832
  });
688
833
  }
689
- const raw = await this.http.get(path, this.auth);
834
+ const raw = await this.http.get(path, this.auth, { signal: opts?.signal });
690
835
  const rows = Array.isArray(raw["balances"]) ? raw["balances"] : [];
691
836
  return {
692
837
  player: raw["player"] ?? {},
@@ -695,6 +840,30 @@ var InvoServer = class {
695
840
  raw
696
841
  };
697
842
  }
843
+ /**
844
+ * List LIVE, unclaimed inbound sends/transfers addressed to a player at YOUR game —
845
+ * the source of truth for a "you have X to collect" badge. Pass the player's email
846
+ * or phone. Match `toPhone` to the logged-in player (don't require `toIdentityId`,
847
+ * which is null when the phone maps to more than one of your players). Pairs with the
848
+ * `transfer.claim_pending` webhook (the webhook is the wake-up; this is the list).
849
+ */
850
+ async getInboundPending(query, opts) {
851
+ const hasEmail = typeof query.playerEmail === "string" && query.playerEmail.trim();
852
+ const hasPhone = typeof query.playerPhone === "string" && query.playerPhone.trim();
853
+ if (!hasEmail && !hasPhone) {
854
+ throw invalidInput("query", query, "requires a playerEmail or playerPhone");
855
+ }
856
+ const q = new URLSearchParams();
857
+ if (hasEmail) q.set("player_email", query.playerEmail.trim());
858
+ if (hasPhone) q.set("player_phone", query.playerPhone.trim());
859
+ const raw = await this.http.get(
860
+ `/api/transfers/inbound-pending?${q.toString()}`,
861
+ this.auth,
862
+ { signal: opts?.signal }
863
+ );
864
+ const rows = Array.isArray(raw["inbound_pending"]) ? raw["inbound_pending"] : [];
865
+ return { inboundPending: rows.map(toInboundPendingItem), raw };
866
+ }
698
867
  toInitiateResult(raw) {
699
868
  const vm = raw["verification_method"];
700
869
  const guardian = raw["guardian_approval"];
@@ -709,6 +878,8 @@ var InvoServer = class {
709
878
 
710
879
  exports.InvoError = InvoError;
711
880
  exports.InvoServer = InvoServer;
881
+ exports.createWebhookHandler = createWebhookHandler;
712
882
  exports.verifyWebhook = verifyWebhook;
883
+ exports.verifyWebhookAsync = verifyWebhookAsync;
713
884
  //# sourceMappingURL=server.cjs.map
714
885
  //# sourceMappingURL=server.cjs.map
package/dist/server.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as ServerConfig, P as PlayerToken, f as InitiateSendInput, g as InitiateResult, h as InitiateTransferInput, i as CreateCheckoutInput, j as CreateCheckoutResult, k as PurchaseInput, l as PurchaseResult, m as ConfirmPaymentResult, O as OrderDetailsResult, n as PurchaseItemInput, o as PurchaseItemResult, p as ItemHistoryQuery, q as ItemHistoryResult, r as ItemOrderQuery, s as PlayerBalanceQuery, t as PlayerBalanceResult } from './types-CBkoUymV.cjs';
2
- export { u as CurrencyBalance, I as InvoError, b as InvoErrorInfo, c as InvoHooks, d as InvoRequestInfo, e as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBkoUymV.cjs';
1
+ import { I as InvoError, S as ServerConfig, a as CallOptions, P as PlayerToken, g as InitiateSendInput, h as InitiateResult, i as InitiateTransferInput, j as CreateCheckoutInput, k as CreateCheckoutResult, l as PurchaseInput, m as PurchaseResult, n as ConfirmPaymentResult, O as OrderDetailsResult, o as PurchaseItemInput, p as PurchaseItemResult, q as ItemHistoryQuery, r as ItemHistoryResult, s as ItemOrderQuery, t as PlayerBalanceQuery, u as PlayerBalanceResult, v as InboundPendingQuery, w as InboundPendingResult } from './types-CBMLNwbe.cjs';
2
+ export { x as CurrencyBalance, y as InboundPendingItem, c as InvoErrorInfo, d as InvoHooks, e as InvoRequestInfo, f as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBMLNwbe.cjs';
3
3
 
4
4
  interface VerifyWebhookOptions {
5
5
  /** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
@@ -94,15 +94,54 @@ type InvoWebhookEvent = (WebhookBase & {
94
94
  data: Record<string, unknown>;
95
95
  });
96
96
  /**
97
- * Verify an Invo webhook and return the parsed, typed event.
97
+ * Verify an Invo webhook and return the parsed, typed event. **Synchronous; uses
98
+ * `node:crypto`** — for non-Node runtimes (Cloudflare Workers, Deno, Vercel/Netlify
99
+ * Edge, Bun edge) use {@link verifyWebhookAsync}.
98
100
  *
99
- * @param rawBody The exact raw request body (Buffer/Uint8Array or string).
101
+ * @param rawBody The exact raw request body (bytes or string) — never a re-serialized object.
100
102
  * @param signatureHeader The `X-Invo-Signature` header value.
101
- * @param secret Your signing secret, or an array of secrets (to accept old + new during rotation).
102
- * @throws InvoError (status 0) with code WEBHOOK_SIGNATURE_MISSING | WEBHOOK_TIMESTAMP_EXPIRED |
103
- * WEBHOOK_SIGNATURE_INVALID | WEBHOOK_MALFORMED on any failure.
103
+ * @param secret Your signing secret, or an array (to accept old + new during rotation).
104
+ * @throws InvoError (status 0): WEBHOOK_SIGNATURE_MISSING | WEBHOOK_SECRET_MISSING |
105
+ * WEBHOOK_TIMESTAMP_EXPIRED | WEBHOOK_SIGNATURE_INVALID | WEBHOOK_MALFORMED.
104
106
  */
105
107
  declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string | string[], opts?: VerifyWebhookOptions): InvoWebhookEvent;
108
+ /**
109
+ * Cross-runtime async variant of {@link verifyWebhook}. Uses the Web Crypto API
110
+ * (`crypto.subtle`) instead of `node:crypto`, so it works on Cloudflare Workers,
111
+ * Deno, Vercel/Netlify Edge, Bun, and modern browsers (as well as Node ≥ 18).
112
+ * Same arguments, errors, and return value — just `await` it.
113
+ */
114
+ declare function verifyWebhookAsync(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string | string[], opts?: VerifyWebhookOptions): Promise<InvoWebhookEvent>;
115
+ interface WebhookHandlerOptions {
116
+ /** Signing secret, or an array to accept old + new during rotation. */
117
+ secret: string | string[];
118
+ /** Called with the verified, typed event after the signature passes. De-dupe on
119
+ * `ctx.idempotencyKey`. Throw to return a 500 (Invo will retry). */
120
+ onEvent: (event: InvoWebhookEvent, ctx: {
121
+ idempotencyKey: string | null;
122
+ request: Request;
123
+ }) => void | Promise<void>;
124
+ /** Optional: handle a verification failure. Return a Response to override the default 400. */
125
+ onError?: (error: InvoError, request: Request) => void | Response | Promise<void | Response>;
126
+ /** Replay tolerance (seconds). Default 300. */
127
+ toleranceSec?: number;
128
+ }
129
+ /**
130
+ * Build a webhook route handler from the Fetch API `(Request) => Promise<Response>` —
131
+ * works in Next.js App Router route handlers, Cloudflare Workers, Deno, Hono, and Bun.
132
+ * It reads the raw body, verifies with {@link verifyWebhookAsync}, and on success calls
133
+ * `onEvent` with the typed event and the idempotency key (de-dupe is yours). Verification
134
+ * failures return `400`; a throwing `onEvent` returns `500` so Invo retries.
135
+ *
136
+ * ```ts
137
+ * // app/invo/webhooks/route.ts (Next.js)
138
+ * export const POST = createWebhookHandler({
139
+ * secret: process.env.INVO_WEBHOOK_SECRET!,
140
+ * onEvent: async (event, { idempotencyKey }) => { await grantValue(event); },
141
+ * });
142
+ * ```
143
+ */
144
+ declare function createWebhookHandler(opts: WebhookHandlerOptions): (request: Request) => Promise<Response>;
106
145
 
107
146
  declare class InvoServer {
108
147
  private readonly http;
@@ -111,16 +150,16 @@ declare class InvoServer {
111
150
  /** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
112
151
  mintPlayerToken(input: {
113
152
  playerEmail: string;
114
- }): Promise<PlayerToken>;
153
+ }, opts?: CallOptions): Promise<PlayerToken>;
115
154
  /** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
116
- initiateSend(input: InitiateSendInput): Promise<InitiateResult>;
155
+ initiateSend(input: InitiateSendInput, opts?: CallOptions): Promise<InitiateResult>;
117
156
  /** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
118
- initiateTransfer(input: InitiateTransferInput): Promise<InitiateResult>;
157
+ initiateTransfer(input: InitiateTransferInput, opts?: CallOptions): Promise<InitiateResult>;
119
158
  /** Create a hosted checkout session (the recommended purchase path). Open the
120
159
  * returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
121
160
  * handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
122
161
  * purchase.completed webhook. */
123
- createCheckout(input: CreateCheckoutInput): Promise<CreateCheckoutResult>;
162
+ createCheckout(input: CreateCheckoutInput, opts?: CallOptions): Promise<CreateCheckoutResult>;
124
163
  /**
125
164
  * Direct purchase via the rail selector. Use when you need a specific rail.
126
165
  * - rail "platform" (default): standard card. May return status "requires_action"
@@ -128,17 +167,17 @@ declare class InvoServer {
128
167
  * - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
129
168
  * - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
130
169
  */
131
- purchaseCurrency(input: PurchaseInput): Promise<PurchaseResult>;
170
+ purchaseCurrency(input: PurchaseInput, opts?: CallOptions): Promise<PurchaseResult>;
132
171
  /** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
133
172
  confirmPayment(input: {
134
173
  paymentIntentId: string;
135
174
  orderId?: string;
136
- }): Promise<ConfirmPaymentResult>;
175
+ }, opts?: CallOptions): Promise<ConfirmPaymentResult>;
137
176
  /** Fetch purchase status (order + financial summary + timeline). */
138
177
  getOrderDetails(query: {
139
178
  orderId?: string;
140
179
  transactionId?: string;
141
- }): Promise<OrderDetailsResult>;
180
+ }, opts?: CallOptions): Promise<OrderDetailsResult>;
142
181
  /**
143
182
  * Buy an in-game item by SPENDING the player's existing game currency (§4.8).
144
183
  * No real money, no payment rail, no passkey — it's a balance debit, authenticated
@@ -147,15 +186,35 @@ declare class InvoServer {
147
186
  * (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
148
187
  * item to your inventory off the `item.purchased` webhook, not just this response.
149
188
  */
150
- purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
189
+ purchaseItem(input: PurchaseItemInput, opts?: CallOptions): Promise<PurchaseItemResult>;
151
190
  /** Paginated item-purchase history for a player (§4.8 companion read). */
152
- getItemPurchaseHistory(query: ItemHistoryQuery): Promise<ItemHistoryResult>;
191
+ getItemPurchaseHistory(query: ItemHistoryQuery, opts?: CallOptions): Promise<ItemHistoryResult>;
153
192
  /** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
154
193
  * (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
155
- getItemOrderDetails(query: ItemOrderQuery): Promise<OrderDetailsResult>;
194
+ getItemOrderDetails(query: ItemOrderQuery, opts?: CallOptions): Promise<OrderDetailsResult>;
195
+ /**
196
+ * Async iterator over a player's entire item-purchase history — pages through
197
+ * `getItemPurchaseHistory` (limit/offset) until exhausted, yielding one row at a time.
198
+ *
199
+ * ```ts
200
+ * for await (const row of invo.iterateItemPurchaseHistory({ playerEmail })) { … }
201
+ * ```
202
+ */
203
+ iterateItemPurchaseHistory(query: {
204
+ playerEmail: string;
205
+ pageSize?: number;
206
+ }, opts?: CallOptions): AsyncGenerator<Record<string, unknown>, void, unknown>;
156
207
  /** Read a player's currency balances, by email or playerId (game-secret). */
157
- getPlayerBalance(query: PlayerBalanceQuery): Promise<PlayerBalanceResult>;
208
+ getPlayerBalance(query: PlayerBalanceQuery, opts?: CallOptions): Promise<PlayerBalanceResult>;
209
+ /**
210
+ * List LIVE, unclaimed inbound sends/transfers addressed to a player at YOUR game —
211
+ * the source of truth for a "you have X to collect" badge. Pass the player's email
212
+ * or phone. Match `toPhone` to the logged-in player (don't require `toIdentityId`,
213
+ * which is null when the phone maps to more than one of your players). Pairs with the
214
+ * `transfer.claim_pending` webhook (the webhook is the wake-up; this is the list).
215
+ */
216
+ getInboundPending(query: InboundPendingQuery, opts?: CallOptions): Promise<InboundPendingResult>;
158
217
  private toInitiateResult;
159
218
  }
160
219
 
161
- export { ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, OrderDetailsResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, type TransferEventData, type VerifyWebhookOptions, verifyWebhook };
220
+ export { CallOptions, ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, InboundPendingQuery, InboundPendingResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoError, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, OrderDetailsResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, type TransferEventData, type VerifyWebhookOptions, type WebhookHandlerOptions, createWebhookHandler, verifyWebhook, verifyWebhookAsync };