@invonetwork/web-sdk 0.5.0 → 0.7.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
@@ -1,150 +1,179 @@
1
- # Changelog
2
-
3
- All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
4
- [Semantic Versioning](https://semver.org/). Releases are managed with
5
- [changesets](https://github.com/changesets/changesets).
6
-
7
- ## [0.5.0] — 2026-07-01
8
-
9
- Support partner `metadata` end-to-end on the purchase flow (additive, backward-compatible).
10
-
11
- - **`PurchaseCompletedData.metadata?`** the `purchase.completed` webhook now echoes your
12
- metadata back (all rails); `event.data.metadata` is typed.
13
- - **`purchaseCurrency`** now accepts and forwards `metadata` (the direct `/purchase-currency`
14
- endpoint accepts it too, matching `createCheckout`).
15
-
16
- ## [0.4.2] 2026-06-30
17
-
18
- Fixes from an independent line-by-line audit against the live backend.
19
-
20
- - **`linkDevice` now works.** `device/link/webauthn/begin` returns a wrapped
21
- `{ link_id, options }` body (challenge nested under `options`) and binds the
22
- challenge to a server-generated `link_id`. The client now unwraps `options` and
23
- echoes the server's `link_id` to `/complete` (it previously threw on an undefined
24
- challenge and sent the wrong id).
25
- - **`InvoError.code` is now populated for the direct purchase rail + guardian flows.**
26
- `errorFromResponse` reads `error_code` (SecureErrorHandler + `/purchase-currency`)
27
- in addition to `code`, and promotes known no-code tokens (`flow_paused`,
28
- `spending_limit_exceeded`, `receiver_not_enrolled_use_claim_code`) to `.code`. Fixes
29
- `isDuplicateRequest` on the direct rail.
30
- - **Guardian/minor routing.** The 202 guardian body also carries
31
- `verification_method:"sms"`; the SDK now suppresses it (reports
32
- `verificationMethod: undefined`) so a guardian-pending minor isn't routed into the
33
- SMS-PIN UI. README example reordered to branch on `guardianApproval` first.
34
-
35
- ## [0.4.1] — 2026-06-30
36
-
37
- Docs only — replaced the README's internal-leaking "Deployment prerequisites"
38
- (backend flag names, DB columns, gating mechanics) with a partner-facing
39
- "Before you go live"; republished so the npm page README is current.
40
-
41
- ## [0.4.0] 2026-06-30
42
-
43
- Additive release more server reads, edge-ready webhooks, cancellation, and tooling.
44
-
45
- - **`getInboundPending({ playerEmail | playerPhone })`** live, unclaimed inbound
46
- sends/transfers for a player (the source of truth behind the "you have X to collect"
47
- badge; pairs with `transfer.claim_pending`).
48
- - **`verifyWebhookAsync`** — Web Crypto variant of `verifyWebhook` that runs on
49
- Cloudflare Workers / Deno / Vercel+Netlify Edge / Bun / browsers; and
50
- **`createWebhookHandler`** a zero-dep Fetch-API `(Request) => Promise<Response>`
51
- webhook route handler (Next.js App Router, Workers, Deno, Hono, Bun).
52
- - **`iterateItemPurchaseHistory`** async iterator that pages through a player's
53
- full item-purchase history.
54
- - **Per-call `AbortSignal`** every method accepts an optional `{ signal }`; an
55
- aborted call throws `InvoError` code `ABORTED` and is never retried.
56
- - **Tooling**: ESLint (+ lint in CI), changesets release automation, `SECURITY.md`,
57
- and `CODEOWNERS`.
58
-
59
- ## [0.3.0] 2026-06-30
60
-
61
- Additive release new server capabilities plus transport resilience/observability.
62
-
63
- - **Webhook verification** (`/server`): `verifyWebhook(rawBody, signatureHeader, secret | secrets, opts?)`
64
- constant-time HMAC-SHA256 over `${t}.${rawBody}`, 5-minute replay window,
65
- multi-secret rotation; returns a typed `InvoWebhookEvent` discriminated union
66
- (`purchase.*`, `item.purchased`, `transfer.*`, `payout.status_changed`, `webhook.test`).
67
- Throws `InvoError` (`WEBHOOK_SIGNATURE_INVALID` / `WEBHOOK_TIMESTAMP_EXPIRED` /
68
- `WEBHOOK_MALFORMED` / `WEBHOOK_SECRET_MISSING`). Server-only; the browser bundle
69
- stays crypto-free. Independently security-audited.
70
- - **`getPlayerBalance({ playerEmail | playerId })`** (`/server`): typed `player` / `balances` / `summary`.
71
- - **Automatic retries**: network errors/timeouts, `429` (honoring `retry_after`), and
72
- `5xx` are retried with exponential backoff + jitter. New config `maxRetries`
73
- (default 2, `0` disables) and `retryBaseDelayMs` (default 250).
74
- - **Observability hooks**: optional `onRequest` / `onResponse` / `onError` on both
75
- entries (best-effort/non-throwing); `InvoError.requestId` carries the backend
76
- request id for support/tracing.
77
- - **Typed reads**: `confirmPayment` `ConfirmPaymentResult`; `getOrderDetails` /
78
- `getItemOrderDetails` `OrderDetailsResult`; `getItemPurchaseHistory`
79
- `ItemHistoryResult` (previously untyped `Record`). All keep `raw`.
80
- - **Light validation**: `mintPlayerToken` and `createCheckout` require a non-blank
81
- `playerEmail` (throws `INVALID_INPUT` before the network call).
82
- - **License**: `package.json` `license` is now `SEE LICENSE IN LICENSE` (was
83
- `UNLICENSED`); `LICENSE` rewritten as an explicit install-and-use grant for
84
- building INVO integrations.
85
-
86
- ## [0.2.1] — 2026-06-30
87
-
88
- Docs onlyno code change (republished so the npm page README is current).
89
-
90
- - Rewrote the README into a complete integration/deployment guide: capability
91
- overview, architecture, deployment prerequisites, per-flow sections (currency
92
- purchase, item purchase, sends, transfers, passkeys), webhooks, errors, and a
93
- full API reference for both entries.
94
- - Added INVO console onboarding: `https://console.invo.network` (production) and
95
- `https://dev.console.invo.network` (testing/sandbox), mapped to their API base URLs.
96
-
97
- ## [0.2.0] 2026-06-30
98
-
99
- Adds **item purchase** (spend existing game currency on an in-game item, §4.8) — an
100
- additive, server-only surface.
101
-
102
- - `InvoServer` (`/server`): `purchaseItem`, `getItemPurchaseHistory`, `getItemOrderDetails`.
103
- - Server-side, game-secret auth no passkey, no real money, no payment rail (it's a
104
- balance debit). Grant the item off the `item.purchased` webhook.
105
- - Client-side guards: required fields (trim-checked), `itemQuantity` integer `1..1000`,
106
- prices `> 0` and `<= 999999.99` (magnitude-safe 2-decimal check), and
107
- `totalPrice == unitPrice × itemQuantity (±0.01)` compared in integer cents.
108
- - Load-bearing response fields (`transaction_id`, `order_id`) throw `INVALID_RESPONSE`
109
- if missing on a 200.
110
- - `InvoError` helpers: `isInsufficientBalance` (gated to 400; not the `429` throttle),
111
- `isDuplicateRequest` (409), `retryAfter` (numeric or string `retry_after`); all
112
- null-safe against non-JSON error bodies.
113
- - Independently audited (2 agents) against handoff doc §4.8/§6/§8; contract verified,
114
- error-classification edge cases fixed.
115
-
116
- ## [0.1.0] — 2026-06-30
117
-
118
- Initial scaffold.
119
-
120
- - `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
121
- `createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
122
- - Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
123
- `purchaseReference`, and `rail:"steam"` rejected before the network call
124
- (`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
125
- - `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
126
- `confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
127
- interchangeable-methods flow (§4.6).
128
- - Optional `refreshToken` hook: transparently re-mints and retries once on
129
- `SDK_TOKEN_EXPIRED` (§11.2).
130
- - Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
131
- - Tests: HTTP layer, server request mapping + purchase guards, browser client
132
- flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
133
- - Contracts extracted + auditor-verified against the INVO backend.
134
-
135
- ### Hardening (independent red-team pass)
136
-
137
- - **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
138
- `initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
139
- `guardianApproval` block on the guardian path, so callers don't route into the
140
- PIN UI by mistake (§4.3).
141
- - `usdAmount` validation tightened: rejects non-plain-decimal strings
142
- (`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
143
- - Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
144
- instead of silently surfacing as empty strings.
145
- - `getOrderDetails` requires at least one of `orderId`/`transactionId`.
146
- - Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
147
- (never replays a single-use assertion) and single-flights concurrent refreshes.
148
- - `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
149
- in cleartext.
150
- - Published tarball excludes sourcemaps (no proprietary source shipped).
1
+ # Changelog
2
+
3
+ All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/). Releases are managed with
5
+ [changesets](https://github.com/changesets/changesets).
6
+
7
+ ## [0.7.0] — 2026-07-01
8
+
9
+ Start of the **discovery layer** for send/transfer UIs.
10
+
11
+ - **`InvoClient.getDestinations({ direction })`** one player-token call
12
+ (`GET /api/sdk/destinations`) returns every game the player can send/transfer to, with
13
+ all display metadata **inline** (game name, icon, currency name/symbol, min/max transfer
14
+ limits) no per-game lookup. Includes source game/currency info and `transferMode`
15
+ (`universal`/`linked` + `linkedGameIds`).
16
+ - _(Coming in 0.7.x: `getBalance` single-game player-token read; `getLinkedIdentities`
17
+ server-side.)_
18
+
19
+ ## [0.6.0] — 2026-07-01
20
+
21
+ Three additive browser flows the dashboard needed, built to the live backend contracts.
22
+
23
+ - **`InvoClient.getPendingCollect()`** — the player's own pending-to-collect list
24
+ (player-token `GET /api/sdk/transfers/pending`), PII-free. Each row's `kind`
25
+ (`identity_gate` `approve*`, `receiving_confirm` `confirmReceipt*`) tells you the
26
+ action; typed fields incl. `held` / `holdReason` / `stepUpRequired`.
27
+ - **First-enrollment OTP grant** `enrollmentBegin()` / `enrollmentVerify(code)` for
28
+ `ENROLLMENT_REQUIRES_AUTHORIZATION` (send OTP to phone+email, verify, then retry the
29
+ same `enrollPasskey()` the server grant is auto-consumed). New `InvoError` helpers
30
+ `isEnrollmentAuthorizationRequired` / `isEnrollmentProofRequired`.
31
+ - **Typed approve/confirm holds** — `approve*`/`confirmReceipt*` now surface a typed
32
+ `holdReason` (from `error_code ?? code`) on HTTP 202 holds, plus `risk` /
33
+ `guardianApproval` / `pollEndpoint` on approve. `RECIPIENT_IDENTITY_PENDING` correctly
34
+ surfaces on confirm-receipt. Success carries no `holdReason`.
35
+
36
+ ## [0.5.0] — 2026-07-01
37
+
38
+ Support partner `metadata` end-to-end on the purchase flow (additive, backward-compatible).
39
+
40
+ - **`PurchaseCompletedData.metadata?`** — the `purchase.completed` webhook now echoes your
41
+ metadata back (all rails); `event.data.metadata` is typed.
42
+ - **`purchaseCurrency`** now accepts and forwards `metadata` (the direct `/purchase-currency`
43
+ endpoint accepts it too, matching `createCheckout`).
44
+
45
+ ## [0.4.2]2026-06-30
46
+
47
+ Fixes from an independent line-by-line audit against the live backend.
48
+
49
+ - **`linkDevice` now works.** `device/link/webauthn/begin` returns a wrapped
50
+ `{ link_id, options }` body (challenge nested under `options`) and binds the
51
+ challenge to a server-generated `link_id`. The client now unwraps `options` and
52
+ echoes the server's `link_id` to `/complete` (it previously threw on an undefined
53
+ challenge and sent the wrong id).
54
+ - **`InvoError.code` is now populated for the direct purchase rail + guardian flows.**
55
+ `errorFromResponse` reads `error_code` (SecureErrorHandler + `/purchase-currency`)
56
+ in addition to `code`, and promotes known no-code tokens (`flow_paused`,
57
+ `spending_limit_exceeded`, `receiver_not_enrolled_use_claim_code`) to `.code`. Fixes
58
+ `isDuplicateRequest` on the direct rail.
59
+ - **Guardian/minor routing.** The 202 guardian body also carries
60
+ `verification_method:"sms"`; the SDK now suppresses it (reports
61
+ `verificationMethod: undefined`) so a guardian-pending minor isn't routed into the
62
+ SMS-PIN UI. README example reordered to branch on `guardianApproval` first.
63
+
64
+ ## [0.4.1] 2026-06-30
65
+
66
+ Docs only replaced the README's internal-leaking "Deployment prerequisites"
67
+ (backend flag names, DB columns, gating mechanics) with a partner-facing
68
+ "Before you go live"; republished so the npm page README is current.
69
+
70
+ ## [0.4.0] 2026-06-30
71
+
72
+ Additive release more server reads, edge-ready webhooks, cancellation, and tooling.
73
+
74
+ - **`getInboundPending({ playerEmail | playerPhone })`** live, unclaimed inbound
75
+ sends/transfers for a player (the source of truth behind the "you have X to collect"
76
+ badge; pairs with `transfer.claim_pending`).
77
+ - **`verifyWebhookAsync`** Web Crypto variant of `verifyWebhook` that runs on
78
+ Cloudflare Workers / Deno / Vercel+Netlify Edge / Bun / browsers; and
79
+ **`createWebhookHandler`** a zero-dep Fetch-API `(Request) => Promise<Response>`
80
+ webhook route handler (Next.js App Router, Workers, Deno, Hono, Bun).
81
+ - **`iterateItemPurchaseHistory`** async iterator that pages through a player's
82
+ full item-purchase history.
83
+ - **Per-call `AbortSignal`** every method accepts an optional `{ signal }`; an
84
+ aborted call throws `InvoError` code `ABORTED` and is never retried.
85
+ - **Tooling**: ESLint (+ lint in CI), changesets release automation, `SECURITY.md`,
86
+ and `CODEOWNERS`.
87
+
88
+ ## [0.3.0]2026-06-30
89
+
90
+ Additive release new server capabilities plus transport resilience/observability.
91
+
92
+ - **Webhook verification** (`/server`): `verifyWebhook(rawBody, signatureHeader, secret | secrets, opts?)`
93
+ constant-time HMAC-SHA256 over `${t}.${rawBody}`, 5-minute replay window,
94
+ multi-secret rotation; returns a typed `InvoWebhookEvent` discriminated union
95
+ (`purchase.*`, `item.purchased`, `transfer.*`, `payout.status_changed`, `webhook.test`).
96
+ Throws `InvoError` (`WEBHOOK_SIGNATURE_INVALID` / `WEBHOOK_TIMESTAMP_EXPIRED` /
97
+ `WEBHOOK_MALFORMED` / `WEBHOOK_SECRET_MISSING`). Server-only; the browser bundle
98
+ stays crypto-free. Independently security-audited.
99
+ - **`getPlayerBalance({ playerEmail | playerId })`** (`/server`): typed `player` / `balances` / `summary`.
100
+ - **Automatic retries**: network errors/timeouts, `429` (honoring `retry_after`), and
101
+ `5xx` are retried with exponential backoff + jitter. New config `maxRetries`
102
+ (default 2, `0` disables) and `retryBaseDelayMs` (default 250).
103
+ - **Observability hooks**: optional `onRequest` / `onResponse` / `onError` on both
104
+ entries (best-effort/non-throwing); `InvoError.requestId` carries the backend
105
+ request id for support/tracing.
106
+ - **Typed reads**: `confirmPayment` `ConfirmPaymentResult`; `getOrderDetails` /
107
+ `getItemOrderDetails` `OrderDetailsResult`; `getItemPurchaseHistory`
108
+ `ItemHistoryResult` (previously untyped `Record`). All keep `raw`.
109
+ - **Light validation**: `mintPlayerToken` and `createCheckout` require a non-blank
110
+ `playerEmail` (throws `INVALID_INPUT` before the network call).
111
+ - **License**: `package.json` `license` is now `SEE LICENSE IN LICENSE` (was
112
+ `UNLICENSED`); `LICENSE` rewritten as an explicit install-and-use grant for
113
+ building INVO integrations.
114
+
115
+ ## [0.2.1] — 2026-06-30
116
+
117
+ Docs only — no code change (republished so the npm page README is current).
118
+
119
+ - Rewrote the README into a complete integration/deployment guide: capability
120
+ overview, architecture, deployment prerequisites, per-flow sections (currency
121
+ purchase, item purchase, sends, transfers, passkeys), webhooks, errors, and a
122
+ full API reference for both entries.
123
+ - Added INVO console onboarding: `https://console.invo.network` (production) and
124
+ `https://dev.console.invo.network` (testing/sandbox), mapped to their API base URLs.
125
+
126
+ ## [0.2.0] 2026-06-30
127
+
128
+ Adds **item purchase** (spend existing game currency on an in-game item, §4.8) an
129
+ additive, server-only surface.
130
+
131
+ - `InvoServer` (`/server`): `purchaseItem`, `getItemPurchaseHistory`, `getItemOrderDetails`.
132
+ - Server-side, game-secret auth — no passkey, no real money, no payment rail (it's a
133
+ balance debit). Grant the item off the `item.purchased` webhook.
134
+ - Client-side guards: required fields (trim-checked), `itemQuantity` integer `1..1000`,
135
+ prices `> 0` and `<= 999999.99` (magnitude-safe 2-decimal check), and
136
+ `totalPrice == unitPrice × itemQuantity (±0.01)` compared in integer cents.
137
+ - Load-bearing response fields (`transaction_id`, `order_id`) throw `INVALID_RESPONSE`
138
+ if missing on a 200.
139
+ - `InvoError` helpers: `isInsufficientBalance` (gated to 400; not the `429` throttle),
140
+ `isDuplicateRequest` (409), `retryAfter` (numeric or string `retry_after`); all
141
+ null-safe against non-JSON error bodies.
142
+ - Independently audited (2 agents) against handoff doc §4.8/§6/§8; contract verified,
143
+ error-classification edge cases fixed.
144
+
145
+ ## [0.1.0] 2026-06-30
146
+
147
+ Initial scaffold.
148
+
149
+ - `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
150
+ `createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
151
+ - Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
152
+ `purchaseReference`, and `rail:"steam"` rejected before the network call
153
+ (`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
154
+ - `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
155
+ `confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
156
+ interchangeable-methods flow (§4.6).
157
+ - Optional `refreshToken` hook: transparently re-mints and retries once on
158
+ `SDK_TOKEN_EXPIRED` (§11.2).
159
+ - Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
160
+ - Tests: HTTP layer, server request mapping + purchase guards, browser client
161
+ flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
162
+ - Contracts extracted + auditor-verified against the INVO backend.
163
+
164
+ ### Hardening (independent red-team pass)
165
+
166
+ - **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
167
+ `initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
168
+ `guardianApproval` block on the guardian path, so callers don't route into the
169
+ PIN UI by mistake (§4.3).
170
+ - `usdAmount` validation tightened: rejects non-plain-decimal strings
171
+ (`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
172
+ - Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
173
+ instead of silently surfacing as empty strings.
174
+ - `getOrderDetails` requires at least one of `orderId`/`transactionId`.
175
+ - Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
176
+ (never replays a single-use assertion) and single-flights concurrent refreshes.
177
+ - `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
178
+ in cleartext.
179
+ - Published tarball excludes sourcemaps (no proprietary source shipped).
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  First-party TypeScript SDK for integrating **INVO** into partner **web** platforms (storefronts, web games, dashboards). It wraps INVO's web money flows behind a typed, versioned API — the web analog of the Unity/Unreal plugins.
4
4
 
5
- > **Status:** `v0.4.0`, published on npm. The backend it wraps is **live** on sandbox + production, so you can build and test against sandbox today.
5
+ > **Status:** `v0.7.0`, published on npm. The backend it wraps is **live** on sandbox + production, so you can build and test against sandbox today.
6
6
  > Canonical partner reference: **https://docs.invo.network/docs/currency-purchase** and **https://docs.invo.network/docs/game-developer-integration**.
7
7
 
8
8
  ## What it does
@@ -251,6 +251,13 @@ Move already-owned game currency from one player to another, authorized by the *
251
251
  | Approve (browser) | `approveSend` | `approveTransfer` — also returns the sender's **claim code** |
252
252
  | Recipient claim (browser) | `confirmReceiptSend` | `confirmReceiptTransfer` |
253
253
 
254
+ **Where can I send? (browser, v0.7.0+)** — before a player can move value, populate the "pick a destination" UI with `client.getDestinations({ direction: "transfer" | "send" })`. One player-token call returns every reachable game with display metadata inline — no per-game lookup:
255
+
256
+ ```ts
257
+ const { availableGames, sourceCurrencyName } = await client.getDestinations({ direction: "transfer" });
258
+ // each: { gameId, gameName, gameIcon, currencyName, currencySymbol, minimumTransfer, maximumTransfer, … }
259
+ ```
260
+
254
261
  ```ts
255
262
  // 1. SERVER — initiate, then branch on how the sender must verify
256
263
  const t = await server.initiateTransfer({
@@ -286,14 +293,12 @@ try {
286
293
  - **Claim codes** are returned only by `approveTransfer`; they're the out-of-band fallback when the recipient isn't enrolled.
287
294
  - **`err.isReceiverNotEnrolled`** on `confirmReceipt*` is the explicit signal to switch to claim-code entry.
288
295
  - Transfer self-claim may be disabled for your tenant; if it is, surface the claim-code path instead.
296
+ - **Holds:** an approve/claim can return an HTTP 202 hold instead of success — inspect `result.holdReason` (`"RISK_HOLD"`, `"GUARDIAN_APPROVAL_PENDING"`, and on confirm-receipt `"RECIPIENT_IDENTITY_PENDING"`); success has no `holdReason` (and `next: "pending_claim"`). Terminal guardian outcomes throw (`GUARDIAN_APPROVAL_REJECTED`/`_EXPIRED`).
289
297
 
290
- **"You have X to collect"** — to render a collect badge, list a player's live, unclaimed inbound sends/transfers (the source of truth behind the `transfer.claim_pending` webhook):
298
+ **"You have X to collect"**
291
299
 
292
- ```ts
293
- const { inboundPending } = await server.getInboundPending({ playerEmail: "q@example.com" });
294
- // each row: { transactionId, flow, amount, netAmount, sourceGame, toPhone, toIdentityId, claimCodeExpiresAt }
295
- // match toPhone to the logged-in player (toIdentityId is null when the phone maps to >1 of your players)
296
- ```
300
+ - **Browser (player-token):** `client.getPendingCollect()` returns the player's own pending items. Each row's `kind` tells you the action — `"identity_gate"` → `approve*` (they initiated), `"receiving_confirm"` → `confirmReceipt*` (a peer sent to them). PII-free (no claim code / phone).
301
+ - **Server (game-secret):** `server.getInboundPending({ playerEmail | playerPhone })` — richer, includes `toPhone`/`toIdentityId` for routing a notification to the right account. Match `toPhone` to the player (`toIdentityId` is null when the phone maps to >1 of your players).
297
302
 
298
303
  ---
299
304
 
@@ -312,6 +317,26 @@ await client.linkDevice(linkId); // → { status: "authorized" }
312
317
  await client.enrollPasskey();
313
318
  ```
314
319
 
320
+ **First-enrollment OTP grant.** For tenants that require it, the first `enrollPasskey()`
321
+ throws `ENROLLMENT_REQUIRES_AUTHORIZATION`. Send a one-time code (to the player's phone +
322
+ email on file), verify it, then **retry the same `enrollPasskey()`** — the server-side
323
+ grant is consumed automatically:
324
+
325
+ ```ts
326
+ try {
327
+ await client.enrollPasskey();
328
+ } catch (e) {
329
+ if (e instanceof InvoError && e.isEnrollmentAuthorizationRequired) {
330
+ await client.enrollmentBegin(); // → { status: "sent", channels: ["sms","email"] }
331
+ await client.enrollmentVerify(codeFromUser); // 6-digit OTP
332
+ await client.enrollPasskey(); // retry — grant auto-consumed
333
+ } else if (e instanceof InvoError && e.isEnrollmentProofRequired) {
334
+ await client.linkDevice(linkId); // a different method already exists
335
+ await client.enrollPasskey();
336
+ } else throw e;
337
+ }
338
+ ```
339
+
315
340
  - **User verification is required** on every approve/claim (a missing-UV assertion fails closed).
316
341
  - Challenges are single-use and bound to `{flow}:{transactionId}`.
317
342
  - The SDK passes the backend's WebAuthn options through unchanged (it does not hard-code `pubKeyCredParams`/`timeout`/`attestation`).
@@ -419,6 +444,8 @@ Helpers:
419
444
  | `.isInsufficientBalance` | item purchase failed (400); `required_amount` + `current_balance` on `.body` |
420
445
  | `.isDuplicateRequest` | idempotency-keyed request was a duplicate (409) |
421
446
  | `.retryAfter` | seconds to back off on a 429 throttle |
447
+ | `.isEnrollmentAuthorizationRequired` | first-enrollment needs the OTP grant → `enrollmentBegin`/`enrollmentVerify` |
448
+ | `.isEnrollmentProofRequired` | another method exists → prove it via `linkDevice` |
422
449
 
423
450
  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`.
424
451
 
@@ -465,11 +492,14 @@ Every method also accepts an optional final `{ signal }` (`AbortSignal`) for can
465
492
  | Method | Returns |
466
493
  |---|---|
467
494
  | `enrollPasskey()` | `{ status, device, raw }` |
468
- | `approveSend(txnId)` / `approveTransfer(txnId)` | `{ status, next, transactionId, claimCode?, claimCodeExpiresAt?, raw }` |
469
- | `confirmReceiptSend(txnId)` / `confirmReceiptTransfer(txnId)` | `{ status, raw }` |
495
+ | `enrollmentBegin()` / `enrollmentVerify(code)` | OTP grant for `ENROLLMENT_REQUIRES_AUTHORIZATION` (then retry `enrollPasskey`) |
496
+ | `approveSend(txnId)` / `approveTransfer(txnId)` | `{ status, next, transactionId, claimCode?, claimCodeExpiresAt?, holdReason?, risk?, guardianApproval?, pollEndpoint?, raw }` |
497
+ | `confirmReceiptSend(txnId)` / `confirmReceiptTransfer(txnId)` | `{ status, holdReason?, raw }` |
470
498
  | `linkDevice(linkId)` | `{ status, raw }` |
499
+ | `getPendingCollect()` | `{ pending, raw }` — the player's own pending-to-collect list (player-token) |
500
+ | `getDestinations({ direction? })` | `{ availableGames, sourceGameName, transferMode, … }` — where the player can send/transfer, metadata inline |
471
501
 
472
- Every method throws `InvoError` on failure. Full inline types ship with the package.
502
+ Every method throws `InvoError` on failure and takes an optional final `{ signal }`. Full inline types ship with the package.
473
503
 
474
504
  ---
475
505
 
@@ -481,7 +511,7 @@ npm run typecheck # tsc --noEmit
481
511
  npm test # vitest
482
512
  ```
483
513
 
484
- The package follows **semver**: patch = fixes, minor = additive surface, major = breaking changes (rare, with a migration note). The server contract is backward-compatible within a major, so an old pinned SDK keeps working. Pin a version and subscribe to release notes for security updates. See [`CHANGELOG.md`](CHANGELOG.md).
514
+ The package follows **semver**: patch = fixes, minor = additive surface, major = breaking changes (rare, with a migration note). The server contract is backward-compatible within a major, so an old pinned SDK keeps working. Pin a version and subscribe to release notes for security updates. Full history: [CHANGELOG](https://github.com/Invo-Technologies/invo-web-sdk/blob/main/CHANGELOG.md) · [release notes](https://github.com/Invo-Technologies/invo-web-sdk/blob/main/docs/RELEASES.md) (absolute links to `main`).
485
515
 
486
516
  ## License
487
517
 
@@ -17,6 +17,14 @@ var InvoError = class _InvoError extends Error {
17
17
  get isTokenExpired() {
18
18
  return this.code === "SDK_TOKEN_EXPIRED";
19
19
  }
20
+ /** True if `enrollPasskey()` needs the OTP-grant flow (`enrollmentBegin`/`enrollmentVerify`). */
21
+ get isEnrollmentAuthorizationRequired() {
22
+ return this.code === "ENROLLMENT_REQUIRES_AUTHORIZATION";
23
+ }
24
+ /** True if enrolling is blocked because another method exists — use `linkDevice`. */
25
+ get isEnrollmentProofRequired() {
26
+ return this.code === "ENROLLMENT_REQUIRES_PROOF";
27
+ }
20
28
  /**
21
29
  * True if an item purchase failed because the player's balance was too low (§4.8 → 400).
22
30
  * The backend carries `required_amount` + `current_balance` on the body for the UI.
@@ -249,5 +257,5 @@ function retryAfterMs(parsed, headers) {
249
257
  }
250
258
 
251
259
  export { Http, InvoError, assertSecureBaseUrl };
252
- //# sourceMappingURL=chunk-JOVATUDY.js.map
253
- //# sourceMappingURL=chunk-JOVATUDY.js.map
260
+ //# sourceMappingURL=chunk-D3XBTH4C.js.map
261
+ //# sourceMappingURL=chunk-D3XBTH4C.js.map