@invonetwork/web-sdk 0.8.0 → 1.1.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,194 +1,221 @@
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.8.0] — 2026-07-01
8
-
9
- Completes the **discovery layer** for send/transfer UIs.
10
-
11
- - **`InvoClient.getBalance()`** (browser, player-token, `GET /api/sdk/balance`) — the
12
- player's balances for the token's game: `balances[]` (`currencyId`, `currencyName`,
13
- `currencySymbol`/`currencySymbolUrl` (nullable), `availableBalance`/`reservedBalance`/
14
- `totalBalance` as decimal strings) plus `totalValue` / `currencyCount` / `hasFunds`. A
15
- player who hasn't transacted here yet returns an empty `balances` array (a `200`, not an
16
- error); `404 GAME_NOT_FOUND` throws.
17
- - **`InvoServer.getLinkedIdentities({ playerEmail | playerPhone })`** (server-only,
18
- game-secret) a player's linked wallet identities (game-scoped). Provide one identifier
19
- (phone wins if both); no in-game match returns `notFound: true` with empty `emails`
20
- instead of throwing. **Returns first-party PII — never expose to the browser.**
21
-
22
- ## [0.7.0]2026-07-01
23
-
24
- Start of the **discovery layer** for send/transfer UIs.
25
-
26
- - **`InvoClient.getDestinations({ direction })`** one player-token call
27
- (`GET /api/sdk/destinations`) returns every game the player can send/transfer to, with
28
- all display metadata **inline** (game name, icon, currency name/symbol, min/max transfer
29
- limits) no per-game lookup. Includes source game/currency info and `transferMode`
30
- (`universal`/`linked` + `linkedGameIds`).
31
- - _(Coming in 0.7.x: `getBalance` single-game player-token read; `getLinkedIdentities`
32
- server-side.)_
33
-
34
- ## [0.6.0] — 2026-07-01
35
-
36
- Three additive browser flows the dashboard needed, built to the live backend contracts.
37
-
38
- - **`InvoClient.getPendingCollect()`** the player's own pending-to-collect list
39
- (player-token `GET /api/sdk/transfers/pending`), PII-free. Each row's `kind`
40
- (`identity_gate` `approve*`, `receiving_confirm` → `confirmReceipt*`) tells you the
41
- action; typed fields incl. `held` / `holdReason` / `stepUpRequired`.
42
- - **First-enrollment OTP grant** `enrollmentBegin()` / `enrollmentVerify(code)` for
43
- `ENROLLMENT_REQUIRES_AUTHORIZATION` (send OTP to phone+email, verify, then retry the
44
- same `enrollPasskey()` the server grant is auto-consumed). New `InvoError` helpers
45
- `isEnrollmentAuthorizationRequired` / `isEnrollmentProofRequired`.
46
- - **Typed approve/confirm holds** `approve*`/`confirmReceipt*` now surface a typed
47
- `holdReason` (from `error_code ?? code`) on HTTP 202 holds, plus `risk` /
48
- `guardianApproval` / `pollEndpoint` on approve. `RECIPIENT_IDENTITY_PENDING` correctly
49
- surfaces on confirm-receipt. Success carries no `holdReason`.
50
-
51
- ## [0.5.0] 2026-07-01
52
-
53
- Support partner `metadata` end-to-end on the purchase flow (additive, backward-compatible).
54
-
55
- - **`PurchaseCompletedData.metadata?`** the `purchase.completed` webhook now echoes your
56
- metadata back (all rails); `event.data.metadata` is typed.
57
- - **`purchaseCurrency`** now accepts and forwards `metadata` (the direct `/purchase-currency`
58
- endpoint accepts it too, matching `createCheckout`).
59
-
60
- ## [0.4.2] — 2026-06-30
61
-
62
- Fixes from an independent line-by-line audit against the live backend.
63
-
64
- - **`linkDevice` now works.** `device/link/webauthn/begin` returns a wrapped
65
- `{ link_id, options }` body (challenge nested under `options`) and binds the
66
- challenge to a server-generated `link_id`. The client now unwraps `options` and
67
- echoes the server's `link_id` to `/complete` (it previously threw on an undefined
68
- challenge and sent the wrong id).
69
- - **`InvoError.code` is now populated for the direct purchase rail + guardian flows.**
70
- `errorFromResponse` reads `error_code` (SecureErrorHandler + `/purchase-currency`)
71
- in addition to `code`, and promotes known no-code tokens (`flow_paused`,
72
- `spending_limit_exceeded`, `receiver_not_enrolled_use_claim_code`) to `.code`. Fixes
73
- `isDuplicateRequest` on the direct rail.
74
- - **Guardian/minor routing.** The 202 guardian body also carries
75
- `verification_method:"sms"`; the SDK now suppresses it (reports
76
- `verificationMethod: undefined`) so a guardian-pending minor isn't routed into the
77
- SMS-PIN UI. README example reordered to branch on `guardianApproval` first.
78
-
79
- ## [0.4.1] — 2026-06-30
80
-
81
- Docs only — replaced the README's internal-leaking "Deployment prerequisites"
82
- (backend flag names, DB columns, gating mechanics) with a partner-facing
83
- "Before you go live"; republished so the npm page README is current.
84
-
85
- ## [0.4.0] 2026-06-30
86
-
87
- Additive releasemore server reads, edge-ready webhooks, cancellation, and tooling.
88
-
89
- - **`getInboundPending({ playerEmail | playerPhone })`** live, unclaimed inbound
90
- sends/transfers for a player (the source of truth behind the "you have X to collect"
91
- badge; pairs with `transfer.claim_pending`).
92
- - **`verifyWebhookAsync`** Web Crypto variant of `verifyWebhook` that runs on
93
- Cloudflare Workers / Deno / Vercel+Netlify Edge / Bun / browsers; and
94
- **`createWebhookHandler`** a zero-dep Fetch-API `(Request) => Promise<Response>`
95
- webhook route handler (Next.js App Router, Workers, Deno, Hono, Bun).
96
- - **`iterateItemPurchaseHistory`** async iterator that pages through a player's
97
- full item-purchase history.
98
- - **Per-call `AbortSignal`** every method accepts an optional `{ signal }`; an
99
- aborted call throws `InvoError` code `ABORTED` and is never retried.
100
- - **Tooling**: ESLint (+ lint in CI), changesets release automation, `SECURITY.md`,
101
- and `CODEOWNERS`.
102
-
103
- ## [0.3.0] 2026-06-30
104
-
105
- Additive release — new server capabilities plus transport resilience/observability.
106
-
107
- - **Webhook verification** (`/server`): `verifyWebhook(rawBody, signatureHeader, secret | secrets, opts?)`
108
- constant-time HMAC-SHA256 over `${t}.${rawBody}`, 5-minute replay window,
109
- multi-secret rotation; returns a typed `InvoWebhookEvent` discriminated union
110
- (`purchase.*`, `item.purchased`, `transfer.*`, `payout.status_changed`, `webhook.test`).
111
- Throws `InvoError` (`WEBHOOK_SIGNATURE_INVALID` / `WEBHOOK_TIMESTAMP_EXPIRED` /
112
- `WEBHOOK_MALFORMED` / `WEBHOOK_SECRET_MISSING`). Server-only; the browser bundle
113
- stays crypto-free. Independently security-audited.
114
- - **`getPlayerBalance({ playerEmail | playerId })`** (`/server`): typed `player` / `balances` / `summary`.
115
- - **Automatic retries**: network errors/timeouts, `429` (honoring `retry_after`), and
116
- `5xx` are retried with exponential backoff + jitter. New config `maxRetries`
117
- (default 2, `0` disables) and `retryBaseDelayMs` (default 250).
118
- - **Observability hooks**: optional `onRequest` / `onResponse` / `onError` on both
119
- entries (best-effort/non-throwing); `InvoError.requestId` carries the backend
120
- request id for support/tracing.
121
- - **Typed reads**: `confirmPayment` `ConfirmPaymentResult`; `getOrderDetails` /
122
- `getItemOrderDetails` `OrderDetailsResult`; `getItemPurchaseHistory`
123
- `ItemHistoryResult` (previously untyped `Record`). All keep `raw`.
124
- - **Light validation**: `mintPlayerToken` and `createCheckout` require a non-blank
125
- `playerEmail` (throws `INVALID_INPUT` before the network call).
126
- - **License**: `package.json` `license` is now `SEE LICENSE IN LICENSE` (was
127
- `UNLICENSED`); `LICENSE` rewritten as an explicit install-and-use grant for
128
- building INVO integrations.
129
-
130
- ## [0.2.1] — 2026-06-30
131
-
132
- Docs onlyno code change (republished so the npm page README is current).
133
-
134
- - Rewrote the README into a complete integration/deployment guide: capability
135
- overview, architecture, deployment prerequisites, per-flow sections (currency
136
- purchase, item purchase, sends, transfers, passkeys), webhooks, errors, and a
137
- full API reference for both entries.
138
- - Added INVO console onboarding: `https://console.invo.network` (production) and
139
- `https://dev.console.invo.network` (testing/sandbox), mapped to their API base URLs.
140
-
141
- ## [0.2.0] 2026-06-30
142
-
143
- Adds **item purchase** (spend existing game currency on an in-game item, §4.8) — an
144
- additive, server-only surface.
145
-
146
- - `InvoServer` (`/server`): `purchaseItem`, `getItemPurchaseHistory`, `getItemOrderDetails`.
147
- - Server-side, game-secret auth — no passkey, no real money, no payment rail (it's a
148
- balance debit). Grant the item off the `item.purchased` webhook.
149
- - Client-side guards: required fields (trim-checked), `itemQuantity` integer `1..1000`,
150
- prices `> 0` and `<= 999999.99` (magnitude-safe 2-decimal check), and
151
- `totalPrice == unitPrice × itemQuantity (±0.01)` compared in integer cents.
152
- - Load-bearing response fields (`transaction_id`, `order_id`) throw `INVALID_RESPONSE`
153
- if missing on a 200.
154
- - `InvoError` helpers: `isInsufficientBalance` (gated to 400; not the `429` throttle),
155
- `isDuplicateRequest` (409), `retryAfter` (numeric or string `retry_after`); all
156
- null-safe against non-JSON error bodies.
157
- - Independently audited (2 agents) against handoff doc §4.8/§6/§8; contract verified,
158
- error-classification edge cases fixed.
159
-
160
- ## [0.1.0] — 2026-06-30
161
-
162
- Initial scaffold.
163
-
164
- - `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
165
- `createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
166
- - Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
167
- `purchaseReference`, and `rail:"steam"` rejected before the network call
168
- (`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
169
- - `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
170
- `confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
171
- interchangeable-methods flow (§4.6).
172
- - Optional `refreshToken` hook: transparently re-mints and retries once on
173
- `SDK_TOKEN_EXPIRED` (§11.2).
174
- - Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
175
- - Tests: HTTP layer, server request mapping + purchase guards, browser client
176
- flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
177
- - Contracts extracted + auditor-verified against the INVO backend.
178
-
179
- ### Hardening (independent red-team pass)
180
-
181
- - **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`**
182
- `initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
183
- `guardianApproval` block on the guardian path, so callers don't route into the
184
- PIN UI by mistake (§4.3).
185
- - `usdAmount` validation tightened: rejects non-plain-decimal strings
186
- (`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
187
- - Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
188
- instead of silently surfacing as empty strings.
189
- - `getOrderDetails` requires at least one of `orderId`/`transactionId`.
190
- - Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
191
- (never replays a single-use assertion) and single-flights concurrent refreshes.
192
- - `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
193
- in cleartext.
194
- - 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
+ ## [1.1.0] — 2026-07-01
8
+
9
+ Four additive server-side flows on `InvoServer` (the SMS-PIN / claim-code / status /
10
+ guardian paths partners still need alongside the passkey path).
11
+
12
+ - **SMS-PIN completion** `verifySmsTransfer` / `verifySmsSend` (for `verificationMethod: "sms"`).
13
+ - **Claim-by-code** — `claimTransfer` / `claimCurrency`, incl. the `needsAccountSelection`
14
+ multi-account disambiguation (a `200` with `candidates`, not an error).
15
+ - **Status reads** `getTransferStatus` / `getSendStatus` (surface `verificationState`).
16
+ - **Guardian approval-status poll** — `getGuardianApprovalStatus` (poll a hold to `state`).
17
+ - New typed results (`SmsVerifyResult`, `ClaimResult`, `TransactionStatusResult`,
18
+ `GuardianApprovalStatusResult`) + `InvoError.isPhoneShareApprovalRequired`.
19
+
20
+ ## [1.0.0] 2026-07-01
21
+
22
+ **1.0 — stable.** The public API is now covered by a stability commitment: it follows
23
+ semver and no breaking change ships without a major bump + migration note. This is a
24
+ milestone release, **not a breaking change** code on `0.8.x` continues to work unchanged.
25
+
26
+ Also in this release (docs + DX):
27
+
28
+ - **Integration modes** documented — **browser-direct** (default) and **behind your proxy**
29
+ (keep the INVO player token server-side via a custom `baseUrl`/`fetch`; no CORS or
30
+ security-posture change). Plus a token-bootstrap example and a CORS + RP-ID/origins
31
+ go-live checklist.
32
+ - `linkDevice` called out as the lowest-risk first adoption (additive, no refactor).
33
+
34
+ ## [0.8.0] — 2026-07-01
35
+
36
+ Completes the **discovery layer** for send/transfer UIs.
37
+
38
+ - **`InvoClient.getBalance()`** (browser, player-token, `GET /api/sdk/balance`) — the
39
+ player's balances for the token's game: `balances[]` (`currencyId`, `currencyName`,
40
+ `currencySymbol`/`currencySymbolUrl` (nullable), `availableBalance`/`reservedBalance`/
41
+ `totalBalance` as decimal strings) plus `totalValue` / `currencyCount` / `hasFunds`. A
42
+ player who hasn't transacted here yet returns an empty `balances` array (a `200`, not an
43
+ error); `404 GAME_NOT_FOUND` throws.
44
+ - **`InvoServer.getLinkedIdentities({ playerEmail | playerPhone })`** (server-only,
45
+ game-secret) a player's linked wallet identities (game-scoped). Provide one identifier
46
+ (phone wins if both); no in-game match returns `notFound: true` with empty `emails`
47
+ instead of throwing. **Returns first-party PII never expose to the browser.**
48
+
49
+ ## [0.7.0] 2026-07-01
50
+
51
+ Start of the **discovery layer** for send/transfer UIs.
52
+
53
+ - **`InvoClient.getDestinations({ direction })`** one player-token call
54
+ (`GET /api/sdk/destinations`) returns every game the player can send/transfer to, with
55
+ all display metadata **inline** (game name, icon, currency name/symbol, min/max transfer
56
+ limits) no per-game lookup. Includes source game/currency info and `transferMode`
57
+ (`universal`/`linked` + `linkedGameIds`).
58
+ - _(Coming in 0.7.x: `getBalance` single-game player-token read; `getLinkedIdentities`
59
+ server-side.)_
60
+
61
+ ## [0.6.0] — 2026-07-01
62
+
63
+ Three additive browser flows the dashboard needed, built to the live backend contracts.
64
+
65
+ - **`InvoClient.getPendingCollect()`** the player's own pending-to-collect list
66
+ (player-token `GET /api/sdk/transfers/pending`), PII-free. Each row's `kind`
67
+ (`identity_gate` `approve*`, `receiving_confirm` `confirmReceipt*`) tells you the
68
+ action; typed fields incl. `held` / `holdReason` / `stepUpRequired`.
69
+ - **First-enrollment OTP grant** `enrollmentBegin()` / `enrollmentVerify(code)` for
70
+ `ENROLLMENT_REQUIRES_AUTHORIZATION` (send OTP to phone+email, verify, then retry the
71
+ same `enrollPasskey()` the server grant is auto-consumed). New `InvoError` helpers
72
+ `isEnrollmentAuthorizationRequired` / `isEnrollmentProofRequired`.
73
+ - **Typed approve/confirm holds** — `approve*`/`confirmReceipt*` now surface a typed
74
+ `holdReason` (from `error_code ?? code`) on HTTP 202 holds, plus `risk` /
75
+ `guardianApproval` / `pollEndpoint` on approve. `RECIPIENT_IDENTITY_PENDING` correctly
76
+ surfaces on confirm-receipt. Success carries no `holdReason`.
77
+
78
+ ## [0.5.0] — 2026-07-01
79
+
80
+ Support partner `metadata` end-to-end on the purchase flow (additive, backward-compatible).
81
+
82
+ - **`PurchaseCompletedData.metadata?`** the `purchase.completed` webhook now echoes your
83
+ metadata back (all rails); `event.data.metadata` is typed.
84
+ - **`purchaseCurrency`** now accepts and forwards `metadata` (the direct `/purchase-currency`
85
+ endpoint accepts it too, matching `createCheckout`).
86
+
87
+ ## [0.4.2]2026-06-30
88
+
89
+ Fixes from an independent line-by-line audit against the live backend.
90
+
91
+ - **`linkDevice` now works.** `device/link/webauthn/begin` returns a wrapped
92
+ `{ link_id, options }` body (challenge nested under `options`) and binds the
93
+ challenge to a server-generated `link_id`. The client now unwraps `options` and
94
+ echoes the server's `link_id` to `/complete` (it previously threw on an undefined
95
+ challenge and sent the wrong id).
96
+ - **`InvoError.code` is now populated for the direct purchase rail + guardian flows.**
97
+ `errorFromResponse` reads `error_code` (SecureErrorHandler + `/purchase-currency`)
98
+ in addition to `code`, and promotes known no-code tokens (`flow_paused`,
99
+ `spending_limit_exceeded`, `receiver_not_enrolled_use_claim_code`) to `.code`. Fixes
100
+ `isDuplicateRequest` on the direct rail.
101
+ - **Guardian/minor routing.** The 202 guardian body also carries
102
+ `verification_method:"sms"`; the SDK now suppresses it (reports
103
+ `verificationMethod: undefined`) so a guardian-pending minor isn't routed into the
104
+ SMS-PIN UI. README example reordered to branch on `guardianApproval` first.
105
+
106
+ ## [0.4.1] — 2026-06-30
107
+
108
+ Docs only replaced the README's internal-leaking "Deployment prerequisites"
109
+ (backend flag names, DB columns, gating mechanics) with a partner-facing
110
+ "Before you go live"; republished so the npm page README is current.
111
+
112
+ ## [0.4.0] — 2026-06-30
113
+
114
+ Additive release more server reads, edge-ready webhooks, cancellation, and tooling.
115
+
116
+ - **`getInboundPending({ playerEmail | playerPhone })`** live, unclaimed inbound
117
+ sends/transfers for a player (the source of truth behind the "you have X to collect"
118
+ badge; pairs with `transfer.claim_pending`).
119
+ - **`verifyWebhookAsync`** — Web Crypto variant of `verifyWebhook` that runs on
120
+ Cloudflare Workers / Deno / Vercel+Netlify Edge / Bun / browsers; and
121
+ **`createWebhookHandler`** a zero-dep Fetch-API `(Request) => Promise<Response>`
122
+ webhook route handler (Next.js App Router, Workers, Deno, Hono, Bun).
123
+ - **`iterateItemPurchaseHistory`** async iterator that pages through a player's
124
+ full item-purchase history.
125
+ - **Per-call `AbortSignal`** every method accepts an optional `{ signal }`; an
126
+ aborted call throws `InvoError` code `ABORTED` and is never retried.
127
+ - **Tooling**: ESLint (+ lint in CI), changesets release automation, `SECURITY.md`,
128
+ and `CODEOWNERS`.
129
+
130
+ ## [0.3.0] — 2026-06-30
131
+
132
+ Additive releasenew server capabilities plus transport resilience/observability.
133
+
134
+ - **Webhook verification** (`/server`): `verifyWebhook(rawBody, signatureHeader, secret | secrets, opts?)`
135
+ constant-time HMAC-SHA256 over `${t}.${rawBody}`, 5-minute replay window,
136
+ multi-secret rotation; returns a typed `InvoWebhookEvent` discriminated union
137
+ (`purchase.*`, `item.purchased`, `transfer.*`, `payout.status_changed`, `webhook.test`).
138
+ Throws `InvoError` (`WEBHOOK_SIGNATURE_INVALID` / `WEBHOOK_TIMESTAMP_EXPIRED` /
139
+ `WEBHOOK_MALFORMED` / `WEBHOOK_SECRET_MISSING`). Server-only; the browser bundle
140
+ stays crypto-free. Independently security-audited.
141
+ - **`getPlayerBalance({ playerEmail | playerId })`** (`/server`): typed `player` / `balances` / `summary`.
142
+ - **Automatic retries**: network errors/timeouts, `429` (honoring `retry_after`), and
143
+ `5xx` are retried with exponential backoff + jitter. New config `maxRetries`
144
+ (default 2, `0` disables) and `retryBaseDelayMs` (default 250).
145
+ - **Observability hooks**: optional `onRequest` / `onResponse` / `onError` on both
146
+ entries (best-effort/non-throwing); `InvoError.requestId` carries the backend
147
+ request id for support/tracing.
148
+ - **Typed reads**: `confirmPayment` `ConfirmPaymentResult`; `getOrderDetails` /
149
+ `getItemOrderDetails` `OrderDetailsResult`; `getItemPurchaseHistory`
150
+ `ItemHistoryResult` (previously untyped `Record`). All keep `raw`.
151
+ - **Light validation**: `mintPlayerToken` and `createCheckout` require a non-blank
152
+ `playerEmail` (throws `INVALID_INPUT` before the network call).
153
+ - **License**: `package.json` `license` is now `SEE LICENSE IN LICENSE` (was
154
+ `UNLICENSED`); `LICENSE` rewritten as an explicit install-and-use grant for
155
+ building INVO integrations.
156
+
157
+ ## [0.2.1] 2026-06-30
158
+
159
+ Docs only — no code change (republished so the npm page README is current).
160
+
161
+ - Rewrote the README into a complete integration/deployment guide: capability
162
+ overview, architecture, deployment prerequisites, per-flow sections (currency
163
+ purchase, item purchase, sends, transfers, passkeys), webhooks, errors, and a
164
+ full API reference for both entries.
165
+ - Added INVO console onboarding: `https://console.invo.network` (production) and
166
+ `https://dev.console.invo.network` (testing/sandbox), mapped to their API base URLs.
167
+
168
+ ## [0.2.0] 2026-06-30
169
+
170
+ Adds **item purchase** (spend existing game currency on an in-game item, §4.8) — an
171
+ additive, server-only surface.
172
+
173
+ - `InvoServer` (`/server`): `purchaseItem`, `getItemPurchaseHistory`, `getItemOrderDetails`.
174
+ - Server-side, game-secret auth no passkey, no real money, no payment rail (it's a
175
+ balance debit). Grant the item off the `item.purchased` webhook.
176
+ - Client-side guards: required fields (trim-checked), `itemQuantity` integer `1..1000`,
177
+ prices `> 0` and `<= 999999.99` (magnitude-safe 2-decimal check), and
178
+ `totalPrice == unitPrice × itemQuantity (±0.01)` compared in integer cents.
179
+ - Load-bearing response fields (`transaction_id`, `order_id`) throw `INVALID_RESPONSE`
180
+ if missing on a 200.
181
+ - `InvoError` helpers: `isInsufficientBalance` (gated to 400; not the `429` throttle),
182
+ `isDuplicateRequest` (409), `retryAfter` (numeric or string `retry_after`); all
183
+ null-safe against non-JSON error bodies.
184
+ - Independently audited (2 agents) against handoff doc §4.8/§6/§8; contract verified,
185
+ error-classification edge cases fixed.
186
+
187
+ ## [0.1.0] 2026-06-30
188
+
189
+ Initial scaffold.
190
+
191
+ - `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
192
+ `createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
193
+ - Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
194
+ `purchaseReference`, and `rail:"steam"` rejected before the network call
195
+ (`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
196
+ - `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
197
+ `confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
198
+ interchangeable-methods flow (§4.6).
199
+ - Optional `refreshToken` hook: transparently re-mints and retries once on
200
+ `SDK_TOKEN_EXPIRED` (§11.2).
201
+ - Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
202
+ - Tests: HTTP layer, server request mapping + purchase guards, browser client
203
+ flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
204
+ - Contracts extracted + auditor-verified against the INVO backend.
205
+
206
+ ### Hardening (independent red-team pass)
207
+
208
+ - **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
209
+ `initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
210
+ `guardianApproval` block on the guardian path, so callers don't route into the
211
+ PIN UI by mistake (§4.3).
212
+ - `usdAmount` validation tightened: rejects non-plain-decimal strings
213
+ (`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
214
+ - Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
215
+ instead of silently surfacing as empty strings.
216
+ - `getOrderDetails` requires at least one of `orderId`/`transactionId`.
217
+ - Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
218
+ (never replays a single-use assertion) and single-flights concurrent refreshes.
219
+ - `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
220
+ in cleartext.
221
+ - 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.8.0`, published on npm. The backend it wraps is **live** on sandbox + production, so you can build and test against sandbox today.
5
+ > **Status:** `v1.0.0` — stable, published on npm. The backend it wraps is **live** on sandbox + production. The API is now covered by a [stability commitment](#stability): no breaking changes without a major bump.
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
@@ -78,12 +78,54 @@ Build and test against the **dev console + sandbox** first, then switch to the *
78
78
 
79
79
  **Never import `/server` into browser code** — it carries the game secret. The two entries are built separately for exactly this reason.
80
80
 
81
+ ## Integration modes
82
+
83
+ `InvoClient` needs a `token` and a `baseUrl` — which lets you adopt it **two ways**. Same SDK, same code in your components; only the config differs.
84
+
85
+ ### Token bootstrap (both modes)
86
+
87
+ Expose one small backend endpoint that calls `mintPlayerToken` (server-side, with the game secret) and hand the token to the browser:
88
+
89
+ ```ts
90
+ // browser
91
+ const client = new InvoClient({
92
+ baseUrl: "https://api.invo.network",
93
+ token: await fetch("/your/sdk-token").then((r) => r.json()).then((j) => j.token),
94
+ // Called automatically on SDK_TOKEN_EXPIRED (~15 min TTL) — just re-fetch from your backend:
95
+ refreshToken: () => fetch("/your/sdk-token").then((r) => r.json()).then((j) => j.token),
96
+ });
97
+ ```
98
+
99
+ ### Mode A — browser-direct (default)
100
+
101
+ The browser holds the short-lived, game-scoped player token and calls INVO directly (`baseUrl` = the INVO API). Fewer moving parts. Requires INVO to **CORS-allow your web origin** and to have your **RP ID / origins** set up for passkeys (see [Before you go live](#before-you-go-live)).
102
+
103
+ ### Mode B — behind your proxy (keep the token server-side)
104
+
105
+ If your architecture requires the INVO player token to **never reach the browser**, route the SDK through your own backend — **no CORS setup needed, and no security-posture change**:
106
+
107
+ - Set `baseUrl` to your proxy (e.g. `https://yourgame.com/invo`), which forwards to the INVO API and **injects the real `Authorization` header server-side**.
108
+ - Give the browser a short-lived **session/CSRF token** (not the INVO token) as `token`; your proxy validates it and swaps in the real player token. Or use cookie auth with a custom `fetch`:
109
+
110
+ ```ts
111
+ const client = new InvoClient({
112
+ baseUrl: "https://yourgame.com/invo", // your proxy → INVO
113
+ token: sessionToken, // your token; the proxy swaps in the real one
114
+ fetch: (url, init) => fetch(url, { ...init, credentials: "include" }), // send your cookie
115
+ });
116
+ ```
117
+
118
+ The WebAuthn ceremony still runs in the browser (it must — it's `navigator.credentials`), but transport/auth stays behind your proxy. You keep the SDK's ceremony handling, token-refresh, typed holds, and error classifiers either way.
119
+
120
+ > **Lowest-risk first step:** adopt just `linkDevice` (passkey ↔ app device interchange) — it's purely additive and needs no refactor. See [Passkeys](#passkeys-enroll-approve-link).
121
+
81
122
  ## Before you go live
82
123
 
83
124
  INVO enables each flow for your tenant in the [console](#get-your-account--game-secret-invo-console). What you need to do:
84
125
 
85
126
  - **Store the game secret server-side** and expose a small endpoint that calls `mintPlayerToken` so the browser can fetch/refresh its token. Never ship the secret to the browser.
86
- - **For passkeys (sends/transfers):** give INVO the **web origin(s)** you'll serve from. Passkeys only validate on approved origins — if a call returns `WEBAUTHN_NOT_ENABLED_FOR_TENANT`, your origins aren't set up yet; contact INVO. (Until enrolled, the sender falls back to the SMS-PIN path automatically.)
127
+ - **If you use browser-direct ([Mode A](#mode-a--browser-direct-default)):** ask INVO to **CORS-allow your web origin(s)** so the browser can call the API directly. (Not needed for [Mode B / proxy](#mode-b--behind-your-proxy-keep-the-token-server-side) those requests are same-origin to your backend.)
128
+ - **For passkeys (sends/transfers):** give INVO the **RP ID + web origin(s)** you'll serve from. Passkeys only validate on approved origins — if a call returns `WEBAUTHN_NOT_ENABLED_FOR_TENANT`, your origins aren't set up yet; contact INVO. (Until enrolled, the sender falls back to the SMS-PIN path automatically.)
87
129
  - **For currency purchase:** card checkout works out of the box; ask INVO to enable the `game`/`steam` rails if you need them.
88
130
  - **For item purchase:** nothing extra — it's a currency-balance debit.
89
131
 
@@ -137,7 +179,7 @@ const { checkoutUrl, sessionId, expiresAt } = await server.createCheckout({
137
179
  rail: "platform", // optional: "platform" (default) | "game" | "steam"
138
180
  successUrl: "https://you/buy/ok",
139
181
  cancelUrl: "https://you/buy/cancel",
140
- 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
141
183
  });
142
184
  // → send the browser to checkoutUrl (single-use, ~15 min)
143
185
  ```
@@ -183,7 +225,7 @@ const purchase = await server.purchaseCurrency({
183
225
  purchaseReference: crypto.randomUUID(), // idempotency key, required
184
226
  rail: "platform",
185
227
  paymentMethodId: "pm_...", // a tokenized payment method
186
- 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
187
229
  });
188
230
  // purchase.status:
189
231
  // "success" → captured, purchase.newBalance updated
@@ -402,7 +444,7 @@ export const POST = createWebhookHandler({
402
444
 
403
445
  | Event | Fires for | Use it to |
404
446
  |---|---|---|
405
- | `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`). |
406
448
  | `purchase.failed` / `purchase.disputed` | `platform` rail only | handle failures/disputes |
407
449
  | `purchase.refunded` | `game` / `steam` rails | handle refunds |
408
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`) |
@@ -488,6 +530,10 @@ try {
488
530
  | `getPlayerBalance({ playerEmail? \| playerId? })` | `{ player, balances, summary, raw }` |
489
531
  | `getInboundPending({ playerEmail? \| playerPhone? })` | `{ inboundPending, raw }` — live unclaimed inbound sends/transfers |
490
532
  | `getLinkedIdentities({ playerEmail? \| playerPhone? })` | `{ walletUserId, primaryEmail, primaryPhone, isMinor, emails, notFound, raw }` — **server-only** (returns PII) |
533
+ | `verifySmsTransfer(txnId, smsPin)` / `verifySmsSend(txnId, smsPin)` | `SmsVerifyResult` — complete the SMS-PIN path when `verificationMethod: "sms"` |
534
+ | `claimTransfer(input)` / `claimCurrency(input)` | `ClaimResult` — redeem a claim code (`needsAccountSelection` + `candidates` on a multi-account phone) |
535
+ | `getTransferStatus(txnId)` / `getSendStatus(txnId)` | `TransactionStatusResult` — poll outbound state (`verificationState`) |
536
+ | `getGuardianApprovalStatus(txnId)` | `GuardianApprovalStatusResult` — poll a guardian hold to resolution (`state`) |
491
537
  | `iterateItemPurchaseHistory({ playerEmail, pageSize? })` | async iterator over all history rows |
492
538
  | `verifyWebhook(rawBody, signatureHeader, secret \| secrets, opts?)` | typed `InvoWebhookEvent` (throws on bad signature) |
493
539
  | `verifyWebhookAsync(...)` | same as `verifyWebhook`, Web Crypto (edge/Workers/Deno/Bun) |
@@ -520,7 +566,11 @@ npm run typecheck # tsc --noEmit
520
566
  npm test # vitest
521
567
  ```
522
568
 
523
- 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`).
569
+ ### Stability
570
+
571
+ As of **`1.0.0`** the public API is **stable**: it follows [semver](https://semver.org/), and **no breaking change ships without a major version bump + a migration note**. Patch = fixes, minor = additive surface (new methods/fields, backward-compatible), major = breaking changes (rare). The wire contract is the same live INVO API you'd call directly, and it's backward-compatible within a major — so a pinned SDK keeps working. Safe to depend on in production; pin a version and watch [releases](https://github.com/Invo-Technologies/invo-web-sdk/releases) for security updates.
572
+
573
+ 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`).
524
574
 
525
575
  ## License
526
576
 
@@ -36,6 +36,10 @@ 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
+ }
39
43
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
40
44
  get isDuplicateRequest() {
41
45
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
@@ -257,5 +261,5 @@ function retryAfterMs(parsed, headers) {
257
261
  }
258
262
 
259
263
  export { Http, InvoError, assertSecureBaseUrl };
260
- //# sourceMappingURL=chunk-D3XBTH4C.js.map
261
- //# sourceMappingURL=chunk-D3XBTH4C.js.map
264
+ //# sourceMappingURL=chunk-ZVKWDXSL.js.map
265
+ //# sourceMappingURL=chunk-ZVKWDXSL.js.map
package/dist/index.cjs CHANGED
@@ -38,6 +38,10 @@ 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
+ }
41
45
  /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
42
46
  get isDuplicateRequest() {
43
47
  return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
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-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';
3
3
 
4
4
  declare class InvoClient {
5
5
  private readonly http;