@invonetwork/web-sdk 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ var crypto = require('crypto');
4
+
3
5
  // src/shared/errors.ts
4
6
  var InvoError = class _InvoError extends Error {
5
7
  constructor(args) {
@@ -8,6 +10,7 @@ var InvoError = class _InvoError extends Error {
8
10
  this.code = args.code;
9
11
  this.status = args.status;
10
12
  this.body = args.body ?? null;
13
+ this.requestId = args.requestId;
11
14
  Object.setPrototypeOf(this, _InvoError.prototype);
12
15
  }
13
16
  /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
@@ -45,7 +48,7 @@ var InvoError = class _InvoError extends Error {
45
48
  return this.body && typeof this.body === "object" ? this.body : {};
46
49
  }
47
50
  };
48
- function errorFromResponse(status, body) {
51
+ function errorFromResponse(status, body, requestId) {
49
52
  let message = `INVO request failed (HTTP ${status})`;
50
53
  let code;
51
54
  if (body && typeof body === "object") {
@@ -56,11 +59,12 @@ function errorFromResponse(status, body) {
56
59
  } else if (typeof body === "string" && body.trim()) {
57
60
  message = body;
58
61
  }
59
- return new InvoError({ message, code, status, body });
62
+ return new InvoError({ message, code, status, body, requestId });
60
63
  }
61
64
 
62
65
  // src/shared/http.ts
63
66
  var DEFAULT_TIMEOUT = 3e4;
67
+ var MAX_RETRY_AFTER_MS = 2e4;
64
68
  function assertSecureBaseUrl(baseUrl) {
65
69
  let u;
66
70
  try {
@@ -74,7 +78,7 @@ function assertSecureBaseUrl(baseUrl) {
74
78
  `baseUrl must use https:// (got "${u.protocol}//"). Plaintext would expose the token/secret on the wire.`
75
79
  );
76
80
  }
77
- var Http = class {
81
+ var _Http = class _Http {
78
82
  constructor(opts) {
79
83
  this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
80
84
  this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT;
@@ -86,12 +90,21 @@ var Http = class {
86
90
  }
87
91
  this.fetchImpl = f;
88
92
  this.userAgent = opts.userAgent;
93
+ this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
94
+ this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 250;
95
+ this.hooks = opts.hooks;
89
96
  }
90
- async post(path, body, auth) {
91
- return this.request("POST", path, body, auth);
97
+ /**
98
+ * @param opts.idempotent - mark this POST safe to auto-retry (it carries an
99
+ * idempotency key, or has no side effect). Default false: non-idempotent POSTs
100
+ * (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
101
+ */
102
+ async post(path, body, auth, opts) {
103
+ return this.request("POST", path, body, auth, opts?.idempotent ?? false);
92
104
  }
105
+ // GET is always idempotent → safe to retry.
93
106
  async get(path, auth) {
94
- return this.request("GET", path, void 0, auth);
107
+ return this.request("GET", path, void 0, auth, true);
95
108
  }
96
109
  authHeaders(auth) {
97
110
  switch (auth.kind) {
@@ -103,7 +116,7 @@ var Http = class {
103
116
  return {};
104
117
  }
105
118
  }
106
- async request(method, path, body, auth) {
119
+ async request(method, path, body, auth, idempotent) {
107
120
  const url = `${this.baseUrl}${path}`;
108
121
  const headers = {
109
122
  Accept: "application/json",
@@ -111,38 +124,188 @@ var Http = class {
111
124
  };
112
125
  if (this.userAgent) headers["User-Agent"] = this.userAgent;
113
126
  if (body !== void 0) headers["Content-Type"] = "application/json";
114
- const controller = new AbortController();
115
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
116
- let res;
117
- try {
118
- res = await this.fetchImpl(url, {
127
+ const payload = body !== void 0 ? JSON.stringify(body) : void 0;
128
+ for (let attempt = 0; ; attempt++) {
129
+ const start = Date.now();
130
+ this.fire("onRequest", { method, url, attempt });
131
+ const controller = new AbortController();
132
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
133
+ let res;
134
+ let networkError;
135
+ try {
136
+ res = await this.fetchImpl(url, { method, headers, body: payload, signal: controller.signal });
137
+ } catch (err) {
138
+ networkError = new InvoError({
139
+ message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
140
+ status: 0,
141
+ body: null
142
+ });
143
+ } finally {
144
+ clearTimeout(timer);
145
+ }
146
+ if (networkError) {
147
+ const willRetry = idempotent && attempt < this.maxRetries;
148
+ this.fire("onError", { method, url, attempt, error: networkError, willRetry });
149
+ if (willRetry) {
150
+ await sleep(this.backoff(attempt));
151
+ continue;
152
+ }
153
+ throw networkError;
154
+ }
155
+ const requestId = pickRequestId(res.headers);
156
+ const text = await res.text();
157
+ let parsed = null;
158
+ if (text) {
159
+ try {
160
+ parsed = JSON.parse(text);
161
+ } catch {
162
+ parsed = text;
163
+ }
164
+ }
165
+ this.fire("onResponse", {
119
166
  method,
120
- headers,
121
- body: body !== void 0 ? JSON.stringify(body) : void 0,
122
- signal: controller.signal
123
- });
124
- } catch (err) {
125
- throw new InvoError({
126
- message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
127
- status: 0,
128
- body: null
167
+ url,
168
+ attempt,
169
+ status: res.status,
170
+ durationMs: Date.now() - start,
171
+ requestId
129
172
  });
130
- } finally {
131
- clearTimeout(timer);
132
- }
133
- const text = await res.text();
134
- let parsed = null;
135
- if (text) {
136
- try {
137
- parsed = JSON.parse(text);
138
- } catch {
139
- parsed = text;
173
+ if (!res.ok) {
174
+ const err = errorFromResponse(res.status, parsed, requestId);
175
+ let wait;
176
+ if (idempotent && attempt < this.maxRetries && _Http.RETRIABLE_STATUS.has(res.status)) {
177
+ if (res.status === 429) {
178
+ const ra = retryAfterMs(parsed, res.headers);
179
+ wait = ra === void 0 ? this.backoff(attempt) : ra <= MAX_RETRY_AFTER_MS ? ra : void 0;
180
+ } else {
181
+ wait = this.backoff(attempt);
182
+ }
183
+ }
184
+ this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
185
+ if (wait !== void 0) {
186
+ await sleep(wait);
187
+ continue;
188
+ }
189
+ throw err;
140
190
  }
191
+ return parsed ?? {};
141
192
  }
142
- if (!res.ok) throw errorFromResponse(res.status, parsed);
143
- return parsed ?? {};
193
+ }
194
+ /** Invoke an observability hook, swallowing any error it throws (best-effort). */
195
+ fire(name, info) {
196
+ const hook = this.hooks?.[name];
197
+ if (!hook) return;
198
+ try {
199
+ hook(info);
200
+ } catch {
201
+ }
202
+ }
203
+ /** Exponential backoff with full jitter: base * 2^attempt, randomized. */
204
+ backoff(attempt) {
205
+ const ceil = this.retryBaseDelayMs * 2 ** attempt;
206
+ return Math.floor(Math.random() * ceil) + this.retryBaseDelayMs;
144
207
  }
145
208
  };
209
+ /** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
210
+ _Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
211
+ var Http = _Http;
212
+ function sleep(ms) {
213
+ return new Promise((resolve) => setTimeout(resolve, ms));
214
+ }
215
+ function pickRequestId(headers) {
216
+ if (!headers || typeof headers.get !== "function") return void 0;
217
+ return headers.get("x-invo-request-id") ?? headers.get("x-request-id") ?? void 0;
218
+ }
219
+ function retryAfterMs(parsed, headers) {
220
+ if (parsed && typeof parsed === "object") {
221
+ const v = parsed["retry_after"];
222
+ const n = typeof v === "string" ? Number(v) : v;
223
+ if (typeof n === "number" && Number.isFinite(n) && n >= 0) return n * 1e3;
224
+ }
225
+ const header = headers && typeof headers.get === "function" ? headers.get("retry-after") : null;
226
+ if (header) {
227
+ const n = Number(header);
228
+ if (Number.isFinite(n) && n >= 0) return n * 1e3;
229
+ }
230
+ return void 0;
231
+ }
232
+ var DEFAULT_TOLERANCE_SEC = 300;
233
+ var ENCODER = new TextEncoder();
234
+ function toBytes(body) {
235
+ return typeof body === "string" ? ENCODER.encode(body) : body;
236
+ }
237
+ function concatBytes(a, b) {
238
+ const out = new Uint8Array(a.length + b.length);
239
+ out.set(a, 0);
240
+ out.set(b, a.length);
241
+ return out;
242
+ }
243
+ function webhookError(message, code) {
244
+ return new InvoError({ message, code, status: 0 });
245
+ }
246
+ function parseSignatureHeader(header) {
247
+ let t = "";
248
+ const sigs = [];
249
+ for (const part of header.split(",")) {
250
+ const idx = part.indexOf("=");
251
+ if (idx === -1) continue;
252
+ const key = part.slice(0, idx).trim();
253
+ const val = part.slice(idx + 1).trim();
254
+ if (key === "t") t = val;
255
+ else if (key === "v1" && val) sigs.push(val);
256
+ }
257
+ return { t, sigs };
258
+ }
259
+ function hmacHex(secret, message) {
260
+ return crypto.createHmac("sha256", secret).update(message).digest("hex");
261
+ }
262
+ function safeEqualHex(a, b) {
263
+ if (a.length !== b.length) return false;
264
+ let diff = 0;
265
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
266
+ return diff === 0;
267
+ }
268
+ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
269
+ if (!signatureHeader) {
270
+ throw webhookError("Missing X-Invo-Signature header.", "WEBHOOK_SIGNATURE_MISSING");
271
+ }
272
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(Boolean);
273
+ if (secrets.length === 0) {
274
+ throw webhookError("A signing secret is required to verify webhooks.", "WEBHOOK_SECRET_MISSING");
275
+ }
276
+ const { t, sigs } = parseSignatureHeader(signatureHeader);
277
+ if (!t || sigs.length === 0) {
278
+ throw webhookError("Malformed X-Invo-Signature header.", "WEBHOOK_MALFORMED");
279
+ }
280
+ const ts = Number(t);
281
+ const now = opts.nowSec ?? Math.floor(Date.now() / 1e3);
282
+ const tolerance = opts.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
283
+ if (!Number.isFinite(ts) || Math.abs(now - ts) > tolerance) {
284
+ throw webhookError(
285
+ `Webhook timestamp outside the ${tolerance}s tolerance (replay guard).`,
286
+ "WEBHOOK_TIMESTAMP_EXPIRED"
287
+ );
288
+ }
289
+ const bodyBytes = toBytes(rawBody);
290
+ 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
+ }
298
+ let parsed;
299
+ try {
300
+ parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
301
+ } catch {
302
+ throw webhookError("Webhook body is not valid JSON.", "WEBHOOK_MALFORMED");
303
+ }
304
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.event_type !== "string") {
305
+ throw webhookError("Webhook body is not a valid event object.", "WEBHOOK_MALFORMED");
306
+ }
307
+ return parsed;
308
+ }
146
309
 
147
310
  // src/server.ts
148
311
  var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
@@ -177,6 +340,26 @@ function parseMoney(value, max, label) {
177
340
  function assertUsdAmount(usdAmount) {
178
341
  parseMoney(usdAmount, MAX_USD_AMOUNT, "usdAmount");
179
342
  }
343
+ function toOrderDetails(raw) {
344
+ return {
345
+ order: raw["order"] ?? {},
346
+ financialSummary: raw["financial_summary"] ?? {},
347
+ statusTimeline: raw["status_timeline"] ?? null,
348
+ raw
349
+ };
350
+ }
351
+ function toCurrencyBalance(row) {
352
+ return {
353
+ currencyId: row["currency_id"] ?? "",
354
+ currencyName: String(row["currency_name"] ?? ""),
355
+ currencySymbol: String(row["currency_symbol"] ?? ""),
356
+ availableBalance: String(row["available_balance"] ?? ""),
357
+ reservedBalance: String(row["reserved_balance"] ?? ""),
358
+ totalBalance: String(row["total_balance"] ?? ""),
359
+ lastTransaction: row["last_transaction"],
360
+ raw: row
361
+ };
362
+ }
180
363
  function requireField(value, field, raw) {
181
364
  const s = value == null ? "" : String(value);
182
365
  if (!s) {
@@ -198,17 +381,25 @@ var InvoServer = class {
198
381
  baseUrl: config.baseUrl,
199
382
  timeoutMs: config.timeoutMs,
200
383
  fetchImpl: config.fetch,
201
- userAgent: DEFAULT_UA
384
+ userAgent: DEFAULT_UA,
202
385
  // must be a non-blocked UA (handoff doc §9)
386
+ maxRetries: config.maxRetries,
387
+ retryBaseDelayMs: config.retryBaseDelayMs,
388
+ hooks: config.hooks
203
389
  });
204
390
  this.auth = { kind: "game-secret", secret: config.gameSecret };
205
391
  }
206
392
  /** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
207
393
  async mintPlayerToken(input) {
394
+ if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
395
+ throw invalidInput("playerEmail", input.playerEmail, "is required");
396
+ }
208
397
  const raw = await this.http.post(
209
398
  "/api/sdk/player-token",
210
399
  { player_email: input.playerEmail },
211
- this.auth
400
+ this.auth,
401
+ { idempotent: true }
402
+ // safe to retry: re-mint just issues a fresh token
212
403
  );
213
404
  return {
214
405
  token: requireField(raw["token"], "token", raw),
@@ -230,7 +421,9 @@ var InvoServer = class {
230
421
  receiving_game_id: input.receivingGameId,
231
422
  amount: input.amount
232
423
  },
233
- this.auth
424
+ this.auth,
425
+ { idempotent: true }
426
+ // client_request_id makes this safe to retry (backend dedupes)
234
427
  );
235
428
  return this.toInitiateResult(raw);
236
429
  }
@@ -248,7 +441,9 @@ var InvoServer = class {
248
441
  target_game_id: input.targetGameId,
249
442
  amount: input.amount
250
443
  },
251
- this.auth
444
+ this.auth,
445
+ { idempotent: true }
446
+ // client_request_id makes this safe to retry (backend dedupes)
252
447
  );
253
448
  return this.toInitiateResult(raw);
254
449
  }
@@ -257,6 +452,9 @@ var InvoServer = class {
257
452
  * handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
258
453
  * purchase.completed webhook. */
259
454
  async createCheckout(input) {
455
+ if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
456
+ throw invalidInput("playerEmail", input.playerEmail, "is required");
457
+ }
260
458
  assertUsdAmount(input.usdAmount);
261
459
  const body = {
262
460
  player_email: input.playerEmail,
@@ -286,6 +484,9 @@ var InvoServer = class {
286
484
  * - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
287
485
  */
288
486
  async purchaseCurrency(input) {
487
+ if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
488
+ throw invalidInput("playerEmail", input.playerEmail, "is required");
489
+ }
289
490
  assertUsdAmount(input.usdAmount);
290
491
  if (!input.purchaseReference) {
291
492
  throw new InvoError({
@@ -314,7 +515,9 @@ var InvoServer = class {
314
515
  const raw = await this.http.post(
315
516
  "/api/currency-purchases/purchase-currency",
316
517
  body,
317
- this.auth
518
+ this.auth,
519
+ { idempotent: true }
520
+ // purchase_reference makes this safe to retry (backend dedupes)
318
521
  );
319
522
  return {
320
523
  status: String(raw["status"] ?? ""),
@@ -331,11 +534,19 @@ var InvoServer = class {
331
534
  async confirmPayment(input) {
332
535
  const body = { payment_intent_id: input.paymentIntentId };
333
536
  if (input.orderId) body["order_id"] = input.orderId;
334
- return this.http.post(
537
+ const raw = await this.http.post(
335
538
  "/api/currency-purchases/confirm-payment",
336
539
  body,
337
- this.auth
540
+ this.auth,
541
+ { idempotent: true }
542
+ // keyed by payment_intent_id — safe to retry
338
543
  );
544
+ return {
545
+ status: String(raw["status"] ?? ""),
546
+ transactionId: raw["transaction_id"],
547
+ newBalance: raw["new_balance"] ?? null,
548
+ raw
549
+ };
339
550
  }
340
551
  /** Fetch purchase status (order + financial summary + timeline). */
341
552
  async getOrderDetails(query) {
@@ -349,10 +560,11 @@ var InvoServer = class {
349
560
  const q = new URLSearchParams();
350
561
  if (query.orderId) q.set("order_id", query.orderId);
351
562
  if (query.transactionId) q.set("transaction_id", query.transactionId);
352
- return this.http.get(
563
+ const raw = await this.http.get(
353
564
  `/api/currency-purchases/order-details?${q.toString()}`,
354
565
  this.auth
355
566
  );
567
+ return toOrderDetails(raw);
356
568
  }
357
569
  /**
358
570
  * Buy an in-game item by SPENDING the player's existing game currency (§4.8).
@@ -403,7 +615,9 @@ var InvoServer = class {
403
615
  const raw = await this.http.post(
404
616
  "/api/item-purchases/purchase-item",
405
617
  body,
406
- this.auth
618
+ this.auth,
619
+ { idempotent: true }
620
+ // client_request_id makes this safe to retry (dup → 409)
407
621
  );
408
622
  return {
409
623
  status: String(raw["status"] ?? ""),
@@ -425,10 +639,15 @@ var InvoServer = class {
425
639
  q.set("player_email", query.playerEmail);
426
640
  if (query.limit != null) q.set("limit", String(query.limit));
427
641
  if (query.offset != null) q.set("offset", String(query.offset));
428
- return this.http.get(
642
+ const raw = await this.http.get(
429
643
  `/api/item-purchases/player-purchase-history?${q.toString()}`,
430
644
  this.auth
431
645
  );
646
+ return {
647
+ history: Array.isArray(raw["item_purchase_history"]) ? raw["item_purchase_history"] : [],
648
+ pagination: raw["pagination"] ?? {},
649
+ raw
650
+ };
432
651
  }
433
652
  /** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
434
653
  * (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
@@ -447,10 +666,34 @@ var InvoServer = class {
447
666
  if (query.orderId) q.set("order_id", query.orderId);
448
667
  if (query.transactionId) q.set("transaction_id", query.transactionId);
449
668
  if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
450
- return this.http.get(
669
+ const raw = await this.http.get(
451
670
  `/api/item-purchases/order-details?${q.toString()}`,
452
671
  this.auth
453
672
  );
673
+ return toOrderDetails(raw);
674
+ }
675
+ /** Read a player's currency balances, by email or playerId (game-secret). */
676
+ async getPlayerBalance(query) {
677
+ let path;
678
+ if (query.playerId != null && String(query.playerId).trim()) {
679
+ path = `/api/player-balances/player/${encodeURIComponent(String(query.playerId))}`;
680
+ } else if (typeof query.playerEmail === "string" && query.playerEmail.trim()) {
681
+ path = `/api/player-balances/player/by-email/${encodeURIComponent(query.playerEmail)}`;
682
+ } else {
683
+ throw new InvoError({
684
+ message: "getPlayerBalance requires a playerEmail or playerId.",
685
+ code: "INVALID_INPUT",
686
+ status: 0
687
+ });
688
+ }
689
+ const raw = await this.http.get(path, this.auth);
690
+ const rows = Array.isArray(raw["balances"]) ? raw["balances"] : [];
691
+ return {
692
+ player: raw["player"] ?? {},
693
+ balances: rows.map(toCurrencyBalance),
694
+ summary: raw["summary"] ?? {},
695
+ raw
696
+ };
454
697
  }
455
698
  toInitiateResult(raw) {
456
699
  const vm = raw["verification_method"];
@@ -466,5 +709,6 @@ var InvoServer = class {
466
709
 
467
710
  exports.InvoError = InvoError;
468
711
  exports.InvoServer = InvoServer;
712
+ exports.verifyWebhook = verifyWebhook;
469
713
  //# sourceMappingURL=server.cjs.map
470
714
  //# sourceMappingURL=server.cjs.map
package/dist/server.d.cts CHANGED
@@ -1,5 +1,108 @@
1
- import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult, i as PurchaseItemInput, j as PurchaseItemResult, k as ItemHistoryQuery, l as ItemOrderQuery } from './errors-DV5QsftP.cjs';
2
- export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.cjs';
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';
3
+
4
+ interface VerifyWebhookOptions {
5
+ /** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
6
+ toleranceSec?: number;
7
+ /** Override the current time (unix seconds) — for tests. Defaults to Date.now(). */
8
+ nowSec?: number;
9
+ }
10
+ interface WebhookBase {
11
+ event_id: string;
12
+ idempotency_key: string;
13
+ schema_version: string;
14
+ created_at: string;
15
+ /** Your game_id. */
16
+ tenant_id: string;
17
+ }
18
+ interface PurchaseCompletedData {
19
+ transaction_id: string;
20
+ order_id: string;
21
+ player_email: string;
22
+ identity_id: string;
23
+ usd_amount: string;
24
+ currency_amount: string;
25
+ currency_name: string;
26
+ new_balance: string;
27
+ rail: string;
28
+ [key: string]: unknown;
29
+ }
30
+ interface PurchaseEventData {
31
+ transaction_id: string;
32
+ order_id?: string;
33
+ player_email?: string;
34
+ identity_id?: string;
35
+ /** Present on purchase.disputed, e.g. "lost". */
36
+ dispute_status?: string;
37
+ [key: string]: unknown;
38
+ }
39
+ interface ItemPurchasedData {
40
+ transaction_id: string;
41
+ order_id: string;
42
+ player_email: string;
43
+ identity_id: string;
44
+ item_id: string;
45
+ item_name: string;
46
+ item_quantity: number;
47
+ unit_price: string;
48
+ total_price: string;
49
+ currency_name: string;
50
+ new_balance: string;
51
+ fee_breakdown?: Record<string, unknown>;
52
+ [key: string]: unknown;
53
+ }
54
+ interface TransferEventData {
55
+ transaction_id: string;
56
+ /** "transfer" | "send". */
57
+ flow?: string;
58
+ amount?: string;
59
+ net_amount?: string;
60
+ /** "inbound" | "outbound" (relative to the receiving tenant). */
61
+ direction?: string;
62
+ /** On inbound claim_pending: the recipient's phone (match to your player). */
63
+ to_phone?: string;
64
+ /** Recipient's opaque identity, present only when the phone maps to a single player. */
65
+ to_identity_id?: string | null;
66
+ [key: string]: unknown;
67
+ }
68
+ type PurchaseEventType = "purchase.failed" | "purchase.refunded" | "purchase.disputed" | "purchase.fraud_warning";
69
+ type TransferEventType = "transfer.sent" | "transfer.received" | "transfer.claim_pending" | "transfer.claim_expired" | "transfer.refunded";
70
+ /**
71
+ * A verified webhook event. Discriminate on `event_type` to narrow `data`.
72
+ * Unknown/future event types fall through to the generic member (data is a record).
73
+ */
74
+ type InvoWebhookEvent = (WebhookBase & {
75
+ event_type: "purchase.completed";
76
+ data: PurchaseCompletedData;
77
+ }) | (WebhookBase & {
78
+ event_type: "item.purchased";
79
+ data: ItemPurchasedData;
80
+ }) | (WebhookBase & {
81
+ event_type: PurchaseEventType;
82
+ data: PurchaseEventData;
83
+ }) | (WebhookBase & {
84
+ event_type: TransferEventType;
85
+ data: TransferEventData;
86
+ }) | (WebhookBase & {
87
+ event_type: "payout.status_changed";
88
+ data: Record<string, unknown>;
89
+ }) | (WebhookBase & {
90
+ event_type: "webhook.test";
91
+ data: Record<string, unknown>;
92
+ }) | (WebhookBase & {
93
+ event_type: string;
94
+ data: Record<string, unknown>;
95
+ });
96
+ /**
97
+ * Verify an Invo webhook and return the parsed, typed event.
98
+ *
99
+ * @param rawBody The exact raw request body (Buffer/Uint8Array or string).
100
+ * @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.
104
+ */
105
+ declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string | string[], opts?: VerifyWebhookOptions): InvoWebhookEvent;
3
106
 
4
107
  declare class InvoServer {
5
108
  private readonly http;
@@ -30,12 +133,12 @@ declare class InvoServer {
30
133
  confirmPayment(input: {
31
134
  paymentIntentId: string;
32
135
  orderId?: string;
33
- }): Promise<Record<string, unknown>>;
136
+ }): Promise<ConfirmPaymentResult>;
34
137
  /** Fetch purchase status (order + financial summary + timeline). */
35
138
  getOrderDetails(query: {
36
139
  orderId?: string;
37
140
  transactionId?: string;
38
- }): Promise<Record<string, unknown>>;
141
+ }): Promise<OrderDetailsResult>;
39
142
  /**
40
143
  * Buy an in-game item by SPENDING the player's existing game currency (§4.8).
41
144
  * No real money, no payment rail, no passkey — it's a balance debit, authenticated
@@ -46,11 +149,13 @@ declare class InvoServer {
46
149
  */
47
150
  purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
48
151
  /** Paginated item-purchase history for a player (§4.8 companion read). */
49
- getItemPurchaseHistory(query: ItemHistoryQuery): Promise<Record<string, unknown>>;
152
+ getItemPurchaseHistory(query: ItemHistoryQuery): Promise<ItemHistoryResult>;
50
153
  /** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
51
154
  * (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
52
- getItemOrderDetails(query: ItemOrderQuery): Promise<Record<string, unknown>>;
155
+ getItemOrderDetails(query: ItemOrderQuery): Promise<OrderDetailsResult>;
156
+ /** Read a player's currency balances, by email or playerId (game-secret). */
157
+ getPlayerBalance(query: PlayerBalanceQuery): Promise<PlayerBalanceResult>;
53
158
  private toInitiateResult;
54
159
  }
55
160
 
56
- export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, ItemHistoryQuery, ItemOrderQuery, PlayerToken, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig };
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 };