@invonetwork/web-sdk 1.1.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,19 @@ 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
+
7
20
  ## [1.1.0] — 2026-07-01
8
21
 
9
22
  Four additive server-side flows on `InvoServer` (the SMS-PIN / claim-code / status /
package/README.md CHANGED
@@ -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
 
@@ -534,6 +536,8 @@ try {
534
536
  | `claimTransfer(input)` / `claimCurrency(input)` | `ClaimResult` — redeem a claim code (`needsAccountSelection` + `candidates` on a multi-account phone) |
535
537
  | `getTransferStatus(txnId)` / `getSendStatus(txnId)` | `TransactionStatusResult` — poll outbound state (`verificationState`) |
536
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 |
537
541
  | `iterateItemPurchaseHistory({ playerEmail, pageSize? })` | async iterator over all history rows |
538
542
  | `verifyWebhook(rawBody, signatureHeader, secret \| secrets, opts?)` | typed `InvoWebhookEvent` (throws on bad signature) |
539
543
  | `verifyWebhookAsync(...)` | same as `verifyWebhook`, Web Crypto (edge/Workers/Deno/Bun) |
@@ -40,6 +40,10 @@ var InvoError = class _InvoError extends Error {
40
40
  get isPhoneShareApprovalRequired() {
41
41
  return this.code === "PHONE_SHARE_APPROVAL_REQUIRED";
42
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
+ }
43
47
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
44
48
  get isDuplicateRequest() {
45
49
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
@@ -260,6 +264,49 @@ function retryAfterMs(parsed, headers) {
260
264
  return void 0;
261
265
  }
262
266
 
263
- export { Http, InvoError, assertSecureBaseUrl };
264
- //# sourceMappingURL=chunk-ZVKWDXSL.js.map
265
- //# sourceMappingURL=chunk-ZVKWDXSL.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
@@ -42,6 +42,10 @@ var InvoError = class _InvoError extends Error {
42
42
  get isPhoneShareApprovalRequired() {
43
43
  return this.code === "PHONE_SHARE_APPROVAL_REQUIRED";
44
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
+ }
45
49
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
46
50
  get isDuplicateRequest() {
47
51
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
@@ -343,6 +347,49 @@ function assertionToJSON(cred) {
343
347
  };
344
348
  }
345
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
+
346
393
  // src/index.ts
347
394
  function toPendingCollectItem(row) {
348
395
  return {
@@ -371,29 +418,6 @@ function toBalanceRow(row) {
371
418
  raw: row
372
419
  };
373
420
  }
374
- function toDestinationGame(row) {
375
- const s = (k) => row[k] === void 0 || row[k] === null ? void 0 : String(row[k]);
376
- return {
377
- gameId: row["game_id"] ?? "",
378
- gameName: String(row["game_name"] ?? ""),
379
- tenantType: s("tenant_type"),
380
- developerName: s("developer_name"),
381
- publisherName: s("publisher_name"),
382
- genre: s("genre"),
383
- platform: s("platform"),
384
- gameStatus: s("game_status"),
385
- gameIcon: s("game_icon"),
386
- gamePoster: s("game_poster"),
387
- gameUrl: s("game_url"),
388
- gameDescription: s("game_description"),
389
- currencyName: String(row["currency_name"] ?? ""),
390
- currencySymbol: String(row["currency_symbol"] ?? ""),
391
- currencySymbolUrl: s("currency_symbol_url"),
392
- minimumTransfer: String(row["minimum_transfer"] ?? ""),
393
- maximumTransfer: String(row["maximum_transfer"] ?? ""),
394
- raw: row
395
- };
396
- }
397
421
  var InvoClient = class {
398
422
  constructor(config) {
399
423
  if (!config.token) throw new Error("InvoClient requires a player `token`.");
@@ -514,22 +538,7 @@ var InvoClient = class {
514
538
  `/api/sdk/destinations?direction=${encodeURIComponent(direction)}`,
515
539
  opts?.signal
516
540
  );
517
- const games = Array.isArray(raw["available_games"]) ? raw["available_games"] : [];
518
- return {
519
- status: String(raw["status"] ?? ""),
520
- sourceGameId: raw["source_game_id"] ?? "",
521
- sourceGameName: String(raw["source_game_name"] ?? ""),
522
- sourceGameIcon: raw["source_game_icon"],
523
- sourceCurrencyName: String(raw["source_currency_name"] ?? ""),
524
- sourceCurrencyIcon: raw["source_currency_icon"],
525
- universalTransfers: raw["universal_transfers"] === true,
526
- transferMode: String(raw["transfer_mode"] ?? ""),
527
- availableGames: games.map(toDestinationGame),
528
- totalDestinations: Number(raw["total_destinations"] ?? games.length),
529
- direction: String(raw["direction"] ?? direction),
530
- linkedGameIds: Array.isArray(raw["linked_game_ids"]) ? raw["linked_game_ids"] : void 0,
531
- raw
532
- };
541
+ return toDestinationsResult(raw, direction);
533
542
  });
534
543
  }
535
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-CPt_5_Aw.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-CPt_5_Aw.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-CPt_5_Aw.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-CPt_5_Aw.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-ZVKWDXSL.js';
2
- export { InvoError } from './chunk-ZVKWDXSL.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
  /**
package/dist/server.cjs CHANGED
@@ -44,6 +44,10 @@ var InvoError = class _InvoError extends Error {
44
44
  get isPhoneShareApprovalRequired() {
45
45
  return this.code === "PHONE_SHARE_APPROVAL_REQUIRED";
46
46
  }
47
+ /** True if the phone-share (phone, requesting_email) pair was already approved. */
48
+ get isPhoneShareAlreadyApproved() {
49
+ return this.code === "PHONE_SHARE_ALREADY_APPROVED";
50
+ }
47
51
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
48
52
  get isDuplicateRequest() {
49
53
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
@@ -263,6 +267,49 @@ function retryAfterMs(parsed, headers) {
263
267
  }
264
268
  return void 0;
265
269
  }
270
+
271
+ // src/shared/destinations.ts
272
+ function toDestinationGame(row) {
273
+ const s = (k) => row[k] === void 0 || row[k] === null ? void 0 : String(row[k]);
274
+ return {
275
+ gameId: row["game_id"] ?? "",
276
+ gameName: String(row["game_name"] ?? ""),
277
+ tenantType: s("tenant_type"),
278
+ developerName: s("developer_name"),
279
+ publisherName: s("publisher_name"),
280
+ genre: s("genre"),
281
+ platform: s("platform"),
282
+ gameStatus: s("game_status"),
283
+ gameIcon: s("game_icon"),
284
+ gamePoster: s("game_poster"),
285
+ gameUrl: s("game_url"),
286
+ gameDescription: s("game_description"),
287
+ currencyName: String(row["currency_name"] ?? ""),
288
+ currencySymbol: String(row["currency_symbol"] ?? ""),
289
+ currencySymbolUrl: s("currency_symbol_url"),
290
+ minimumTransfer: String(row["minimum_transfer"] ?? ""),
291
+ maximumTransfer: String(row["maximum_transfer"] ?? ""),
292
+ raw: row
293
+ };
294
+ }
295
+ function toDestinationsResult(raw, direction) {
296
+ const games = Array.isArray(raw["available_games"]) ? raw["available_games"] : [];
297
+ return {
298
+ status: String(raw["status"] ?? ""),
299
+ sourceGameId: raw["source_game_id"] ?? "",
300
+ sourceGameName: String(raw["source_game_name"] ?? ""),
301
+ sourceGameIcon: raw["source_game_icon"],
302
+ sourceCurrencyName: String(raw["source_currency_name"] ?? ""),
303
+ sourceCurrencyIcon: raw["source_currency_icon"],
304
+ universalTransfers: raw["universal_transfers"] === true,
305
+ transferMode: String(raw["transfer_mode"] ?? ""),
306
+ availableGames: games.map(toDestinationGame),
307
+ totalDestinations: Number(raw["total_destinations"] ?? games.length),
308
+ direction: String(raw["direction"] ?? direction),
309
+ linkedGameIds: Array.isArray(raw["linked_game_ids"]) ? raw["linked_game_ids"] : void 0,
310
+ raw
311
+ };
312
+ }
266
313
  var DEFAULT_TOLERANCE_SEC = 300;
267
314
  var ENCODER = new TextEncoder();
268
315
  function toBytes(body) {
@@ -422,7 +469,7 @@ async function hmacHexSubtle(secret, message) {
422
469
  }
423
470
 
424
471
  // src/server.ts
425
- var DEFAULT_UA = "invonetwork-web-sdk/1.1.0 (+https://invo.network)";
472
+ var DEFAULT_UA = "invonetwork-web-sdk/1.2.0 (+https://invo.network)";
426
473
  var MAX_USD_AMOUNT = 999.99;
427
474
  var MAX_ITEM_PRICE = 999999.99;
428
475
  function invalidInput(label, value, why) {
@@ -535,6 +582,24 @@ function toGuardianApprovalStatus(raw) {
535
582
  raw
536
583
  };
537
584
  }
585
+ function toPhoneShareInitiateResult(raw) {
586
+ return {
587
+ approvalId: raw["approval_id"],
588
+ codeSent: typeof raw["code_sent"] === "boolean" ? raw["code_sent"] : void 0,
589
+ expiresAt: raw["expires_at"],
590
+ alreadyApproved: typeof raw["already_approved"] === "boolean" ? raw["already_approved"] : void 0,
591
+ raw
592
+ };
593
+ }
594
+ function toPhoneShareApproveResult(raw) {
595
+ return {
596
+ approvalId: raw["approval_id"],
597
+ approved: typeof raw["approved"] === "boolean" ? raw["approved"] : void 0,
598
+ alreadyApproved: typeof raw["already_approved"] === "boolean" ? raw["already_approved"] : void 0,
599
+ expiresAt: raw["expires_at"],
600
+ raw
601
+ };
602
+ }
538
603
  function toCurrencyBalance(row) {
539
604
  return {
540
605
  currencyId: row["currency_id"] ?? "",
@@ -1107,6 +1172,101 @@ var InvoServer = class {
1107
1172
  );
1108
1173
  return toGuardianApprovalStatus(raw);
1109
1174
  }
1175
+ /**
1176
+ * List the games/tenants a player can send/transfer to FROM `sourceGameId`, with
1177
+ * display metadata inline (name, icon, currency, min/max limits) — one call, no
1178
+ * per-game lookup. SERVER variant (game-secret) of the browser `InvoClient.getDestinations`;
1179
+ * the source game is passed explicitly rather than inferred from a player token. Rows
1180
+ * share the browser shape. Idempotent (a POST read — safe to retry).
1181
+ */
1182
+ async getDestinations(query, opts) {
1183
+ if (query.sourceGameId == null || String(query.sourceGameId).trim() === "") {
1184
+ throw invalidInput("sourceGameId", query.sourceGameId, "is required");
1185
+ }
1186
+ const direction = query.direction ?? "transfer";
1187
+ const path = direction === "send" ? "/api/currency-sends/available-destinations" : "/api/transfers/available-destinations";
1188
+ const raw = await this.http.post(
1189
+ path,
1190
+ { source_game_id: query.sourceGameId },
1191
+ this.auth,
1192
+ { idempotent: true, signal: opts?.signal }
1193
+ // a POST read — no side effect, safe to retry
1194
+ );
1195
+ return toDestinationsResult(raw, direction);
1196
+ }
1197
+ /**
1198
+ * Phone-share OTP handshake — resolves a 409 `PHONE_SHARE_APPROVAL_REQUIRED`.
1199
+ *
1200
+ * These three endpoints are UNAUTHENTICATED (phone-owner-driven: no game secret,
1201
+ * no player token) — a partner backend relays the handshake. The triggering 409
1202
+ * carries `approval_id, expires_at, code_sent, existing_account_hints[], next_endpoint`.
1203
+ *
1204
+ * `phoneShareInitiate` is the fallback OTP send (use when the 409 didn't already
1205
+ * dispatch a code). NOT idempotent — never auto-retried. OTP TTL ~10 min.
1206
+ */
1207
+ async phoneShareInitiate(input, opts) {
1208
+ if (typeof input.phone !== "string" || !input.phone.trim()) {
1209
+ throw invalidInput("phone", input.phone, "is required");
1210
+ }
1211
+ if (typeof input.email !== "string" || !input.email.trim()) {
1212
+ throw invalidInput("email", input.email, "is required");
1213
+ }
1214
+ const raw = await this.http.post(
1215
+ "/api/wallet/phone-share/initiate",
1216
+ { phone: input.phone, email: input.email },
1217
+ { kind: "none" },
1218
+ // UNAUTHENTICATED — phone-owner-driven; send no auth header
1219
+ { signal: opts?.signal }
1220
+ // NOT idempotent — sends an OTP; never auto-retried
1221
+ );
1222
+ return toPhoneShareInitiateResult(raw);
1223
+ }
1224
+ /**
1225
+ * Approve a phone-share with the OTP the phone owner received. On success the grant
1226
+ * is stored server-side, keyed by (phone, requesting_email) and auto-consumed — the
1227
+ * caller simply RE-ISSUES the identical original request. UNAUTHENTICATED. NOT
1228
+ * idempotent (attempts are counted) — never auto-retried.
1229
+ */
1230
+ async phoneShareApprove(input, opts) {
1231
+ if (typeof input.approvalId !== "string" || !input.approvalId.trim()) {
1232
+ throw invalidInput("approvalId", input.approvalId, "is required");
1233
+ }
1234
+ if (typeof input.otp !== "string" || !input.otp.trim()) {
1235
+ throw invalidInput("otp", input.otp, "is required");
1236
+ }
1237
+ const raw = await this.http.post(
1238
+ "/api/wallet/phone-share/approve",
1239
+ { approval_id: input.approvalId, otp: input.otp },
1240
+ { kind: "none" },
1241
+ // UNAUTHENTICATED — phone-owner-driven; send no auth header
1242
+ { signal: opts?.signal }
1243
+ // NOT idempotent — OTP attempts are counted; never auto-retried
1244
+ );
1245
+ return toPhoneShareApproveResult(raw);
1246
+ }
1247
+ /**
1248
+ * Poll whether a phone-share (phone, requesting_email) pair is approved yet.
1249
+ * UNAUTHENTICATED, idempotent GET. `approved:true` means the original request can
1250
+ * be re-issued.
1251
+ */
1252
+ async phoneShareStatus(input, opts) {
1253
+ if (typeof input.phone !== "string" || !input.phone.trim()) {
1254
+ throw invalidInput("phone", input.phone, "is required");
1255
+ }
1256
+ if (typeof input.email !== "string" || !input.email.trim()) {
1257
+ throw invalidInput("email", input.email, "is required");
1258
+ }
1259
+ const q = new URLSearchParams();
1260
+ q.set("phone", input.phone);
1261
+ q.set("email", input.email);
1262
+ const raw = await this.http.get(
1263
+ `/api/wallet/phone-share/status?${q.toString()}`,
1264
+ { kind: "none" },
1265
+ // UNAUTHENTICATED — phone-owner-driven; send no auth header
1266
+ { signal: opts?.signal }
1267
+ );
1268
+ return { approved: raw["approved"] === true, raw };
1269
+ }
1110
1270
  toInitiateResult(raw) {
1111
1271
  const vm = raw["verification_method"];
1112
1272
  const guardian = raw["guardian_approval"];
package/dist/server.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InvoError, S as ServerConfig, a as CallOptions, l as PlayerToken, m as InitiateSendInput, n as InitiateResult, o as InitiateTransferInput, p as CreateCheckoutInput, q as CreateCheckoutResult, r as PurchaseInput, s as PurchaseResult, t as ConfirmPaymentResult, O as OrderDetailsResult, u as PurchaseItemInput, v as PurchaseItemResult, w as ItemHistoryQuery, x as ItemHistoryResult, y as ItemOrderQuery, z as PlayerBalanceQuery, F as PlayerBalanceResult, G as InboundPendingQuery, H as InboundPendingResult, J as LinkedIdentitiesQuery, K as LinkedIdentitiesResult, M as SmsVerifyResult, N as ClaimTransferInput, Q as ClaimResult, T as ClaimCurrencyInput, U as TransactionStatusResult, W as GuardianApprovalStatusResult } from './types-CPt_5_Aw.cjs';
2
- export { X as CurrencyBalance, Y as InboundPendingItem, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, Z as LinkedIdentityEmail, R as Rail, V as VerificationMethod } from './types-CPt_5_Aw.cjs';
1
+ import { I as InvoError, S as ServerConfig, a as CallOptions, l as PlayerToken, m as InitiateSendInput, n as InitiateResult, o as InitiateTransferInput, p as CreateCheckoutInput, q as CreateCheckoutResult, r as PurchaseInput, s as PurchaseResult, t as ConfirmPaymentResult, O as OrderDetailsResult, u as PurchaseItemInput, v as PurchaseItemResult, w as ItemHistoryQuery, x as ItemHistoryResult, y as ItemOrderQuery, z as PlayerBalanceQuery, F as PlayerBalanceResult, G as InboundPendingQuery, H as InboundPendingResult, J as LinkedIdentitiesQuery, K as LinkedIdentitiesResult, M as SmsVerifyResult, N as ClaimTransferInput, Q as ClaimResult, T as ClaimCurrencyInput, U as TransactionStatusResult, W as GuardianApprovalStatusResult, X as ServerDestinationsQuery, c as DestinationsResult, Y as PhoneShareContact, Z as PhoneShareInitiateResult, _ as PhoneShareApproveInput, $ as PhoneShareApproveResult, a0 as PhoneShareStatusResult } from './types-DTAmriOB.cjs';
2
+ export { a1 as CurrencyBalance, f as DestinationGame, a2 as InboundPendingItem, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, a3 as LinkedIdentityEmail, R as Rail, V as VerificationMethod } from './types-DTAmriOB.cjs';
3
3
 
4
4
  interface VerifyWebhookOptions {
5
5
  /** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
@@ -258,7 +258,39 @@ declare class InvoServer {
258
258
  * a missing approval throws a 404 InvoError (`{status:"not_found"}`).
259
259
  */
260
260
  getGuardianApprovalStatus(transactionId: string, opts?: CallOptions): Promise<GuardianApprovalStatusResult>;
261
+ /**
262
+ * List the games/tenants a player can send/transfer to FROM `sourceGameId`, with
263
+ * display metadata inline (name, icon, currency, min/max limits) — one call, no
264
+ * per-game lookup. SERVER variant (game-secret) of the browser `InvoClient.getDestinations`;
265
+ * the source game is passed explicitly rather than inferred from a player token. Rows
266
+ * share the browser shape. Idempotent (a POST read — safe to retry).
267
+ */
268
+ getDestinations(query: ServerDestinationsQuery, opts?: CallOptions): Promise<DestinationsResult>;
269
+ /**
270
+ * Phone-share OTP handshake — resolves a 409 `PHONE_SHARE_APPROVAL_REQUIRED`.
271
+ *
272
+ * These three endpoints are UNAUTHENTICATED (phone-owner-driven: no game secret,
273
+ * no player token) — a partner backend relays the handshake. The triggering 409
274
+ * carries `approval_id, expires_at, code_sent, existing_account_hints[], next_endpoint`.
275
+ *
276
+ * `phoneShareInitiate` is the fallback OTP send (use when the 409 didn't already
277
+ * dispatch a code). NOT idempotent — never auto-retried. OTP TTL ~10 min.
278
+ */
279
+ phoneShareInitiate(input: PhoneShareContact, opts?: CallOptions): Promise<PhoneShareInitiateResult>;
280
+ /**
281
+ * Approve a phone-share with the OTP the phone owner received. On success the grant
282
+ * is stored server-side, keyed by (phone, requesting_email) and auto-consumed — the
283
+ * caller simply RE-ISSUES the identical original request. UNAUTHENTICATED. NOT
284
+ * idempotent (attempts are counted) — never auto-retried.
285
+ */
286
+ phoneShareApprove(input: PhoneShareApproveInput, opts?: CallOptions): Promise<PhoneShareApproveResult>;
287
+ /**
288
+ * Poll whether a phone-share (phone, requesting_email) pair is approved yet.
289
+ * UNAUTHENTICATED, idempotent GET. `approved:true` means the original request can
290
+ * be re-issued.
291
+ */
292
+ phoneShareStatus(input: PhoneShareContact, opts?: CallOptions): Promise<PhoneShareStatusResult>;
261
293
  private toInitiateResult;
262
294
  }
263
295
 
264
- export { CallOptions, ClaimCurrencyInput, ClaimResult, ClaimTransferInput, ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, GuardianApprovalStatusResult, InboundPendingQuery, InboundPendingResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoError, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, LinkedIdentitiesQuery, LinkedIdentitiesResult, OrderDetailsResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, SmsVerifyResult, TransactionStatusResult, type TransferEventData, type VerifyWebhookOptions, type WebhookHandlerOptions, createWebhookHandler, verifyWebhook, verifyWebhookAsync };
296
+ export { CallOptions, ClaimCurrencyInput, ClaimResult, ClaimTransferInput, ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, DestinationsResult, GuardianApprovalStatusResult, InboundPendingQuery, InboundPendingResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoError, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, LinkedIdentitiesQuery, LinkedIdentitiesResult, OrderDetailsResult, PhoneShareApproveInput, PhoneShareApproveResult, PhoneShareContact, PhoneShareInitiateResult, PhoneShareStatusResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, ServerDestinationsQuery, SmsVerifyResult, TransactionStatusResult, type TransferEventData, type VerifyWebhookOptions, type WebhookHandlerOptions, createWebhookHandler, verifyWebhook, verifyWebhookAsync };
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { I as InvoError, S as ServerConfig, a as CallOptions, l as PlayerToken, m as InitiateSendInput, n as InitiateResult, o as InitiateTransferInput, p as CreateCheckoutInput, q as CreateCheckoutResult, r as PurchaseInput, s as PurchaseResult, t as ConfirmPaymentResult, O as OrderDetailsResult, u as PurchaseItemInput, v as PurchaseItemResult, w as ItemHistoryQuery, x as ItemHistoryResult, y as ItemOrderQuery, z as PlayerBalanceQuery, F as PlayerBalanceResult, G as InboundPendingQuery, H as InboundPendingResult, J as LinkedIdentitiesQuery, K as LinkedIdentitiesResult, M as SmsVerifyResult, N as ClaimTransferInput, Q as ClaimResult, T as ClaimCurrencyInput, U as TransactionStatusResult, W as GuardianApprovalStatusResult } from './types-CPt_5_Aw.js';
2
- export { X as CurrencyBalance, Y as InboundPendingItem, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, Z as LinkedIdentityEmail, R as Rail, V as VerificationMethod } from './types-CPt_5_Aw.js';
1
+ import { I as InvoError, S as ServerConfig, a as CallOptions, l as PlayerToken, m as InitiateSendInput, n as InitiateResult, o as InitiateTransferInput, p as CreateCheckoutInput, q as CreateCheckoutResult, r as PurchaseInput, s as PurchaseResult, t as ConfirmPaymentResult, O as OrderDetailsResult, u as PurchaseItemInput, v as PurchaseItemResult, w as ItemHistoryQuery, x as ItemHistoryResult, y as ItemOrderQuery, z as PlayerBalanceQuery, F as PlayerBalanceResult, G as InboundPendingQuery, H as InboundPendingResult, J as LinkedIdentitiesQuery, K as LinkedIdentitiesResult, M as SmsVerifyResult, N as ClaimTransferInput, Q as ClaimResult, T as ClaimCurrencyInput, U as TransactionStatusResult, W as GuardianApprovalStatusResult, X as ServerDestinationsQuery, c as DestinationsResult, Y as PhoneShareContact, Z as PhoneShareInitiateResult, _ as PhoneShareApproveInput, $ as PhoneShareApproveResult, a0 as PhoneShareStatusResult } from './types-DTAmriOB.js';
2
+ export { a1 as CurrencyBalance, f as DestinationGame, a2 as InboundPendingItem, g as InvoErrorInfo, h as InvoHooks, i as InvoRequestInfo, j as InvoResponseInfo, a3 as LinkedIdentityEmail, R as Rail, V as VerificationMethod } from './types-DTAmriOB.js';
3
3
 
4
4
  interface VerifyWebhookOptions {
5
5
  /** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
@@ -258,7 +258,39 @@ declare class InvoServer {
258
258
  * a missing approval throws a 404 InvoError (`{status:"not_found"}`).
259
259
  */
260
260
  getGuardianApprovalStatus(transactionId: string, opts?: CallOptions): Promise<GuardianApprovalStatusResult>;
261
+ /**
262
+ * List the games/tenants a player can send/transfer to FROM `sourceGameId`, with
263
+ * display metadata inline (name, icon, currency, min/max limits) — one call, no
264
+ * per-game lookup. SERVER variant (game-secret) of the browser `InvoClient.getDestinations`;
265
+ * the source game is passed explicitly rather than inferred from a player token. Rows
266
+ * share the browser shape. Idempotent (a POST read — safe to retry).
267
+ */
268
+ getDestinations(query: ServerDestinationsQuery, opts?: CallOptions): Promise<DestinationsResult>;
269
+ /**
270
+ * Phone-share OTP handshake — resolves a 409 `PHONE_SHARE_APPROVAL_REQUIRED`.
271
+ *
272
+ * These three endpoints are UNAUTHENTICATED (phone-owner-driven: no game secret,
273
+ * no player token) — a partner backend relays the handshake. The triggering 409
274
+ * carries `approval_id, expires_at, code_sent, existing_account_hints[], next_endpoint`.
275
+ *
276
+ * `phoneShareInitiate` is the fallback OTP send (use when the 409 didn't already
277
+ * dispatch a code). NOT idempotent — never auto-retried. OTP TTL ~10 min.
278
+ */
279
+ phoneShareInitiate(input: PhoneShareContact, opts?: CallOptions): Promise<PhoneShareInitiateResult>;
280
+ /**
281
+ * Approve a phone-share with the OTP the phone owner received. On success the grant
282
+ * is stored server-side, keyed by (phone, requesting_email) and auto-consumed — the
283
+ * caller simply RE-ISSUES the identical original request. UNAUTHENTICATED. NOT
284
+ * idempotent (attempts are counted) — never auto-retried.
285
+ */
286
+ phoneShareApprove(input: PhoneShareApproveInput, opts?: CallOptions): Promise<PhoneShareApproveResult>;
287
+ /**
288
+ * Poll whether a phone-share (phone, requesting_email) pair is approved yet.
289
+ * UNAUTHENTICATED, idempotent GET. `approved:true` means the original request can
290
+ * be re-issued.
291
+ */
292
+ phoneShareStatus(input: PhoneShareContact, opts?: CallOptions): Promise<PhoneShareStatusResult>;
261
293
  private toInitiateResult;
262
294
  }
263
295
 
264
- export { CallOptions, ClaimCurrencyInput, ClaimResult, ClaimTransferInput, ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, GuardianApprovalStatusResult, InboundPendingQuery, InboundPendingResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoError, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, LinkedIdentitiesQuery, LinkedIdentitiesResult, OrderDetailsResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, SmsVerifyResult, TransactionStatusResult, type TransferEventData, type VerifyWebhookOptions, type WebhookHandlerOptions, createWebhookHandler, verifyWebhook, verifyWebhookAsync };
296
+ export { CallOptions, ClaimCurrencyInput, ClaimResult, ClaimTransferInput, ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, DestinationsResult, GuardianApprovalStatusResult, InboundPendingQuery, InboundPendingResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoError, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, LinkedIdentitiesQuery, LinkedIdentitiesResult, OrderDetailsResult, PhoneShareApproveInput, PhoneShareApproveResult, PhoneShareContact, PhoneShareInitiateResult, PhoneShareStatusResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, ServerDestinationsQuery, SmsVerifyResult, TransactionStatusResult, type TransferEventData, type VerifyWebhookOptions, type WebhookHandlerOptions, createWebhookHandler, verifyWebhook, verifyWebhookAsync };
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
- import { InvoError, assertSecureBaseUrl, Http } from './chunk-ZVKWDXSL.js';
2
- export { InvoError } from './chunk-ZVKWDXSL.js';
1
+ import { InvoError, assertSecureBaseUrl, Http, toDestinationsResult } from './chunk-P65XQ6VF.js';
2
+ export { InvoError } from './chunk-P65XQ6VF.js';
3
3
  import { createHmac } from 'crypto';
4
4
 
5
5
  var DEFAULT_TOLERANCE_SEC = 300;
@@ -161,7 +161,7 @@ async function hmacHexSubtle(secret, message) {
161
161
  }
162
162
 
163
163
  // src/server.ts
164
- var DEFAULT_UA = "invonetwork-web-sdk/1.1.0 (+https://invo.network)";
164
+ var DEFAULT_UA = "invonetwork-web-sdk/1.2.0 (+https://invo.network)";
165
165
  var MAX_USD_AMOUNT = 999.99;
166
166
  var MAX_ITEM_PRICE = 999999.99;
167
167
  function invalidInput(label, value, why) {
@@ -274,6 +274,24 @@ function toGuardianApprovalStatus(raw) {
274
274
  raw
275
275
  };
276
276
  }
277
+ function toPhoneShareInitiateResult(raw) {
278
+ return {
279
+ approvalId: raw["approval_id"],
280
+ codeSent: typeof raw["code_sent"] === "boolean" ? raw["code_sent"] : void 0,
281
+ expiresAt: raw["expires_at"],
282
+ alreadyApproved: typeof raw["already_approved"] === "boolean" ? raw["already_approved"] : void 0,
283
+ raw
284
+ };
285
+ }
286
+ function toPhoneShareApproveResult(raw) {
287
+ return {
288
+ approvalId: raw["approval_id"],
289
+ approved: typeof raw["approved"] === "boolean" ? raw["approved"] : void 0,
290
+ alreadyApproved: typeof raw["already_approved"] === "boolean" ? raw["already_approved"] : void 0,
291
+ expiresAt: raw["expires_at"],
292
+ raw
293
+ };
294
+ }
277
295
  function toCurrencyBalance(row) {
278
296
  return {
279
297
  currencyId: row["currency_id"] ?? "",
@@ -846,6 +864,101 @@ var InvoServer = class {
846
864
  );
847
865
  return toGuardianApprovalStatus(raw);
848
866
  }
867
+ /**
868
+ * List the games/tenants a player can send/transfer to FROM `sourceGameId`, with
869
+ * display metadata inline (name, icon, currency, min/max limits) — one call, no
870
+ * per-game lookup. SERVER variant (game-secret) of the browser `InvoClient.getDestinations`;
871
+ * the source game is passed explicitly rather than inferred from a player token. Rows
872
+ * share the browser shape. Idempotent (a POST read — safe to retry).
873
+ */
874
+ async getDestinations(query, opts) {
875
+ if (query.sourceGameId == null || String(query.sourceGameId).trim() === "") {
876
+ throw invalidInput("sourceGameId", query.sourceGameId, "is required");
877
+ }
878
+ const direction = query.direction ?? "transfer";
879
+ const path = direction === "send" ? "/api/currency-sends/available-destinations" : "/api/transfers/available-destinations";
880
+ const raw = await this.http.post(
881
+ path,
882
+ { source_game_id: query.sourceGameId },
883
+ this.auth,
884
+ { idempotent: true, signal: opts?.signal }
885
+ // a POST read — no side effect, safe to retry
886
+ );
887
+ return toDestinationsResult(raw, direction);
888
+ }
889
+ /**
890
+ * Phone-share OTP handshake — resolves a 409 `PHONE_SHARE_APPROVAL_REQUIRED`.
891
+ *
892
+ * These three endpoints are UNAUTHENTICATED (phone-owner-driven: no game secret,
893
+ * no player token) — a partner backend relays the handshake. The triggering 409
894
+ * carries `approval_id, expires_at, code_sent, existing_account_hints[], next_endpoint`.
895
+ *
896
+ * `phoneShareInitiate` is the fallback OTP send (use when the 409 didn't already
897
+ * dispatch a code). NOT idempotent — never auto-retried. OTP TTL ~10 min.
898
+ */
899
+ async phoneShareInitiate(input, opts) {
900
+ if (typeof input.phone !== "string" || !input.phone.trim()) {
901
+ throw invalidInput("phone", input.phone, "is required");
902
+ }
903
+ if (typeof input.email !== "string" || !input.email.trim()) {
904
+ throw invalidInput("email", input.email, "is required");
905
+ }
906
+ const raw = await this.http.post(
907
+ "/api/wallet/phone-share/initiate",
908
+ { phone: input.phone, email: input.email },
909
+ { kind: "none" },
910
+ // UNAUTHENTICATED — phone-owner-driven; send no auth header
911
+ { signal: opts?.signal }
912
+ // NOT idempotent — sends an OTP; never auto-retried
913
+ );
914
+ return toPhoneShareInitiateResult(raw);
915
+ }
916
+ /**
917
+ * Approve a phone-share with the OTP the phone owner received. On success the grant
918
+ * is stored server-side, keyed by (phone, requesting_email) and auto-consumed — the
919
+ * caller simply RE-ISSUES the identical original request. UNAUTHENTICATED. NOT
920
+ * idempotent (attempts are counted) — never auto-retried.
921
+ */
922
+ async phoneShareApprove(input, opts) {
923
+ if (typeof input.approvalId !== "string" || !input.approvalId.trim()) {
924
+ throw invalidInput("approvalId", input.approvalId, "is required");
925
+ }
926
+ if (typeof input.otp !== "string" || !input.otp.trim()) {
927
+ throw invalidInput("otp", input.otp, "is required");
928
+ }
929
+ const raw = await this.http.post(
930
+ "/api/wallet/phone-share/approve",
931
+ { approval_id: input.approvalId, otp: input.otp },
932
+ { kind: "none" },
933
+ // UNAUTHENTICATED — phone-owner-driven; send no auth header
934
+ { signal: opts?.signal }
935
+ // NOT idempotent — OTP attempts are counted; never auto-retried
936
+ );
937
+ return toPhoneShareApproveResult(raw);
938
+ }
939
+ /**
940
+ * Poll whether a phone-share (phone, requesting_email) pair is approved yet.
941
+ * UNAUTHENTICATED, idempotent GET. `approved:true` means the original request can
942
+ * be re-issued.
943
+ */
944
+ async phoneShareStatus(input, opts) {
945
+ if (typeof input.phone !== "string" || !input.phone.trim()) {
946
+ throw invalidInput("phone", input.phone, "is required");
947
+ }
948
+ if (typeof input.email !== "string" || !input.email.trim()) {
949
+ throw invalidInput("email", input.email, "is required");
950
+ }
951
+ const q = new URLSearchParams();
952
+ q.set("phone", input.phone);
953
+ q.set("email", input.email);
954
+ const raw = await this.http.get(
955
+ `/api/wallet/phone-share/status?${q.toString()}`,
956
+ { kind: "none" },
957
+ // UNAUTHENTICATED — phone-owner-driven; send no auth header
958
+ { signal: opts?.signal }
959
+ );
960
+ return { approved: raw["approved"] === true, raw };
961
+ }
849
962
  toInitiateResult(raw) {
850
963
  const vm = raw["verification_method"];
851
964
  const guardian = raw["guardian_approval"];
@@ -39,6 +39,8 @@ declare class InvoError extends Error {
39
39
  get isInsufficientBalance(): boolean;
40
40
  /** True if a claim needs phone-share approval before it can complete (claim → 409). */
41
41
  get isPhoneShareApprovalRequired(): boolean;
42
+ /** True if the phone-share (phone, requesting_email) pair was already approved. */
43
+ get isPhoneShareAlreadyApproved(): boolean;
42
44
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
43
45
  get isDuplicateRequest(): boolean;
44
46
  /** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
@@ -407,6 +409,14 @@ interface DestinationsQuery {
407
409
  /** "transfer" | "send". Defaults "transfer" (the set is identical for both today). */
408
410
  direction?: "transfer" | "send";
409
411
  }
412
+ /** Server-side (game-secret) destinations query. Unlike the browser variant, the
413
+ * source game isn't inferred from a player token — pass it explicitly. */
414
+ interface ServerDestinationsQuery {
415
+ /** The game the player is sending/transferring FROM. */
416
+ sourceGameId: string | number;
417
+ /** "transfer" | "send". Defaults "transfer" (the set is identical for both today). */
418
+ direction?: "transfer" | "send";
419
+ }
410
420
  /** A game/tenant this player can send/transfer to, with display metadata inline. */
411
421
  interface DestinationGame {
412
422
  gameId: string | number;
@@ -535,5 +545,53 @@ interface GuardianApprovalStatusResult {
535
545
  actionDescription?: string;
536
546
  raw: Record<string, unknown>;
537
547
  }
548
+ /**
549
+ * A phone + requesting-email pair identifying a phone-share approval. Used to send
550
+ * the fallback OTP and to poll status. The server-side grant is keyed by this pair.
551
+ */
552
+ interface PhoneShareContact {
553
+ /** The phone number whose owner must approve the share. */
554
+ phone: string;
555
+ /** The email of the account requesting to use that phone (requesting_email). */
556
+ email: string;
557
+ }
558
+ /** Result of `phoneShareInitiate` (fallback OTP send). Known fields are surfaced;
559
+ * everything else stays on `raw`. */
560
+ interface PhoneShareInitiateResult {
561
+ /** The approval id to pass back to `phoneShareApprove`. */
562
+ approvalId?: string;
563
+ /** Whether the backend actually dispatched an OTP. */
564
+ codeSent?: boolean;
565
+ /** When the OTP / approval expires (ISO). OTP TTL is ~10 min. */
566
+ expiresAt?: string;
567
+ /** true when the (phone, email) pair was already approved — no OTP needed. */
568
+ alreadyApproved?: boolean;
569
+ raw: Record<string, unknown>;
570
+ }
571
+ /** Input for `phoneShareApprove`. */
572
+ interface PhoneShareApproveInput {
573
+ /** The `approval_id` from the 409 body or from `phoneShareInitiate`. */
574
+ approvalId: string;
575
+ /** The one-time code the phone owner received. */
576
+ otp: string;
577
+ }
578
+ /** Result of `phoneShareApprove`. Known fields are surfaced; the rest stays on `raw`.
579
+ * On success the grant is stored server-side, so the caller simply re-issues the
580
+ * identical original request (the grant is auto-consumed). */
581
+ interface PhoneShareApproveResult {
582
+ approvalId?: string;
583
+ /** true once the share is approved. */
584
+ approved?: boolean;
585
+ /** true when the pair was already approved before this call. */
586
+ alreadyApproved?: boolean;
587
+ expiresAt?: string;
588
+ raw: Record<string, unknown>;
589
+ }
590
+ /** Result of `phoneShareStatus` (idempotent GET). */
591
+ interface PhoneShareStatusResult {
592
+ /** true when the (phone, email) pair is approved and the original request can be re-issued. */
593
+ approved: boolean;
594
+ raw: Record<string, unknown>;
595
+ }
538
596
 
539
- export { type ApproveResult as A, type BalanceResult as B, type ClientConfig as C, type DestinationsQuery as D, type EnrollmentBeginResult as E, type PlayerBalanceResult as F, type InboundPendingQuery as G, type InboundPendingResult as H, InvoError as I, type LinkedIdentitiesQuery as J, type LinkedIdentitiesResult as K, type LinkDeviceResult as L, type SmsVerifyResult as M, type ClaimTransferInput as N, type OrderDetailsResult as O, type PendingCollectResult as P, type ClaimResult as Q, type Rail as R, type ServerConfig as S, type ClaimCurrencyInput as T, type TransactionStatusResult as U, type VerificationMethod as V, type GuardianApprovalStatusResult as W, type CurrencyBalance as X, type InboundPendingItem as Y, type LinkedIdentityEmail as Z, type CallOptions as a, type ConfirmReceiptResult as b, type DestinationsResult as c, type EnrollmentVerifyResult as d, type BalanceRow as e, type DestinationGame as f, type InvoErrorInfo as g, type InvoHooks as h, type InvoRequestInfo as i, type InvoResponseInfo as j, type PendingCollectItem as k, type PlayerToken as l, type InitiateSendInput as m, type InitiateResult as n, type InitiateTransferInput as o, type CreateCheckoutInput as p, type CreateCheckoutResult as q, type PurchaseInput as r, type PurchaseResult as s, type ConfirmPaymentResult as t, type PurchaseItemInput as u, type PurchaseItemResult as v, type ItemHistoryQuery as w, type ItemHistoryResult as x, type ItemOrderQuery as y, type PlayerBalanceQuery as z };
597
+ export { type PhoneShareApproveResult as $, type ApproveResult as A, type BalanceResult as B, type ClientConfig as C, type DestinationsQuery as D, type EnrollmentBeginResult as E, type PlayerBalanceResult as F, type InboundPendingQuery as G, type InboundPendingResult as H, InvoError as I, type LinkedIdentitiesQuery as J, type LinkedIdentitiesResult as K, type LinkDeviceResult as L, type SmsVerifyResult as M, type ClaimTransferInput as N, type OrderDetailsResult as O, type PendingCollectResult as P, type ClaimResult as Q, type Rail as R, type ServerConfig as S, type ClaimCurrencyInput as T, type TransactionStatusResult as U, type VerificationMethod as V, type GuardianApprovalStatusResult as W, type ServerDestinationsQuery as X, type PhoneShareContact as Y, type PhoneShareInitiateResult as Z, type PhoneShareApproveInput as _, type CallOptions as a, type PhoneShareStatusResult as a0, type CurrencyBalance as a1, type InboundPendingItem as a2, type LinkedIdentityEmail as a3, type ConfirmReceiptResult as b, type DestinationsResult as c, type EnrollmentVerifyResult as d, type BalanceRow as e, type DestinationGame as f, type InvoErrorInfo as g, type InvoHooks as h, type InvoRequestInfo as i, type InvoResponseInfo as j, type PendingCollectItem as k, type PlayerToken as l, type InitiateSendInput as m, type InitiateResult as n, type InitiateTransferInput as o, type CreateCheckoutInput as p, type CreateCheckoutResult as q, type PurchaseInput as r, type PurchaseResult as s, type ConfirmPaymentResult as t, type PurchaseItemInput as u, type PurchaseItemResult as v, type ItemHistoryQuery as w, type ItemHistoryResult as x, type ItemOrderQuery as y, type PlayerBalanceQuery as z };
@@ -39,6 +39,8 @@ declare class InvoError extends Error {
39
39
  get isInsufficientBalance(): boolean;
40
40
  /** True if a claim needs phone-share approval before it can complete (claim → 409). */
41
41
  get isPhoneShareApprovalRequired(): boolean;
42
+ /** True if the phone-share (phone, requesting_email) pair was already approved. */
43
+ get isPhoneShareAlreadyApproved(): boolean;
42
44
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
43
45
  get isDuplicateRequest(): boolean;
44
46
  /** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
@@ -407,6 +409,14 @@ interface DestinationsQuery {
407
409
  /** "transfer" | "send". Defaults "transfer" (the set is identical for both today). */
408
410
  direction?: "transfer" | "send";
409
411
  }
412
+ /** Server-side (game-secret) destinations query. Unlike the browser variant, the
413
+ * source game isn't inferred from a player token — pass it explicitly. */
414
+ interface ServerDestinationsQuery {
415
+ /** The game the player is sending/transferring FROM. */
416
+ sourceGameId: string | number;
417
+ /** "transfer" | "send". Defaults "transfer" (the set is identical for both today). */
418
+ direction?: "transfer" | "send";
419
+ }
410
420
  /** A game/tenant this player can send/transfer to, with display metadata inline. */
411
421
  interface DestinationGame {
412
422
  gameId: string | number;
@@ -535,5 +545,53 @@ interface GuardianApprovalStatusResult {
535
545
  actionDescription?: string;
536
546
  raw: Record<string, unknown>;
537
547
  }
548
+ /**
549
+ * A phone + requesting-email pair identifying a phone-share approval. Used to send
550
+ * the fallback OTP and to poll status. The server-side grant is keyed by this pair.
551
+ */
552
+ interface PhoneShareContact {
553
+ /** The phone number whose owner must approve the share. */
554
+ phone: string;
555
+ /** The email of the account requesting to use that phone (requesting_email). */
556
+ email: string;
557
+ }
558
+ /** Result of `phoneShareInitiate` (fallback OTP send). Known fields are surfaced;
559
+ * everything else stays on `raw`. */
560
+ interface PhoneShareInitiateResult {
561
+ /** The approval id to pass back to `phoneShareApprove`. */
562
+ approvalId?: string;
563
+ /** Whether the backend actually dispatched an OTP. */
564
+ codeSent?: boolean;
565
+ /** When the OTP / approval expires (ISO). OTP TTL is ~10 min. */
566
+ expiresAt?: string;
567
+ /** true when the (phone, email) pair was already approved — no OTP needed. */
568
+ alreadyApproved?: boolean;
569
+ raw: Record<string, unknown>;
570
+ }
571
+ /** Input for `phoneShareApprove`. */
572
+ interface PhoneShareApproveInput {
573
+ /** The `approval_id` from the 409 body or from `phoneShareInitiate`. */
574
+ approvalId: string;
575
+ /** The one-time code the phone owner received. */
576
+ otp: string;
577
+ }
578
+ /** Result of `phoneShareApprove`. Known fields are surfaced; the rest stays on `raw`.
579
+ * On success the grant is stored server-side, so the caller simply re-issues the
580
+ * identical original request (the grant is auto-consumed). */
581
+ interface PhoneShareApproveResult {
582
+ approvalId?: string;
583
+ /** true once the share is approved. */
584
+ approved?: boolean;
585
+ /** true when the pair was already approved before this call. */
586
+ alreadyApproved?: boolean;
587
+ expiresAt?: string;
588
+ raw: Record<string, unknown>;
589
+ }
590
+ /** Result of `phoneShareStatus` (idempotent GET). */
591
+ interface PhoneShareStatusResult {
592
+ /** true when the (phone, email) pair is approved and the original request can be re-issued. */
593
+ approved: boolean;
594
+ raw: Record<string, unknown>;
595
+ }
538
596
 
539
- export { type ApproveResult as A, type BalanceResult as B, type ClientConfig as C, type DestinationsQuery as D, type EnrollmentBeginResult as E, type PlayerBalanceResult as F, type InboundPendingQuery as G, type InboundPendingResult as H, InvoError as I, type LinkedIdentitiesQuery as J, type LinkedIdentitiesResult as K, type LinkDeviceResult as L, type SmsVerifyResult as M, type ClaimTransferInput as N, type OrderDetailsResult as O, type PendingCollectResult as P, type ClaimResult as Q, type Rail as R, type ServerConfig as S, type ClaimCurrencyInput as T, type TransactionStatusResult as U, type VerificationMethod as V, type GuardianApprovalStatusResult as W, type CurrencyBalance as X, type InboundPendingItem as Y, type LinkedIdentityEmail as Z, type CallOptions as a, type ConfirmReceiptResult as b, type DestinationsResult as c, type EnrollmentVerifyResult as d, type BalanceRow as e, type DestinationGame as f, type InvoErrorInfo as g, type InvoHooks as h, type InvoRequestInfo as i, type InvoResponseInfo as j, type PendingCollectItem as k, type PlayerToken as l, type InitiateSendInput as m, type InitiateResult as n, type InitiateTransferInput as o, type CreateCheckoutInput as p, type CreateCheckoutResult as q, type PurchaseInput as r, type PurchaseResult as s, type ConfirmPaymentResult as t, type PurchaseItemInput as u, type PurchaseItemResult as v, type ItemHistoryQuery as w, type ItemHistoryResult as x, type ItemOrderQuery as y, type PlayerBalanceQuery as z };
597
+ export { type PhoneShareApproveResult as $, type ApproveResult as A, type BalanceResult as B, type ClientConfig as C, type DestinationsQuery as D, type EnrollmentBeginResult as E, type PlayerBalanceResult as F, type InboundPendingQuery as G, type InboundPendingResult as H, InvoError as I, type LinkedIdentitiesQuery as J, type LinkedIdentitiesResult as K, type LinkDeviceResult as L, type SmsVerifyResult as M, type ClaimTransferInput as N, type OrderDetailsResult as O, type PendingCollectResult as P, type ClaimResult as Q, type Rail as R, type ServerConfig as S, type ClaimCurrencyInput as T, type TransactionStatusResult as U, type VerificationMethod as V, type GuardianApprovalStatusResult as W, type ServerDestinationsQuery as X, type PhoneShareContact as Y, type PhoneShareInitiateResult as Z, type PhoneShareApproveInput as _, type CallOptions as a, type PhoneShareStatusResult as a0, type CurrencyBalance as a1, type InboundPendingItem as a2, type LinkedIdentityEmail as a3, type ConfirmReceiptResult as b, type DestinationsResult as c, type EnrollmentVerifyResult as d, type BalanceRow as e, type DestinationGame as f, type InvoErrorInfo as g, type InvoHooks as h, type InvoRequestInfo as i, type InvoResponseInfo as j, type PendingCollectItem as k, type PlayerToken as l, type InitiateSendInput as m, type InitiateResult as n, type InitiateTransferInput as o, type CreateCheckoutInput as p, type CreateCheckoutResult as q, type PurchaseInput as r, type PurchaseResult as s, type ConfirmPaymentResult as t, type PurchaseItemInput as u, type PurchaseItemResult as v, type ItemHistoryQuery as w, type ItemHistoryResult as x, type ItemOrderQuery as y, type PlayerBalanceQuery as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invonetwork/web-sdk",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "INVO Web SDK — currency purchase + passkey (WebAuthn) verification for partner web platforms.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,