@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.d.ts 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.js';
2
- export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.js';
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.js';
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.js';
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 };
package/dist/server.js CHANGED
@@ -1,5 +1,84 @@
1
- import { assertSecureBaseUrl, Http, InvoError } from './chunk-A44O4KC3.js';
2
- export { InvoError } from './chunk-A44O4KC3.js';
1
+ import { assertSecureBaseUrl, Http, InvoError } from './chunk-DV3WZGMH.js';
2
+ export { InvoError } from './chunk-DV3WZGMH.js';
3
+ import { createHmac } from 'crypto';
4
+
5
+ var DEFAULT_TOLERANCE_SEC = 300;
6
+ var ENCODER = new TextEncoder();
7
+ function toBytes(body) {
8
+ return typeof body === "string" ? ENCODER.encode(body) : body;
9
+ }
10
+ function concatBytes(a, b) {
11
+ const out = new Uint8Array(a.length + b.length);
12
+ out.set(a, 0);
13
+ out.set(b, a.length);
14
+ return out;
15
+ }
16
+ function webhookError(message, code) {
17
+ return new InvoError({ message, code, status: 0 });
18
+ }
19
+ function parseSignatureHeader(header) {
20
+ let t = "";
21
+ const sigs = [];
22
+ for (const part of header.split(",")) {
23
+ const idx = part.indexOf("=");
24
+ if (idx === -1) continue;
25
+ const key = part.slice(0, idx).trim();
26
+ const val = part.slice(idx + 1).trim();
27
+ if (key === "t") t = val;
28
+ else if (key === "v1" && val) sigs.push(val);
29
+ }
30
+ return { t, sigs };
31
+ }
32
+ function hmacHex(secret, message) {
33
+ return createHmac("sha256", secret).update(message).digest("hex");
34
+ }
35
+ function safeEqualHex(a, b) {
36
+ if (a.length !== b.length) return false;
37
+ let diff = 0;
38
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
39
+ return diff === 0;
40
+ }
41
+ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
42
+ if (!signatureHeader) {
43
+ throw webhookError("Missing X-Invo-Signature header.", "WEBHOOK_SIGNATURE_MISSING");
44
+ }
45
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(Boolean);
46
+ if (secrets.length === 0) {
47
+ throw webhookError("A signing secret is required to verify webhooks.", "WEBHOOK_SECRET_MISSING");
48
+ }
49
+ const { t, sigs } = parseSignatureHeader(signatureHeader);
50
+ if (!t || sigs.length === 0) {
51
+ throw webhookError("Malformed X-Invo-Signature header.", "WEBHOOK_MALFORMED");
52
+ }
53
+ const ts = Number(t);
54
+ const now = opts.nowSec ?? Math.floor(Date.now() / 1e3);
55
+ const tolerance = opts.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
56
+ if (!Number.isFinite(ts) || Math.abs(now - ts) > tolerance) {
57
+ throw webhookError(
58
+ `Webhook timestamp outside the ${tolerance}s tolerance (replay guard).`,
59
+ "WEBHOOK_TIMESTAMP_EXPIRED"
60
+ );
61
+ }
62
+ const bodyBytes = toBytes(rawBody);
63
+ const message = concatBytes(ENCODER.encode(`${t}.`), bodyBytes);
64
+ const matched = secrets.some((s) => {
65
+ const expected = hmacHex(s, message);
66
+ return sigs.some((sig) => safeEqualHex(sig, expected));
67
+ });
68
+ if (!matched) {
69
+ throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
70
+ }
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
74
+ } catch {
75
+ throw webhookError("Webhook body is not valid JSON.", "WEBHOOK_MALFORMED");
76
+ }
77
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.event_type !== "string") {
78
+ throw webhookError("Webhook body is not a valid event object.", "WEBHOOK_MALFORMED");
79
+ }
80
+ return parsed;
81
+ }
3
82
 
4
83
  // src/server.ts
5
84
  var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
