@invonetwork/web-sdk 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to `@invonetwork/web-sdk` are documented here. This project
4
4
  [Semantic Versioning](https://semver.org/). Releases are managed with
5
5
  [changesets](https://github.com/changesets/changesets).
6
6
 
7
+ ## [1.2.0] — 2026-07-01
8
+
9
+ Two more server-side capabilities on `InvoServer`.
10
+
11
+ - **Phone-share OTP handshake** — `phoneShareInitiate` / `phoneShareApprove` / `phoneShareStatus`
12
+ resolve a `409 PHONE_SHARE_APPROVAL_REQUIRED`. Unauthenticated (phone-owner-driven — no game
13
+ secret, no player token), relayed by a partner backend; after approve, re-issue the identical
14
+ original request (the grant is server-side + auto-consumed). Adds
15
+ `InvoError.isPhoneShareAlreadyApproved`.
16
+ - **Server-side `getDestinations`** — game-secret variant of the browser
17
+ `InvoClient.getDestinations`; pass `{ sourceGameId, direction }` (default `"transfer"`), rows
18
+ share the same `DestinationGame` shape.
19
+
20
+ ## [1.1.0] — 2026-07-01
21
+
22
+ Four additive server-side flows on `InvoServer` (the SMS-PIN / claim-code / status /
23
+ guardian paths partners still need alongside the passkey path).
24
+
25
+ - **SMS-PIN completion** — `verifySmsTransfer` / `verifySmsSend` (for `verificationMethod: "sms"`).
26
+ - **Claim-by-code** — `claimTransfer` / `claimCurrency`, incl. the `needsAccountSelection`
27
+ multi-account disambiguation (a `200` with `candidates`, not an error).
28
+ - **Status reads** — `getTransferStatus` / `getSendStatus` (surface `verificationState`).
29
+ - **Guardian approval-status poll** — `getGuardianApprovalStatus` (poll a hold to `state`).
30
+ - New typed results (`SmsVerifyResult`, `ClaimResult`, `TransactionStatusResult`,
31
+ `GuardianApprovalStatusResult`) + `InvoError.isPhoneShareApprovalRequired`.
32
+
7
33
  ## [1.0.0] — 2026-07-01
8
34
 
9
35
  **1.0 — stable.** The public API is now covered by a stability commitment: it follows
package/README.md CHANGED
@@ -179,7 +179,7 @@ const { checkoutUrl, sessionId, expiresAt } = await server.createCheckout({
179
179
  rail: "platform", // optional: "platform" (default) | "game" | "steam"
180
180
  successUrl: "https://you/buy/ok",
181
181
  cancelUrl: "https://you/buy/cancel",
182
- metadata: { yourOrderId: "ord_42" }, // echoed on the purchase.completed webhook
182
+ metadata: { yourOrderId: "ord_42" }, // echoed on the purchase.completed webhook (all rails); order_id also reconciles
183
183
  });
184
184
  // → send the browser to checkoutUrl (single-use, ~15 min)
185
185
  ```
@@ -225,7 +225,7 @@ const purchase = await server.purchaseCurrency({
225
225
  purchaseReference: crypto.randomUUID(), // idempotency key, required
226
226
  rail: "platform",
227
227
  paymentMethodId: "pm_...", // a tokenized payment method
228
- metadata: { yourOrderId: "ord_42" }, // echoed back on the purchase.completed webhook
228
+ metadata: { yourOrderId: "ord_42" }, // echoed on the purchase.completed webhook (all rails); order_id also reconciles
229
229
  });
230
230
  // purchase.status:
231
231
  // "success" → captured, purchase.newBalance updated
@@ -444,7 +444,7 @@ export const POST = createWebhookHandler({
444
444
 
445
445
  | Event | Fires for | Use it to |
446
446
  |---|---|---|
447
- | `purchase.completed` | every currency-purchase rail | grant currency (payload: `transaction_id, order_id, player_email, identity_id, usd_amount, currency_amount, currency_name, new_balance, rail, metadata`) — `metadata` echoes what you passed to `createCheckout`/`purchaseCurrency` |
447
+ | `purchase.completed` | every currency-purchase rail | grant currency (payload: `transaction_id, order_id, player_email, identity_id, usd_amount, currency_amount, currency_name, new_balance, rail`, `metadata`) — `metadata` echoes what you passed to `createCheckout`/`purchaseCurrency` (all rails); `order_id` is also on every webhook as a secondary reconciliation key (`getOrderDetails`). |
448
448
  | `purchase.failed` / `purchase.disputed` | `platform` rail only | handle failures/disputes |
449
449
  | `purchase.refunded` | `game` / `steam` rails | handle refunds |
450
450
  | `item.purchased` | every item purchase | **grant the in-game item** (payload includes `transaction_id, order_id, player_email, identity_id, item_id, item_name, item_quantity, unit_price, total_price, currency_name, new_balance, fee_breakdown`) |
@@ -495,6 +495,8 @@ Helpers:
495
495
  | `.retryAfter` | seconds to back off on a 429 throttle |
496
496
  | `.isEnrollmentAuthorizationRequired` | first-enrollment needs the OTP grant → `enrollmentBegin`/`enrollmentVerify` |
497
497
  | `.isEnrollmentProofRequired` | another method exists → prove it via `linkDevice` |
498
+ | `.isPhoneShareApprovalRequired` | a new-account phone needs owner approval → run the `phoneShare*` handshake, then retry |
499
+ | `.isPhoneShareAlreadyApproved` | phone-share was already approved (e.g. from a `/reject`) |
498
500
 
499
501
  Client-side guards (bad amount, missing idempotency key, `rail:"steam"` on `purchaseCurrency`, item validation) throw `InvoError` with `.status === 0` **before** any network call. Notable backend codes: `SDK_TOKEN_EXPIRED`, `TENANT_NOT_MIGRATED`, `WEBAUTHN_NOT_ENABLED_FOR_TENANT`, `WEBAUTHN_UV_REQUIRED`, `ENROLLMENT_REQUIRES_PROOF`, `WRONG_RAIL_ENDPOINT`, `flow_paused`.
500
502
 
@@ -530,6 +532,12 @@ try {
530
532
  | `getPlayerBalance({ playerEmail? \| playerId? })` | `{ player, balances, summary, raw }` |
531
533
  | `getInboundPending({ playerEmail? \| playerPhone? })` | `{ inboundPending, raw }` — live unclaimed inbound sends/transfers |
532
534
  | `getLinkedIdentities({ playerEmail? \| playerPhone? })` | `{ walletUserId, primaryEmail, primaryPhone, isMinor, emails, notFound, raw }` — **server-only** (returns PII) |
535
+ | `verifySmsTransfer(txnId, smsPin)` / `verifySmsSend(txnId, smsPin)` | `SmsVerifyResult` — complete the SMS-PIN path when `verificationMethod: "sms"` |
536
+ | `claimTransfer(input)` / `claimCurrency(input)` | `ClaimResult` — redeem a claim code (`needsAccountSelection` + `candidates` on a multi-account phone) |
537
+ | `getTransferStatus(txnId)` / `getSendStatus(txnId)` | `TransactionStatusResult` — poll outbound state (`verificationState`) |
538
+ | `getGuardianApprovalStatus(txnId)` | `GuardianApprovalStatusResult` — poll a guardian hold to resolution (`state`) |
539
+ | `getDestinations({ sourceGameId, direction? })` | `DestinationsResult` — server-side (game-secret) destinations; same rows as the browser variant |
540
+ | `phoneShareInitiate({ phone, email })` / `phoneShareApprove({ approvalId, otp })` / `phoneShareStatus({ phone, email })` | phone-share OTP handshake (unauthenticated) to resolve a `PHONE_SHARE_APPROVAL_REQUIRED` (409); then re-issue the original request |
533
541
  | `iterateItemPurchaseHistory({ playerEmail, pageSize? })` | async iterator over all history rows |
534
542
  | `verifyWebhook(rawBody, signatureHeader, secret \| secrets, opts?)` | typed `InvoWebhookEvent` (throws on bad signature) |
535
543
  | `verifyWebhookAsync(...)` | same as `verifyWebhook`, Web Crypto (edge/Workers/Deno/Bun) |
@@ -36,6 +36,14 @@ var InvoError = class _InvoError extends Error {
36
36
  const b = this.bodyObject();
37
37
  return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
38
38
  }
39
+ /** True if a claim needs phone-share approval before it can complete (claim → 409). */
40
+ get isPhoneShareApprovalRequired() {
41
+ return this.code === "PHONE_SHARE_APPROVAL_REQUIRED";
42
+ }
43
+ /** True if the phone-share (phone, requesting_email) pair was already approved. */
44
+ get isPhoneShareAlreadyApproved() {
45
+ return this.code === "PHONE_SHARE_ALREADY_APPROVED";
46
+ }
39
47
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
40
48
  get isDuplicateRequest() {
41
49
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
@@ -256,6 +264,49 @@ function retryAfterMs(parsed, headers) {
256
264
  return void 0;
257
265
  }
258
266
 
259
- export { Http, InvoError, assertSecureBaseUrl };
260
- //# sourceMappingURL=chunk-D3XBTH4C.js.map
261
- //# sourceMappingURL=chunk-D3XBTH4C.js.map
267
+ // src/shared/destinations.ts
268
+ function toDestinationGame(row) {
269
+ const s = (k) => row[k] === void 0 || row[k] === null ? void 0 : String(row[k]);
270
+ return {
271
+ gameId: row["game_id"] ?? "",
272
+ gameName: String(row["game_name"] ?? ""),
273
+ tenantType: s("tenant_type"),
274
+ developerName: s("developer_name"),
275
+ publisherName: s("publisher_name"),
276
+ genre: s("genre"),
277
+ platform: s("platform"),
278
+ gameStatus: s("game_status"),
279
+ gameIcon: s("game_icon"),
280
+ gamePoster: s("game_poster"),
281
+ gameUrl: s("game_url"),
282
+ gameDescription: s("game_description"),
283
+ currencyName: String(row["currency_name"] ?? ""),
284
+ currencySymbol: String(row["currency_symbol"] ?? ""),
285
+ currencySymbolUrl: s("currency_symbol_url"),
286
+ minimumTransfer: String(row["minimum_transfer"] ?? ""),
287
+ maximumTransfer: String(row["maximum_transfer"] ?? ""),
288
+ raw: row
289
+ };
290
+ }
291
+ function toDestinationsResult(raw, direction) {
292
+ const games = Array.isArray(raw["available_games"]) ? raw["available_games"] : [];
293
+ return {
294
+ status: String(raw["status"] ?? ""),
295
+ sourceGameId: raw["source_game_id"] ?? "",
296
+ sourceGameName: String(raw["source_game_name"] ?? ""),
297
+ sourceGameIcon: raw["source_game_icon"],
298
+ sourceCurrencyName: String(raw["source_currency_name"] ?? ""),
299
+ sourceCurrencyIcon: raw["source_currency_icon"],
300
+ universalTransfers: raw["universal_transfers"] === true,
301
+ transferMode: String(raw["transfer_mode"] ?? ""),
302
+ availableGames: games.map(toDestinationGame),
303
+ totalDestinations: Number(raw["total_destinations"] ?? games.length),
304
+ direction: String(raw["direction"] ?? direction),
305
+ linkedGameIds: Array.isArray(raw["linked_game_ids"]) ? raw["linked_game_ids"] : void 0,
306
+ raw
307
+ };
308
+ }
309
+
310
+ export { Http, InvoError, assertSecureBaseUrl, toDestinationsResult };
311
+ //# sourceMappingURL=chunk-P65XQ6VF.js.map
312
+ //# sourceMappingURL=chunk-P65XQ6VF.js.map
package/dist/index.cjs CHANGED
@@ -38,6 +38,14 @@ var InvoError = class _InvoError extends Error {
38
38
  const b = this.bodyObject();
39
39
  return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
40
40
  }
41
+ /** True if a claim needs phone-share approval before it can complete (claim → 409). */
42
+ get isPhoneShareApprovalRequired() {
43
+ return this.code === "PHONE_SHARE_APPROVAL_REQUIRED";
44
+ }
45
+ /** True if the phone-share (phone, requesting_email) pair was already approved. */
46
+ get isPhoneShareAlreadyApproved() {
47
+ return this.code === "PHONE_SHARE_ALREADY_APPROVED";
48
+ }
41
49
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
42
50
  get isDuplicateRequest() {
43
51
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
@@ -339,6 +347,49 @@ function assertionToJSON(cred) {
339
347
  };
340
348
  }
341
349
 
350
+ // src/shared/destinations.ts
351
+ function toDestinationGame(row) {
352
+ const s = (k) => row[k] === void 0 || row[k] === null ? void 0 : String(row[k]);
353
+ return {
354
+ gameId: row["game_id"] ?? "",
355
+ gameName: String(row["game_name"] ?? ""),
356
+ tenantType: s("tenant_type"),
357
+ developerName: s("developer_name"),
358
+ publisherName: s("publisher_name"),
359
+ genre: s("genre"),
360
+ platform: s("platform"),
361
+ gameStatus: s("game_status"),
362
+ gameIcon: s("game_icon"),
363
+ gamePoster: s("game_poster"),
364
+ gameUrl: s("game_url"),
365
+ gameDescription: s("game_description"),
366
+ currencyName: String(row["currency_name"] ?? ""),
367
+ currencySymbol: String(row["currency_symbol"] ?? ""),
368
+ currencySymbolUrl: s("currency_symbol_url"),
369
+ minimumTransfer: String(row["minimum_transfer"] ?? ""),
370
+ maximumTransfer: String(row["maximum_transfer"] ?? ""),
371
+ raw: row
372
+ };
373
+ }
374
+ function toDestinationsResult(raw, direction) {
375
+ const games = Array.isArray(raw["available_games"]) ? raw["available_games"] : [];
376
+ return {
377
+ status: String(raw["status"] ?? ""),
378
+ sourceGameId: raw["source_game_id"] ?? "",
379
+ sourceGameName: String(raw["source_game_name"] ?? ""),
380
+ sourceGameIcon: raw["source_game_icon"],
381
+ sourceCurrencyName: String(raw["source_currency_name"] ?? ""),
382
+ sourceCurrencyIcon: raw["source_currency_icon"],
383
+ universalTransfers: raw["universal_transfers"] === true,
384
+ transferMode: String(raw["transfer_mode"] ?? ""),
385
+ availableGames: games.map(toDestinationGame),
386
+ totalDestinations: Number(raw["total_destinations"] ?? games.length),
387
+ direction: String(raw["direction"] ?? direction),
388
+ linkedGameIds: Array.isArray(raw["linked_game_ids"]) ? raw["linked_game_ids"] : void 0,
389
+ raw
390
+ };
391
+ }
392
+
342
393
  // src/index.ts
343
394
  function toPendingCollectItem(row) {
344
395
  return {
@@ -367,29 +418,6 @@ function toBalanceRow(row) {
367
418
  raw: row
368
419
  };
369
420
  }
370
- function toDestinationGame(row) {
371
- const s = (k) => row[k] === void 0 || row[k] === null ? void 0 : String(row[k]);
372
- return {
373
- gameId: row["game_id"] ?? "",
374
- gameName: String(row["game_name"] ?? ""),
375
- tenantType: s("tenant_type"),
376
- developerName: s("developer_name"),
377
- publisherName: s("publisher_name"),
378
- genre: s("genre"),
379
- platform: s("platform"),
380
- gameStatus: s("game_status"),
381
- gameIcon: s("game_icon"),
382
- gamePoster: s("game_poster"),
383
- gameUrl: s("game_url"),
384
- gameDescription: s("game_description"),
385
- currencyName: String(row["currency_name"] ?? ""),
386
- currencySymbol: String(row["currency_symbol"] ?? ""),
387
- currencySymbolUrl: s("currency_symbol_url"),
388
- minimumTransfer: String(row["minimum_transfer"] ?? ""),
389
- maximumTransfer: String(row["maximum_transfer"] ?? ""),
390
- raw: row
391
- };
392
- }
393
421
  var InvoClient = class {
394
422
  constructor(config) {
395
423
  if (!config.token) throw new Error("InvoClient requires a player `token`.");
@@ -510,22 +538,7 @@ var InvoClient = class {
510
538
  `/api/sdk/destinations?direction=${encodeURIComponent(direction)}`,
511
539
  opts?.signal
512
540
  );
513
- const games = Array.isArray(raw["available_games"]) ? raw["available_games"] : [];
514
- return {
515
- status: String(raw["status"] ?? ""),
516
- sourceGameId: raw["source_game_id"] ?? "",
517
- sourceGameName: String(raw["source_game_name"] ?? ""),
518
- sourceGameIcon: raw["source_game_icon"],
519
- sourceCurrencyName: String(raw["source_currency_name"] ?? ""),
520
- sourceCurrencyIcon: raw["source_currency_icon"],
521
- universalTransfers: raw["universal_transfers"] === true,
522
- transferMode: String(raw["transfer_mode"] ?? ""),
523
- availableGames: games.map(toDestinationGame),
524
- totalDestinations: Number(raw["total_destinations"] ?? games.length),
525
- direction: String(raw["direction"] ?? direction),
526
- linkedGameIds: Array.isArray(raw["linked_game_ids"]) ? raw["linked_game_ids"] : void 0,
527
- raw
528
- };
541
+ return toDestinationsResult(raw, direction);
529
542
  });
530
543
  }
531
544
  /**
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as ClientConfig, a as CallOptions, A as ApproveResult, b as ConfirmReceiptResult, L as LinkDeviceResult, P as PendingCollectResult, D as DestinationsQuery, c as DestinationsResult, B as BalanceResult, E as EnrollmentBeginResult, d as EnrollmentVerifyResult } from './types-DLSCxpoT.cjs';
2
- export { e as BalanceRow, f as DestinationGame, I as InvoError, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, k as PendingCollectItem, R as Rail, V as VerificationMethod } from './types-DLSCxpoT.cjs';
1
+ import { C as ClientConfig, a as CallOptions, A as ApproveResult, b as ConfirmReceiptResult, L as LinkDeviceResult, P as PendingCollectResult, D as DestinationsQuery, c as DestinationsResult, B as BalanceResult, E as EnrollmentBeginResult, d as EnrollmentVerifyResult } from './types-DTAmriOB.cjs';
2
+ export { e as BalanceRow, f as DestinationGame, I as InvoError, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, k as PendingCollectItem, R as Rail, V as VerificationMethod } from './types-DTAmriOB.cjs';
3
3
 
4
4
  declare class InvoClient {
5
5
  private readonly http;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as ClientConfig, a as CallOptions, A as ApproveResult, b as ConfirmReceiptResult, L as LinkDeviceResult, P as PendingCollectResult, D as DestinationsQuery, c as DestinationsResult, B as BalanceResult, E as EnrollmentBeginResult, d as EnrollmentVerifyResult } from './types-DLSCxpoT.js';
2
- export { e as BalanceRow, f as DestinationGame, I as InvoError, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, k as PendingCollectItem, R as Rail, V as VerificationMethod } from './types-DLSCxpoT.js';
1
+ import { C as ClientConfig, a as CallOptions, A as ApproveResult, b as ConfirmReceiptResult, L as LinkDeviceResult, P as PendingCollectResult, D as DestinationsQuery, c as DestinationsResult, B as BalanceResult, E as EnrollmentBeginResult, d as EnrollmentVerifyResult } from './types-DTAmriOB.js';
2
+ export { e as BalanceRow, f as DestinationGame, I as InvoError, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, k as PendingCollectItem, R as Rail, V as VerificationMethod } from './types-DTAmriOB.js';
3
3
 
4
4
  declare class InvoClient {
5
5
  private readonly http;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { assertSecureBaseUrl, Http, InvoError } from './chunk-D3XBTH4C.js';
2
- export { InvoError } from './chunk-D3XBTH4C.js';
1
+ import { assertSecureBaseUrl, Http, toDestinationsResult, InvoError } from './chunk-P65XQ6VF.js';
2
+ export { InvoError } from './chunk-P65XQ6VF.js';
3
3
 
4
4
  // src/shared/webauthn.ts
5
5
  function b64urlToBuffer(value) {
@@ -110,29 +110,6 @@ function toBalanceRow(row) {
110
110
  raw: row
111
111
  };
112
112
  }
113
- function toDestinationGame(row) {
114
- const s = (k) => row[k] === void 0 || row[k] === null ? void 0 : String(row[k]);
115
- return {
116
- gameId: row["game_id"] ?? "",
117
- gameName: String(row["game_name"] ?? ""),
118
- tenantType: s("tenant_type"),
119
- developerName: s("developer_name"),
120
- publisherName: s("publisher_name"),
121
- genre: s("genre"),
122
- platform: s("platform"),
123
- gameStatus: s("game_status"),
124
- gameIcon: s("game_icon"),
125
- gamePoster: s("game_poster"),
126
- gameUrl: s("game_url"),
127
- gameDescription: s("game_description"),
128
- currencyName: String(row["currency_name"] ?? ""),
129
- currencySymbol: String(row["currency_symbol"] ?? ""),
130
- currencySymbolUrl: s("currency_symbol_url"),
131
- minimumTransfer: String(row["minimum_transfer"] ?? ""),
132
- maximumTransfer: String(row["maximum_transfer"] ?? ""),
133
- raw: row
134
- };
135
- }
136
113
  var InvoClient = class {
137
114
  constructor(config) {
138
115
  if (!config.token) throw new Error("InvoClient requires a player `token`.");
@@ -253,22 +230,7 @@ var InvoClient = class {
253
230
  `/api/sdk/destinations?direction=${encodeURIComponent(direction)}`,
254
231
  opts?.signal
255
232
  );
256
- const games = Array.isArray(raw["available_games"]) ? raw["available_games"] : [];
257
- return {
258
- status: String(raw["status"] ?? ""),
259
- sourceGameId: raw["source_game_id"] ?? "",
260
- sourceGameName: String(raw["source_game_name"] ?? ""),
261
- sourceGameIcon: raw["source_game_icon"],
262
- sourceCurrencyName: String(raw["source_currency_name"] ?? ""),
263
- sourceCurrencyIcon: raw["source_currency_icon"],
264
- universalTransfers: raw["universal_transfers"] === true,
265
- transferMode: String(raw["transfer_mode"] ?? ""),
266
- availableGames: games.map(toDestinationGame),
267
- totalDestinations: Number(raw["total_destinations"] ?? games.length),
268
- direction: String(raw["direction"] ?? direction),
269
- linkedGameIds: Array.isArray(raw["linked_game_ids"]) ? raw["linked_game_ids"] : void 0,
270
- raw
271
- };
233
+ return toDestinationsResult(raw, direction);
272
234
  });
273
235
  }
274
236
  /**