@@ -34,6 +113,26 @@ function parseMoney(value, max, label) {
34
113
  function assertUsdAmount(usdAmount) {
35
114
  parseMoney(usdAmount, MAX_USD_AMOUNT, "usdAmount");
36
115
  }
116
+ function toOrderDetails(raw) {
117
+ return {
118
+ order: raw["order"] ?? {},
119
+ financialSummary: raw["financial_summary"] ?? {},
120
+ statusTimeline: raw["status_timeline"] ?? null,
121
+ raw
122
+ };
123
+ }
124
+ function toCurrencyBalance(row) {
125
+ return {
126
+ currencyId: row["currency_id"] ?? "",
127
+ currencyName: String(row["currency_name"] ?? ""),
128
+ currencySymbol: String(row["currency_symbol"] ?? ""),
129
+ availableBalance: String(row["available_balance"] ?? ""),
130
+ reservedBalance: String(row["reserved_balance"] ?? ""),
131
+ totalBalance: String(row["total_balance"] ?? ""),
132
+ lastTransaction: row["last_transaction"],
133
+ raw: row
134
+ };
135
+ }
37
136
  function requireField(value, field, raw) {
38
137
  const s = value == null ? "" : String(value);
39
138
  if (!s) {
@@ -55,17 +154,25 @@ var InvoServer = class {
55
154
  baseUrl: config.baseUrl,
56
155
  timeoutMs: config.timeoutMs,
57
156
  fetchImpl: config.fetch,
58
- userAgent: DEFAULT_UA
157
+ userAgent: DEFAULT_UA,
59
158
  // must be a non-blocked UA (handoff doc §9)
159
+ maxRetries: config.maxRetries,
160
+ retryBaseDelayMs: config.retryBaseDelayMs,
161
+ hooks: config.hooks
60
162
  });
61
163
  this.auth = { kind: "game-secret", secret: config.gameSecret };
62
164
  }
63
165
  /** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
64
166
  async mintPlayerToken(input) {
167
+ if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
168
+ throw invalidInput("playerEmail", input.playerEmail, "is required");
169
+ }
65
170
  const raw = await this.http.post(
66
171
  "/api/sdk/player-token",
67
172
  { player_email: input.playerEmail },
68
- this.auth
173
+ this.auth,
174
+ { idempotent: true }
175
+ // safe to retry: re-mint just issues a fresh token
69
176
  );
70
177
  return {
71
178
  token: requireField(raw["token"], "token", raw),
@@ -87,7 +194,9 @@ var InvoServer = class {
87
194
  receiving_game_id: input.receivingGameId,
88
195
  amount: input.amount
89
196
  },
90
- this.auth
197
+ this.auth,
198
+ { idempotent: true }
199
+ // client_request_id makes this safe to retry (backend dedupes)
91
200
  );
92
201
  return this.toInitiateResult(raw);
93
202
  }
@@ -105,7 +214,9 @@ var InvoServer = class {
105
214
  target_game_id: input.targetGameId,
106
215
  amount: input.amount
107
216
  },
108
- this.auth
217
+ this.auth,
218
+ { idempotent: true }
219
+ // client_request_id makes this safe to retry (backend dedupes)
109
220
  );
110
221
  return this.toInitiateResult(raw);
111
222
  }
@@ -114,6 +225,9 @@ var InvoServer = class {
114
225
  * handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
115
226
  * purchase.completed webhook. */
116
227
  async createCheckout(input) {
228
+ if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
229
+ throw invalidInput("playerEmail", input.playerEmail, "is required");
230
+ }
117
231
  assertUsdAmount(input.usdAmount);
118
232
  const body = {
119
233
  player_email: input.playerEmail,
@@ -143,6 +257,9 @@ var InvoServer = class {
143
257
  * - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
144
258
  */
145
259
  async purchaseCurrency(input) {
260
+ if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
261
+ throw invalidInput("playerEmail", input.playerEmail, "is required");
262
+ }
146
263
  assertUsdAmount(input.usdAmount);
147
264
  if (!input.purchaseReference) {
148
265
  throw new InvoError({
@@ -171,7 +288,9 @@ var InvoServer = class {
171
288
  const raw = await this.http.post(
172
289
  "/api/currency-purchases/purchase-currency",
173
290
  body,
174
- this.auth
291
+ this.auth,
292
+ { idempotent: true }
293
+ // purchase_reference makes this safe to retry (backend dedupes)
175
294
  );
176
295
  return {
177
296
  status: String(raw["status"] ?? ""),
@@ -188,11 +307,19 @@ var InvoServer = class {
188
307
  async confirmPayment(input) {
189
308
  const body = { payment_intent_id: input.paymentIntentId };
190
309
  if (input.orderId) body["order_id"] = input.orderId;
191
- return this.http.post(
310
+ const raw = await this.http.post(
192
311
  "/api/currency-purchases/confirm-payment",
193
312
  body,
194
- this.auth
313
+ this.auth,
314
+ { idempotent: true }
315
+ // keyed by payment_intent_id — safe to retry
195
316
  );
317
+ return {
318
+ status: String(raw["status"] ?? ""),
319
+ transactionId: raw["transaction_id"],
320
+ newBalance: raw["new_balance"] ?? null,
321
+ raw
322
+ };
196
323
  }
197
324
  /** Fetch purchase status (order + financial summary + timeline). */
198
325
  async getOrderDetails(query) {
@@ -206,10 +333,11 @@ var InvoServer = class {
206
333
  const q = new URLSearchParams();
207
334
  if (query.orderId) q.set("order_id", query.orderId);
208
335
  if (query.transactionId) q.set("transaction_id", query.transactionId);
209
- return this.http.get(
336
+ const raw = await this.http.get(
210
337
  `/api/currency-purchases/order-details?${q.toString()}`,
211
338
  this.auth
212
339
  );
340
+ return toOrderDetails(raw);
213
341
  }
214
342
  /**
215
343
  * Buy an in-game item by SPENDING the player's existing game currency (§4.8).
@@ -260,7 +388,9 @@ var InvoServer = class {
260
388
  const raw = await this.http.post(
261
389
  "/api/item-purchases/purchase-item",
262
390
  body,
263
- this.auth
391
+ this.auth,
392
+ { idempotent: true }
393
+ // client_request_id makes this safe to retry (dup → 409)
264
394
  );
265
395
  return {
266
396
  status: String(raw["status"] ?? ""),
@@ -282,10 +412,15 @@ var InvoServer = class {
282
412
  q.set("player_email", query.playerEmail);
283
413
  if (query.limit != null) q.set("limit", String(query.limit));
284
414
  if (query.offset != null) q.set("offset", String(query.offset));
285
- return this.http.get(
415
+ const raw = await this.http.get(
286
416
  `/api/item-purchases/player-purchase-history?${q.toString()}`,
287
417
  this.auth
288
418
  );
419
+ return {
420
+ history: Array.isArray(raw["item_purchase_history"]) ? raw["item_purchase_history"] : [],
421
+ pagination: raw["pagination"] ?? {},
422
+ raw
423
+ };
289
424
  }
290
425
  /** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
291
426
  * (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
@@ -304,10 +439,34 @@ var InvoServer = class {
304
439
  if (query.orderId) q.set("order_id", query.orderId);
305
440
  if (query.transactionId) q.set("transaction_id", query.transactionId);
306
441
  if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
307
- return this.http.get(
442
+ const raw = await this.http.get(
308
443
  `/api/item-purchases/order-details?${q.toString()}`,
309
444
  this.auth
310
445
  );
446
+ return toOrderDetails(raw);
447
+ }
448
+ /** Read a player's currency balances, by email or playerId (game-secret). */
449
+ async getPlayerBalance(query) {
450
+ let path;
451
+ if (query.playerId != null && String(query.playerId).trim()) {
452
+ path = `/api/player-balances/player/${encodeURIComponent(String(query.playerId))}`;
453
+ } else if (typeof query.playerEmail === "string" && query.playerEmail.trim()) {
454
+ path = `/api/player-balances/player/by-email/${encodeURIComponent(query.playerEmail)}`;
455
+ } else {
456
+ throw new InvoError({
457
+ message: "getPlayerBalance requires a playerEmail or playerId.",
458
+ code: "INVALID_INPUT",
459
+ status: 0
460
+ });
461
+ }
462
+ const raw = await this.http.get(path, this.auth);
463
+ const rows = Array.isArray(raw["balances"]) ? raw["balances"] : [];
464
+ return {
465
+ player: raw["player"] ?? {},
466
+ balances: rows.map(toCurrencyBalance),
467
+ summary: raw["summary"] ?? {},
468
+ raw
469
+ };
311
470
  }
312
471
  toInitiateResult(raw) {
313
472
  const vm = raw["verification_method"];
@@ -321,6 +480,6 @@ var InvoServer = class {
321
480
  }
322
481
  };
323
482
 
324
- export { InvoServer };
483
+ export { InvoServer, verifyWebhook };
325
484
  //# sourceMappingURL=server.js.map
326
485
  //# sourceMappingURL=server.js.map
@@ -1,3 +1,71 @@
1
+ /**
2
+ * Typed error for every INVO backend failure.
3
+ *
4
+ * The backend error envelope is usually `{ "error": <message>, "code": <STABLE_CODE> }`,
5
+ * but a few txn-state/claim errors carry only `error` (no `code`) and some use
6
+ * `{ "status": "error", "message": ... }`. So `code` may be undefined — branch on
7
+ * `code` when present, else fall back to `message`. See the handoff doc §8.
8
+ */
9
+ declare class InvoError extends Error {
10
+ /** Stable machine code from the backend (`code`), when present. */
11
+ readonly code: string | undefined;
12
+ /** HTTP status. */
13
+ readonly status: number;
14
+ /** Raw parsed body, for debugging / fields not surfaced here. */
15
+ readonly body: unknown;
16
+ /** Backend request id (from the response headers), when present — quote it in support tickets. */
17
+ readonly requestId: string | undefined;
18
+ constructor(args: {
19
+ message: string;
20
+ code?: string;
21
+ status: number;
22
+ body?: unknown;
23
+ requestId?: string;
24
+ });
25
+ /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
26
+ get isReceiverNotEnrolled(): boolean;
27
+ /** True if the session/SDK token has expired and the caller should re-mint + retry. */
28
+ get isTokenExpired(): boolean;
29
+ /**
30
+ * True if an item purchase failed because the player's balance was too low (§4.8 → 400).
31
+ * The backend carries `required_amount` + `current_balance` on the body for the UI.
32
+ * Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
33
+ * is a rate-limit, not a top-up condition) is NOT misclassified as this.
34
+ */
35
+ get isInsufficientBalance(): boolean;
36
+ /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
37
+ get isDuplicateRequest(): boolean;
38
+ /** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
39
+ get retryAfter(): number | undefined;
40
+ /** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
41
+ * The `in` operator throws on primitives, so callers must go through this. */
42
+ private bodyObject;
43
+ }
44
+
45
+ interface InvoRequestInfo {
46
+ method: string;
47
+ url: string;
48
+ /** 0-based attempt number (0 = first try, 1 = first retry, …). */
49
+ attempt: number;
50
+ }
51
+ interface InvoResponseInfo extends InvoRequestInfo {
52
+ status: number;
53
+ durationMs: number;
54
+ /** Backend request id from the response headers, when present. */
55
+ requestId?: string;
56
+ }
57
+ interface InvoErrorInfo extends InvoRequestInfo {
58
+ error: InvoError;
59
+ /** Whether the SDK will retry after this error. */
60
+ willRetry: boolean;
61
+ }
62
+ /** Optional observability callbacks. All are best-effort — a throwing hook is swallowed. */
63
+ interface InvoHooks {
64
+ onRequest?: (info: InvoRequestInfo) => void;
65
+ onResponse?: (info: InvoResponseInfo) => void;
66
+ onError?: (info: InvoErrorInfo) => void;
67
+ }
68
+
1
69
  /** Public, neutral payment-rail names. Provider/processor names are an internal
2
70
  * backend routing detail and are deliberately NOT exposed here. */
3
71
  type Rail = "platform" | "game" | "steam";
@@ -10,6 +78,16 @@ interface InvoConfig {
10
78
  timeoutMs?: number;
11
79
  /** Optional fetch override (e.g. a custom agent). Defaults to global fetch. */
12
80
  fetch?: typeof fetch;
81
+ /**
82
+ * Max automatic retries on transient failures — network error/timeout, `429`
83
+ * (honoring `retry_after`), and `5xx`. Default 2. Set 0 to disable. Mutating
84
+ * calls carry idempotency keys, so retries are safe.
85
+ */
86
+ maxRetries?: number;
87
+ /** Base backoff delay in ms (exponential + jitter). Default 250. */
88
+ retryBaseDelayMs?: number;
89
+ /** Optional observability callbacks: onRequest / onResponse / onError (best-effort). */
90
+ hooks?: InvoHooks;
13
91
  }
14
92
  interface ServerConfig extends InvoConfig {
15
93
  /** The game secret. Server-side ONLY — never ship this to a browser. */
@@ -107,6 +185,24 @@ interface PurchaseResult {
107
185
  newBalance?: string | number | null;
108
186
  raw: Record<string, unknown>;
109
187
  }
188
+ interface ConfirmPaymentResult {
189
+ status: string;
190
+ transactionId?: string;
191
+ newBalance?: string | number | null;
192
+ raw: Record<string, unknown>;
193
+ }
194
+ /** Shape of the order-details reads (currency purchase + item purchase). */
195
+ interface OrderDetailsResult {
196
+ order: Record<string, unknown>;
197
+ financialSummary: Record<string, unknown>;
198
+ statusTimeline: unknown;
199
+ raw: Record<string, unknown>;
200
+ }
201
+ interface ItemHistoryResult {
202
+ history: Record<string, unknown>[];
203
+ pagination: Record<string, unknown>;
204
+ raw: Record<string, unknown>;
205
+ }
110
206
  interface PurchaseItemInput {
111
207
  /** Idempotency key, unique per game. Required. A duplicate returns 409. */
112
208
  clientRequestId: string;
@@ -148,6 +244,30 @@ interface ItemOrderQuery {
148
244
  transactionId?: string;
149
245
  clientRequestId?: string;
150
246
  }
247
+ interface PlayerBalanceQuery {
248
+ /** Look up by email, or by playerId — provide one. */
249
+ playerEmail?: string;
250
+ playerId?: string | number;
251
+ }
252
+ interface CurrencyBalance {
253
+ currencyId: string | number;
254
+ currencyName: string;
255
+ currencySymbol: string;
256
+ availableBalance: string;
257
+ reservedBalance: string;
258
+ totalBalance: string;
259
+ lastTransaction?: string;
260
+ /** Raw balance row for any field not surfaced here. */
261
+ raw: Record<string, unknown>;
262
+ }
263
+ interface PlayerBalanceResult {
264
+ /** { player_id, player_name, player_email, date_joined, last_active }. */
265
+ player: Record<string, unknown>;
266
+ balances: CurrencyBalance[];
267
+ /** { total_currencies, total_value_usd, has_funds, last_updated }. */
268
+ summary: Record<string, unknown>;
269
+ raw: Record<string, unknown>;
270
+ }
151
271
  interface ApproveResult {
152
272
  status: string;
153
273
  next: string;
@@ -167,45 +287,4 @@ interface LinkDeviceResult {
167
287
  raw: Record<string, unknown>;
168
288
  }
169
289
 
170
- /**
171
- * Typed error for every INVO backend failure.
172
- *
173
- * The backend error envelope is usually `{ "error": <message>, "code": <STABLE_CODE> }`,
174
- * but a few txn-state/claim errors carry only `error` (no `code`) and some use
175
- * `{ "status": "error", "message": ... }`. So `code` may be undefined — branch on
176
- * `code` when present, else fall back to `message`. See the handoff doc §8.
177
- */
178
- declare class InvoError extends Error {
179
- /** Stable machine code from the backend (`code`), when present. */
180
- readonly code: string | undefined;
181
- /** HTTP status. */
182
- readonly status: number;
183
- /** Raw parsed body, for debugging / fields not surfaced here. */
184
- readonly body: unknown;
185
- constructor(args: {
186
- message: string;
187
- code?: string;
188
- status: number;
189
- body?: unknown;
190
- });
191
- /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
192
- get isReceiverNotEnrolled(): boolean;
193
- /** True if the session/SDK token has expired and the caller should re-mint + retry. */
194
- get isTokenExpired(): boolean;
195
- /**
196
- * True if an item purchase failed because the player's balance was too low (§4.8 → 400).
197
- * The backend carries `required_amount` + `current_balance` on the body for the UI.
198
- * Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
199
- * is a rate-limit, not a top-up condition) is NOT misclassified as this.
200
- */
201
- get isInsufficientBalance(): boolean;
202
- /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
203
- get isDuplicateRequest(): boolean;
204
- /** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
205
- get retryAfter(): number | undefined;
206
- /** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
207
- * The `in` operator throws on primitives, so callers must go through this. */
208
- private bodyObject;
209
- }
210
-
211
- export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h, type PurchaseItemInput as i, type PurchaseItemResult as j, type ItemHistoryQuery as k, type ItemOrderQuery as l };
290
+ export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type OrderDetailsResult as O, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InvoErrorInfo as b, type InvoHooks as c, type InvoRequestInfo as d, type InvoResponseInfo as e, type InitiateSendInput as f, type InitiateResult as g, type InitiateTransferInput as h, type CreateCheckoutInput as i, type CreateCheckoutResult as j, type PurchaseInput as k, type PurchaseResult as l, type ConfirmPaymentResult as m, type PurchaseItemInput as n, type PurchaseItemResult as o, type ItemHistoryQuery as p, type ItemHistoryResult as q, type ItemOrderQuery as r, type PlayerBalanceQuery as s, type PlayerBalanceResult as t, type CurrencyBalance as u };