@invonetwork/web-sdk 0.1.0 → 0.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 +58 -39
- package/README.md +219 -142
- package/dist/{chunk-KUQVVH2P.js → chunk-A44O4KC3.js} +28 -2
- package/dist/{errors-B7rVID2r.d.cts → errors-DV5QsftP.d.cts} +56 -1
- package/dist/{errors-B7rVID2r.d.ts → errors-DV5QsftP.d.ts} +56 -1
- package/dist/index.cjs +26 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server.cjs +142 -12
- package/dist/server.d.cts +17 -3
- package/dist/server.d.ts +17 -3
- package/dist/server.js +118 -14
- package/package.json +68 -68
package/CHANGELOG.md
CHANGED
|
@@ -1,39 +1,58 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
|
|
4
|
-
[Semantic Versioning](https://semver.org/).
|
|
5
|
-
|
|
6
|
-
## [0.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- `
|
|
38
|
-
|
|
39
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@invonetwork/web-sdk` are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.2.0] — 2026-06-30
|
|
7
|
+
|
|
8
|
+
Adds **item purchase** (spend existing game currency on an in-game item, §4.8) — an
|
|
9
|
+
additive, server-only surface.
|
|
10
|
+
|
|
11
|
+
- `InvoServer` (`/server`): `purchaseItem`, `getItemPurchaseHistory`, `getItemOrderDetails`.
|
|
12
|
+
- Server-side, game-secret auth — no passkey, no real money, no payment rail (it's a
|
|
13
|
+
balance debit). Grant the item off the `item.purchased` webhook.
|
|
14
|
+
- Client-side guards: required fields (trim-checked), `itemQuantity` integer `1..1000`,
|
|
15
|
+
prices `> 0` and `<= 999999.99` (magnitude-safe 2-decimal check), and
|
|
16
|
+
`totalPrice == unitPrice × itemQuantity (±0.01)` compared in integer cents.
|
|
17
|
+
- Load-bearing response fields (`transaction_id`, `order_id`) throw `INVALID_RESPONSE`
|
|
18
|
+
if missing on a 200.
|
|
19
|
+
- `InvoError` helpers: `isInsufficientBalance` (gated to 400; not the `429` throttle),
|
|
20
|
+
`isDuplicateRequest` (409), `retryAfter` (numeric or string `retry_after`); all
|
|
21
|
+
null-safe against non-JSON error bodies.
|
|
22
|
+
- Independently audited (2 agents) against handoff doc §4.8/§6/§8; contract verified,
|
|
23
|
+
error-classification edge cases fixed.
|
|
24
|
+
|
|
25
|
+
## [0.1.0] — 2026-06-30
|
|
26
|
+
|
|
27
|
+
Initial scaffold.
|
|
28
|
+
|
|
29
|
+
- `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
|
|
30
|
+
`createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
|
|
31
|
+
- Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
|
|
32
|
+
`purchaseReference`, and `rail:"steam"` rejected before the network call
|
|
33
|
+
(`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
|
|
34
|
+
- `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
|
|
35
|
+
`confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
|
|
36
|
+
interchangeable-methods flow (§4.6).
|
|
37
|
+
- Optional `refreshToken` hook: transparently re-mints and retries once on
|
|
38
|
+
`SDK_TOKEN_EXPIRED` (§11.2).
|
|
39
|
+
- Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
|
|
40
|
+
- Tests: HTTP layer, server request mapping + purchase guards, browser client
|
|
41
|
+
flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
|
|
42
|
+
- Contracts extracted + auditor-verified against the INVO backend.
|
|
43
|
+
|
|
44
|
+
### Hardening (independent red-team pass)
|
|
45
|
+
- **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
|
|
46
|
+
`initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
|
|
47
|
+
`guardianApproval` block on the guardian path, so callers don't route into the
|
|
48
|
+
PIN UI by mistake (§4.3).
|
|
49
|
+
- `usdAmount` validation tightened: rejects non-plain-decimal strings
|
|
50
|
+
(`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
|
|
51
|
+
- Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
|
|
52
|
+
instead of silently surfacing as empty strings.
|
|
53
|
+
- `getOrderDetails` requires at least one of `orderId`/`transactionId`.
|
|
54
|
+
- Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
|
|
55
|
+
(never replays a single-use assertion) and single-flights concurrent refreshes.
|
|
56
|
+
- `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
|
|
57
|
+
in cleartext.
|
|
58
|
+
- Published tarball excludes sourcemaps (no proprietary source shipped).
|
package/README.md
CHANGED
|
@@ -1,142 +1,219 @@
|
|
|
1
|
-
# @invonetwork/web-sdk
|
|
2
|
-
|
|
3
|
-
INVO Web SDK for partner **web** platforms — **currency purchase** + **passkey (WebAuthn)** verification for cross-game **sends/transfers**. The web analog of the Unity/UE plugins.
|
|
4
|
-
|
|
5
|
-
> **Status:** v0.1.0. The backend this wraps is **live** — the INVO-hosted checkout page, sessions, rails, and webhooks are deployed on sandbox + production, so you can build and test against sandbox today. Full, current API reference + flow docs: **https://docs.invo.network/docs/currency-purchase** and **https://docs.invo.network/docs/game-developer-integration**.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @invonetwork/web-sdk
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
> Published under the INVO-owned `@invonetwork` npm org. (Until the first `npm publish`, develop with `npm link` or a local install.)
|
|
14
|
-
|
|
15
|
-
## Two entry points (the game secret never reaches the browser)
|
|
16
|
-
|
|
17
|
-
| Import | Runs | Holds | Does |
|
|
18
|
-
|---|---|---|---|
|
|
19
|
-
| `@invonetwork/web-sdk/server` | your server (Node ≥18) | the **game secret** | mint player token, initiate send/transfer, currency purchase |
|
|
20
|
-
| `@invonetwork/web-sdk` | the browser | a short-lived **player token** | enroll passkey, approve, self-claim |
|
|
21
|
-
|
|
22
|
-
## Base URLs
|
|
23
|
-
|
|
24
|
-
- Production: `https://invo.network`
|
|
25
|
-
- Sandbox: `https://sandbox.invo.network/sandbox`
|
|
26
|
-
|
|
27
|
-
`baseUrl` must be `https://` (the game secret and player token travel in request headers); `http://localhost` is allowed for local development only.
|
|
28
|
-
|
|
29
|
-
## Server (Node)
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
import { InvoServer } from "@invonetwork/web-sdk/server";
|
|
33
|
-
|
|
34
|
-
const invo = new InvoServer({
|
|
35
|
-
gameSecret: process.env.INVO_GAME_SECRET!, // server-side only
|
|
36
|
-
baseUrl: "https://sandbox.invo.network/sandbox",
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// 1. mint a short-lived token for the browser
|
|
40
|
-
const { token } = await invo.mintPlayerToken({ playerEmail: "p@example.com" });
|
|
41
|
-
|
|
42
|
-
// 2a. buy currency — hosted checkout (recommended; INVO's page handles the processor)
|
|
43
|
-
const { checkoutUrl } = await invo.createCheckout({
|
|
44
|
-
playerEmail: "p@example.com",
|
|
45
|
-
usdAmount: "20.00",
|
|
46
|
-
rail: "platform", // optional: "platform" (card, default) | "game" | "steam"
|
|
47
|
-
metadata: { yourOrderId: "ord_42" }, // echoed back on the purchase.completed webhook
|
|
48
|
-
});
|
|
49
|
-
// → open checkoutUrl in a WebView/redirect or an iframe (see "Consuming the checkout")
|
|
50
|
-
|
|
51
|
-
// 2b. or initiate a send; inspect verificationMethod
|
|
52
|
-
const send = await invo.initiateSend({
|
|
53
|
-
clientRequestId: crypto.randomUUID(),
|
|
54
|
-
senderPlayerName: "P", senderPlayerEmail: "p@example.com", senderPlayerPhone: "+15555550100",
|
|
55
|
-
receiverPlayerEmail: "q@example.com", receiverPlayerPhone: "+15555550111",
|
|
56
|
-
receivingGameId: 123456, amount: "50",
|
|
57
|
-
});
|
|
58
|
-
if (send.verificationMethod === "in_app") {
|
|
59
|
-
// hand send.transactionId + the player token to the browser to approve via passkey
|
|
60
|
-
} else if (send.verificationMethod === "sms") {
|
|
61
|
-
// sender not enrolled — fall back to the SMS-PIN UI
|
|
62
|
-
} else if (send.guardianApproval) {
|
|
63
|
-
// minor/guardian path (HTTP 202): pending guardian approval — do NOT show the PIN UI
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Browser
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
import { InvoClient } from "@invonetwork/web-sdk";
|
|
71
|
-
|
|
72
|
-
const invo = new InvoClient({
|
|
73
|
-
token,
|
|
74
|
-
baseUrl: "https://sandbox.invo.network/sandbox",
|
|
75
|
-
// Optional: player tokens live ~15 min. If a call fails with SDK_TOKEN_EXPIRED,
|
|
76
|
-
// the SDK calls this once for a fresh token (re-minted by your backend) and
|
|
77
|
-
// retries the request transparently.
|
|
78
|
-
refreshToken: () => fetch("/invo/token").then((r) => r.json()).then((j) => j.token),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
await invo.enrollPasskey(); // once per user
|
|
82
|
-
await invo.approveSend(transactionId); // or approveTransfer(...)
|
|
83
|
-
await invo.confirmReceiptSend(transactionId); // recipient self-claim
|
|
84
|
-
|
|
85
|
-
// Interchangeable methods (optional): prove an already-enrolled method (e.g. the
|
|
86
|
-
// INVO app device key) to authorize adding this passkey, then enroll it.
|
|
87
|
-
await invo.linkDevice(linkId); // → { status: "authorized" }
|
|
88
|
-
await invo.enrollPasskey();
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Currency purchase has **no browser SDK method** — the server mints `checkoutUrl`; the browser just opens it.
|
|
92
|
-
|
|
93
|
-
## Consuming the checkout (browser)
|
|
94
|
-
|
|
95
|
-
`createCheckout` returns a `checkoutUrl` (15-min, single-use). Open it either way:
|
|
96
|
-
|
|
97
|
-
- **WebView / full-page redirect** (works everywhere, no setup): on success the page redirects to your `successUrl`.
|
|
98
|
-
- **Embedded `<iframe>`** — **works by default** (any https origin may frame it; no allow-listing): the page does *not* redirect your top window — listen for the `INVO_CHECKOUT_COMPLETE` postMessage.
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
const iframe = document.createElement("iframe");
|
|
102
|
-
iframe.src = checkoutUrl;
|
|
103
|
-
iframe.style.cssText = "width:440px;height:720px;border:0";
|
|
104
|
-
document.body.appendChild(iframe);
|
|
105
|
-
|
|
106
|
-
window.addEventListener("message", (e) => {
|
|
107
|
-
if (e.origin !== "https://invo.network") return; // sandbox: "https://sandbox.invo.network"
|
|
108
|
-
if (e.data?.type === "INVO_CHECKOUT_COMPLETE") {
|
|
109
|
-
// UX hint only (unsigned). data = { status:'success', new_balance, currency_name, transaction_id }
|
|
110
|
-
refreshBalanceOptimistically(e.data.data.new_balance);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
The hosted page handles **card entry, saved cards, and 3-D Secure** (including a top-level break-out when embedded). You build no payment UI and never touch card data.
|
|
116
|
-
|
|
117
|
-
**Completion is authoritative via the `purchase.completed` webhook** (HMAC-signed). **Dedupe on `X-Invo-Idempotency-Key`** (stable across retries/replays — *not* `X-Invo-Event-Id`, which changes per delivery). Treat `INVO_CHECKOUT_COMPLETE` as a UX hint; grant currency off the webhook (or re-read the balance).
|
|
118
|
-
|
|
119
|
-
## Payment rails (neutral names)
|
|
120
|
-
|
|
121
|
-
The `rail` passed to `createCheckout` selects the in-page experience — all branded as INVO, no visible redirect:
|
|
122
|
-
- `"platform"` (default) — card checkout on the hosted page.
|
|
123
|
-
- `"game"` — regional / game-store methods, embedded in-page.
|
|
124
|
-
- `"steam"` — Steam titles use the dedicated Steam flow; a `steam` session only shows an in-client hand-off, it doesn't drive the purchase.
|
|
125
|
-
|
|
126
|
-
Provider/processor names are an internal backend detail and never appear in the API. (`purchaseCurrency()` on the server is an advanced direct path for partners who tokenize cards themselves — most integrations should use `createCheckout`.)
|
|
127
|
-
|
|
128
|
-
##
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1
|
+
# @invonetwork/web-sdk
|
|
2
|
+
|
|
3
|
+
INVO Web SDK for partner **web** platforms — **currency purchase** + **passkey (WebAuthn)** verification for cross-game **sends/transfers**. The web analog of the Unity/UE plugins.
|
|
4
|
+
|
|
5
|
+
> **Status:** v0.1.0. The backend this wraps is **live** — the INVO-hosted checkout page, sessions, rails, and webhooks are deployed on sandbox + production, so you can build and test against sandbox today. Full, current API reference + flow docs: **https://docs.invo.network/docs/currency-purchase** and **https://docs.invo.network/docs/game-developer-integration**.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @invonetwork/web-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
> Published under the INVO-owned `@invonetwork` npm org. (Until the first `npm publish`, develop with `npm link` or a local install.)
|
|
14
|
+
|
|
15
|
+
## Two entry points (the game secret never reaches the browser)
|
|
16
|
+
|
|
17
|
+
| Import | Runs | Holds | Does |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| `@invonetwork/web-sdk/server` | your server (Node ≥18) | the **game secret** | mint player token, initiate send/transfer, currency purchase, item purchase |
|
|
20
|
+
| `@invonetwork/web-sdk` | the browser | a short-lived **player token** | enroll passkey, approve, self-claim |
|
|
21
|
+
|
|
22
|
+
## Base URLs
|
|
23
|
+
|
|
24
|
+
- Production: `https://invo.network`
|
|
25
|
+
- Sandbox: `https://sandbox.invo.network/sandbox`
|
|
26
|
+
|
|
27
|
+
`baseUrl` must be `https://` (the game secret and player token travel in request headers); `http://localhost` is allowed for local development only.
|
|
28
|
+
|
|
29
|
+
## Server (Node)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { InvoServer } from "@invonetwork/web-sdk/server";
|
|
33
|
+
|
|
34
|
+
const invo = new InvoServer({
|
|
35
|
+
gameSecret: process.env.INVO_GAME_SECRET!, // server-side only
|
|
36
|
+
baseUrl: "https://sandbox.invo.network/sandbox",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// 1. mint a short-lived token for the browser
|
|
40
|
+
const { token } = await invo.mintPlayerToken({ playerEmail: "p@example.com" });
|
|
41
|
+
|
|
42
|
+
// 2a. buy currency — hosted checkout (recommended; INVO's page handles the processor)
|
|
43
|
+
const { checkoutUrl } = await invo.createCheckout({
|
|
44
|
+
playerEmail: "p@example.com",
|
|
45
|
+
usdAmount: "20.00",
|
|
46
|
+
rail: "platform", // optional: "platform" (card, default) | "game" | "steam"
|
|
47
|
+
metadata: { yourOrderId: "ord_42" }, // echoed back on the purchase.completed webhook
|
|
48
|
+
});
|
|
49
|
+
// → open checkoutUrl in a WebView/redirect or an iframe (see "Consuming the checkout")
|
|
50
|
+
|
|
51
|
+
// 2b. or initiate a send; inspect verificationMethod
|
|
52
|
+
const send = await invo.initiateSend({
|
|
53
|
+
clientRequestId: crypto.randomUUID(),
|
|
54
|
+
senderPlayerName: "P", senderPlayerEmail: "p@example.com", senderPlayerPhone: "+15555550100",
|
|
55
|
+
receiverPlayerEmail: "q@example.com", receiverPlayerPhone: "+15555550111",
|
|
56
|
+
receivingGameId: 123456, amount: "50",
|
|
57
|
+
});
|
|
58
|
+
if (send.verificationMethod === "in_app") {
|
|
59
|
+
// hand send.transactionId + the player token to the browser to approve via passkey
|
|
60
|
+
} else if (send.verificationMethod === "sms") {
|
|
61
|
+
// sender not enrolled — fall back to the SMS-PIN UI
|
|
62
|
+
} else if (send.guardianApproval) {
|
|
63
|
+
// minor/guardian path (HTTP 202): pending guardian approval — do NOT show the PIN UI
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Browser
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { InvoClient } from "@invonetwork/web-sdk";
|
|
71
|
+
|
|
72
|
+
const invo = new InvoClient({
|
|
73
|
+
token,
|
|
74
|
+
baseUrl: "https://sandbox.invo.network/sandbox",
|
|
75
|
+
// Optional: player tokens live ~15 min. If a call fails with SDK_TOKEN_EXPIRED,
|
|
76
|
+
// the SDK calls this once for a fresh token (re-minted by your backend) and
|
|
77
|
+
// retries the request transparently.
|
|
78
|
+
refreshToken: () => fetch("/invo/token").then((r) => r.json()).then((j) => j.token),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await invo.enrollPasskey(); // once per user
|
|
82
|
+
await invo.approveSend(transactionId); // or approveTransfer(...)
|
|
83
|
+
await invo.confirmReceiptSend(transactionId); // recipient self-claim
|
|
84
|
+
|
|
85
|
+
// Interchangeable methods (optional): prove an already-enrolled method (e.g. the
|
|
86
|
+
// INVO app device key) to authorize adding this passkey, then enroll it.
|
|
87
|
+
await invo.linkDevice(linkId); // → { status: "authorized" }
|
|
88
|
+
await invo.enrollPasskey();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Currency purchase has **no browser SDK method** — the server mints `checkoutUrl`; the browser just opens it.
|
|
92
|
+
|
|
93
|
+
## Consuming the checkout (browser)
|
|
94
|
+
|
|
95
|
+
`createCheckout` returns a `checkoutUrl` (15-min, single-use). Open it either way:
|
|
96
|
+
|
|
97
|
+
- **WebView / full-page redirect** (works everywhere, no setup): on success the page redirects to your `successUrl`.
|
|
98
|
+
- **Embedded `<iframe>`** — **works by default** (any https origin may frame it; no allow-listing): the page does *not* redirect your top window — listen for the `INVO_CHECKOUT_COMPLETE` postMessage.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const iframe = document.createElement("iframe");
|
|
102
|
+
iframe.src = checkoutUrl;
|
|
103
|
+
iframe.style.cssText = "width:440px;height:720px;border:0";
|
|
104
|
+
document.body.appendChild(iframe);
|
|
105
|
+
|
|
106
|
+
window.addEventListener("message", (e) => {
|
|
107
|
+
if (e.origin !== "https://invo.network") return; // sandbox: "https://sandbox.invo.network"
|
|
108
|
+
if (e.data?.type === "INVO_CHECKOUT_COMPLETE") {
|
|
109
|
+
// UX hint only (unsigned). data = { status:'success', new_balance, currency_name, transaction_id }
|
|
110
|
+
refreshBalanceOptimistically(e.data.data.new_balance);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The hosted page handles **card entry, saved cards, and 3-D Secure** (including a top-level break-out when embedded). You build no payment UI and never touch card data.
|
|
116
|
+
|
|
117
|
+
**Completion is authoritative via the `purchase.completed` webhook** (HMAC-signed). **Dedupe on `X-Invo-Idempotency-Key`** (stable across retries/replays — *not* `X-Invo-Event-Id`, which changes per delivery). Treat `INVO_CHECKOUT_COMPLETE` as a UX hint; grant currency off the webhook (or re-read the balance).
|
|
118
|
+
|
|
119
|
+
## Payment rails (neutral names)
|
|
120
|
+
|
|
121
|
+
The `rail` passed to `createCheckout` selects the in-page experience — all branded as INVO, no visible redirect:
|
|
122
|
+
- `"platform"` (default) — card checkout on the hosted page.
|
|
123
|
+
- `"game"` — regional / game-store methods, embedded in-page.
|
|
124
|
+
- `"steam"` — Steam titles use the dedicated Steam flow; a `steam` session only shows an in-client hand-off, it doesn't drive the purchase.
|
|
125
|
+
|
|
126
|
+
Provider/processor names are an internal backend detail and never appear in the API. (`purchaseCurrency()` on the server is an advanced direct path for partners who tokenize cards themselves — most integrations should use `createCheckout`.)
|
|
127
|
+
|
|
128
|
+
## Sends & transfers (move existing game currency, no real money)
|
|
129
|
+
|
|
130
|
+
Distinct from *purchase* (which brings real money in), **sends** and **transfers** move
|
|
131
|
+
already-owned game currency from one player to another and are authorized by the
|
|
132
|
+
sender's **passkey** (or an SMS PIN if they aren't enrolled). The two are parallel
|
|
133
|
+
flows with the same shape:
|
|
134
|
+
|
|
135
|
+
| | Send | Transfer |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| Initiate (server) | `initiateSend` → `/api/currency-sends/initiate-send` | `initiateTransfer` → `/api/transfers/initiate-transfer` |
|
|
138
|
+
| Parties | `sender*` / `receiver*`, `receivingGameId` | `source*` / `target*`, `targetGameId` |
|
|
139
|
+
| Approve (browser) | `approveSend` | `approveTransfer` (also returns the sender's **claim code**) |
|
|
140
|
+
| Recipient claim (browser) | `confirmReceiptSend` | `confirmReceiptTransfer` |
|
|
141
|
+
|
|
142
|
+
### End-to-end (transfer shown; send is identical with `*Send` methods)
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// 1. SERVER — initiate, then branch on how the sender must verify
|
|
146
|
+
const t = await invo.initiateTransfer({
|
|
147
|
+
clientRequestId: crypto.randomUUID(),
|
|
148
|
+
sourcePlayerName: "P", sourcePlayerEmail: "p@example.com", sourcePlayerPhone: "+15555550100",
|
|
149
|
+
targetPlayerEmail: "q@example.com", targetPlayerPhone: "+15555550111",
|
|
150
|
+
targetGameId: 123456, amount: "50",
|
|
151
|
+
});
|
|
152
|
+
// t.verificationMethod: "in_app" (passkey) | "sms" (PIN fallback) | undefined (+ t.guardianApproval on the minor/guardian path)
|
|
153
|
+
|
|
154
|
+
// 2. BROWSER — the sender approves with their passkey (hand them t.transactionId + the player token)
|
|
155
|
+
const approved = await invo.approveTransfer(t.transactionId);
|
|
156
|
+
// approved.claimCode + approved.claimCodeExpiresAt — deliver to the recipient if they can't self-claim
|
|
157
|
+
|
|
158
|
+
// 3. BROWSER — the recipient claims. If they have a passkey, self-claim; else fall back to the claim code.
|
|
159
|
+
try {
|
|
160
|
+
await invo.confirmReceiptTransfer(t.transactionId);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (e instanceof InvoError && e.isReceiverNotEnrolled) {
|
|
163
|
+
// recipient has no passkey here → show a claim-code entry UI using approved.claimCode
|
|
164
|
+
} else throw e;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
- **`verificationMethod`** comes from initiate: `"in_app"` → passkey approve; `"sms"` → the sender wasn't enrolled and a PIN was sent (build a PIN-entry fallback); `undefined` with `guardianApproval` → minor/guardian path (do **not** show the PIN UI).
|
|
169
|
+
- **Claim codes** are only returned by `approveTransfer` (transfer flow). They're the out-of-band fallback when the recipient isn't passkey-enrolled.
|
|
170
|
+
- **`isReceiverNotEnrolled`** on the `confirmReceipt*` error is the explicit signal to switch to claim-code entry.
|
|
171
|
+
- Transfer self-claim additionally requires the tenant's `SDK_TRANSFER_CONFIRM_RECEIPT_ENABLED` flag; if it's off, surface the claim-code path instead.
|
|
172
|
+
|
|
173
|
+
## Item purchase (spend game currency on an in-game item)
|
|
174
|
+
|
|
175
|
+
Distinct from *currency purchase* (real money **in**): an **item purchase spends the
|
|
176
|
+
currency a player already owns** to buy an in-game item. It's a balance debit — **no
|
|
177
|
+
real money, no payment rail, no passkey** — authenticated server-side by the game
|
|
178
|
+
secret, so it lives on the **`/server`** entry. Amounts are in **game-currency units**
|
|
179
|
+
(not USD).
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const item = await invo.purchaseItem({
|
|
183
|
+
clientRequestId: crypto.randomUUID(), // idempotency key, unique per game
|
|
184
|
+
playerEmail: "p@example.com",
|
|
185
|
+
playerName: "P",
|
|
186
|
+
itemId: "sword_001",
|
|
187
|
+
itemName: "Legendary Sword",
|
|
188
|
+
itemQuantity: 1,
|
|
189
|
+
unitPrice: "100.00",
|
|
190
|
+
totalPrice: "100.00", // must equal unitPrice × itemQuantity (±0.01)
|
|
191
|
+
// optional: playerPhone, itemDescription, itemCategory
|
|
192
|
+
});
|
|
193
|
+
// item.status === "success", item.newBalance (post-spend), item.previousBalance,
|
|
194
|
+
// item.transactionId, item.orderId, item.financialBreakdown { total_paid, developer_revenue, platform_fee }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- **Grant the item off the `item.purchased` webhook**, not just this response — the webhook fires atomically with the spend (dedupe on `X-Invo-Idempotency-Key`). INVO debits currency; your game owns the catalog and grants the item.
|
|
198
|
+
- **Idempotent** on `clientRequestId` — a duplicate throws a `409` `InvoError` (`err.isDuplicateRequest`).
|
|
199
|
+
- **Insufficient balance** throws a `400` (`err.isInsufficientBalance`; `required_amount` + `current_balance` on `err.body`).
|
|
200
|
+
- **Throttles** return `429` with `err.retryAfter` (seconds). Validation (missing fields, quantity outside `1..1000`, price `≤0`/`>999999.99`/non-decimal, total ≠ unit×qty) throws `INVALID_INPUT` **before** any network call.
|
|
201
|
+
- Fee split is **90% developer / 10% INVO** by default (per-partner override). Not guardian-gated (the currency was already guardian-approved when it entered the wallet).
|
|
202
|
+
|
|
203
|
+
Companion reads (server): `invo.getItemPurchaseHistory({ playerEmail, limit?, offset? })` and `invo.getItemOrderDetails({ orderId | transactionId | clientRequestId })` — pass **exactly one** id; use `clientRequestId` for recovery ("did this purchase complete?").
|
|
204
|
+
|
|
205
|
+
## Errors
|
|
206
|
+
|
|
207
|
+
Every failure throws `InvoError` with `.code` (when present), `.status`, `.message`, `.body`. Branch on `.code`; for the handful of state errors with no `code` (e.g. `receiver_not_enrolled_use_claim_code`), use `.message` / the `.isReceiverNotEnrolled` helper. `.isTokenExpired` flags an expired token — if you pass `refreshToken` to `InvoClient` the SDK handles this for you (one re-mint + retry); otherwise re-mint server-side and retry. Client-side input guards (e.g. a USD amount outside `0 < x ≤ 999.99`, a missing `purchaseReference`, or `rail:"steam"` on `purchaseCurrency`) also throw `InvoError` with `.status === 0` before any network call.
|
|
208
|
+
|
|
209
|
+
## Scripts
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npm run build # tsup → dist (ESM + CJS + d.ts)
|
|
213
|
+
npm run typecheck # tsc --noEmit
|
|
214
|
+
npm test # vitest
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
Proprietary — © Invo Tech Inc. See `LICENSE` (the partner-distribution license is a business decision; swap to MIT/Apache-2.0 if preferred).
|
|
@@ -16,6 +16,32 @@ var InvoError = class _InvoError extends Error {
|
|
|
16
16
|
get isTokenExpired() {
|
|
17
17
|
return this.code === "SDK_TOKEN_EXPIRED";
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
21
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
22
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
23
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
24
|
+
*/
|
|
25
|
+
get isInsufficientBalance() {
|
|
26
|
+
if (this.status !== 400) return false;
|
|
27
|
+
const b = this.bodyObject();
|
|
28
|
+
return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
|
|
29
|
+
}
|
|
30
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
31
|
+
get isDuplicateRequest() {
|
|
32
|
+
return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
|
|
33
|
+
}
|
|
34
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
35
|
+
get retryAfter() {
|
|
36
|
+
const v = this.bodyObject()["retry_after"];
|
|
37
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
38
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
39
|
+
}
|
|
40
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
41
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
42
|
+
bodyObject() {
|
|
43
|
+
return this.body && typeof this.body === "object" ? this.body : {};
|
|
44
|
+
}
|
|
19
45
|
};
|
|
20
46
|
function errorFromResponse(status, body) {
|
|
21
47
|
let message = `INVO request failed (HTTP ${status})`;
|
|
@@ -117,5 +143,5 @@ var Http = class {
|
|
|
117
143
|
};
|
|
118
144
|
|
|
119
145
|
export { Http, InvoError, assertSecureBaseUrl };
|
|
120
|
-
//# sourceMappingURL=chunk-
|
|
121
|
-
//# sourceMappingURL=chunk-
|
|
146
|
+
//# sourceMappingURL=chunk-A44O4KC3.js.map
|
|
147
|
+
//# sourceMappingURL=chunk-A44O4KC3.js.map
|
|
@@ -107,6 +107,47 @@ interface PurchaseResult {
|
|
|
107
107
|
newBalance?: string | number | null;
|
|
108
108
|
raw: Record<string, unknown>;
|
|
109
109
|
}
|
|
110
|
+
interface PurchaseItemInput {
|
|
111
|
+
/** Idempotency key, unique per game. Required. A duplicate returns 409. */
|
|
112
|
+
clientRequestId: string;
|
|
113
|
+
playerEmail: string;
|
|
114
|
+
playerName: string;
|
|
115
|
+
itemId: string;
|
|
116
|
+
itemName: string;
|
|
117
|
+
/** Integer quantity, 1..1000. */
|
|
118
|
+
itemQuantity: number;
|
|
119
|
+
/** Price PER UNIT, in game-currency units (not USD). > 0 and <= 999999.99. */
|
|
120
|
+
unitPrice: string | number;
|
|
121
|
+
/** Must equal unitPrice × itemQuantity (±0.01). > 0 and <= 999999.99. */
|
|
122
|
+
totalPrice: string | number;
|
|
123
|
+
playerPhone?: string;
|
|
124
|
+
itemDescription?: string;
|
|
125
|
+
itemCategory?: string;
|
|
126
|
+
}
|
|
127
|
+
interface PurchaseItemResult {
|
|
128
|
+
/** "success" on a completed spend. */
|
|
129
|
+
status: string;
|
|
130
|
+
transactionId: string;
|
|
131
|
+
orderId: string;
|
|
132
|
+
/** Canonical balance AFTER the spend (game-currency units). */
|
|
133
|
+
newBalance: string | number | null;
|
|
134
|
+
previousBalance: string | number | null;
|
|
135
|
+
currencyName: string;
|
|
136
|
+
/** { total_paid, developer_revenue, platform_fee } when present. */
|
|
137
|
+
financialBreakdown?: Record<string, unknown>;
|
|
138
|
+
raw: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
interface ItemHistoryQuery {
|
|
141
|
+
playerEmail: string;
|
|
142
|
+
limit?: number;
|
|
143
|
+
offset?: number;
|
|
144
|
+
}
|
|
145
|
+
/** Look up an item order by EXACTLY ONE of these. */
|
|
146
|
+
interface ItemOrderQuery {
|
|
147
|
+
orderId?: string;
|
|
148
|
+
transactionId?: string;
|
|
149
|
+
clientRequestId?: string;
|
|
150
|
+
}
|
|
110
151
|
interface ApproveResult {
|
|
111
152
|
status: string;
|
|
112
153
|
next: string;
|
|
@@ -151,6 +192,20 @@ declare class InvoError extends Error {
|
|
|
151
192
|
get isReceiverNotEnrolled(): boolean;
|
|
152
193
|
/** True if the session/SDK token has expired and the caller should re-mint + retry. */
|
|
153
194
|
get isTokenExpired(): boolean;
|
|
195
|
+
/**
|
|
196
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
197
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
198
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
199
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
200
|
+
*/
|
|
201
|
+
get isInsufficientBalance(): boolean;
|
|
202
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
203
|
+
get isDuplicateRequest(): boolean;
|
|
204
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
205
|
+
get retryAfter(): number | undefined;
|
|
206
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
207
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
208
|
+
private bodyObject;
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h };
|
|
211
|
+
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h, type PurchaseItemInput as i, type PurchaseItemResult as j, type ItemHistoryQuery as k, type ItemOrderQuery as l };
|
|
@@ -107,6 +107,47 @@ interface PurchaseResult {
|
|
|
107
107
|
newBalance?: string | number | null;
|
|
108
108
|
raw: Record<string, unknown>;
|
|
109
109
|
}
|
|
110
|
+
interface PurchaseItemInput {
|
|
111
|
+
/** Idempotency key, unique per game. Required. A duplicate returns 409. */
|
|
112
|
+
clientRequestId: string;
|
|
113
|
+
playerEmail: string;
|
|
114
|
+
playerName: string;
|
|
115
|
+
itemId: string;
|
|
116
|
+
itemName: string;
|
|
117
|
+
/** Integer quantity, 1..1000. */
|
|
118
|
+
itemQuantity: number;
|
|
119
|
+
/** Price PER UNIT, in game-currency units (not USD). > 0 and <= 999999.99. */
|
|
120
|
+
unitPrice: string | number;
|
|
121
|
+
/** Must equal unitPrice × itemQuantity (±0.01). > 0 and <= 999999.99. */
|
|
122
|
+
totalPrice: string | number;
|
|
123
|
+
playerPhone?: string;
|
|
124
|
+
itemDescription?: string;
|
|
125
|
+
itemCategory?: string;
|
|
126
|
+
}
|
|
127
|
+
interface PurchaseItemResult {
|
|
128
|
+
/** "success" on a completed spend. */
|
|
129
|
+
status: string;
|
|
130
|
+
transactionId: string;
|
|
131
|
+
orderId: string;
|
|
132
|
+
/** Canonical balance AFTER the spend (game-currency units). */
|
|
133
|
+
newBalance: string | number | null;
|
|
134
|
+
previousBalance: string | number | null;
|
|
135
|
+
currencyName: string;
|
|
136
|
+
/** { total_paid, developer_revenue, platform_fee } when present. */
|
|
137
|
+
financialBreakdown?: Record<string, unknown>;
|
|
138
|
+
raw: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
interface ItemHistoryQuery {
|
|
141
|
+
playerEmail: string;
|
|
142
|
+
limit?: number;
|
|
143
|
+
offset?: number;
|
|
144
|
+
}
|
|
145
|
+
/** Look up an item order by EXACTLY ONE of these. */
|
|
146
|
+
interface ItemOrderQuery {
|
|
147
|
+
orderId?: string;
|
|
148
|
+
transactionId?: string;
|
|
149
|
+
clientRequestId?: string;
|
|
150
|
+
}
|
|
110
151
|
interface ApproveResult {
|
|
111
152
|
status: string;
|
|
112
153
|
next: string;
|
|
@@ -151,6 +192,20 @@ declare class InvoError extends Error {
|
|
|
151
192
|
get isReceiverNotEnrolled(): boolean;
|
|
152
193
|
/** True if the session/SDK token has expired and the caller should re-mint + retry. */
|
|
153
194
|
get isTokenExpired(): boolean;
|
|
195
|
+
/**
|
|
196
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
197
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
198
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
199
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
200
|
+
*/
|
|
201
|
+
get isInsufficientBalance(): boolean;
|
|
202
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
203
|
+
get isDuplicateRequest(): boolean;
|
|
204
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
205
|
+
get retryAfter(): number | undefined;
|
|
206
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
207
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
208
|
+
private bodyObject;
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h };
|
|
211
|
+
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h, type PurchaseItemInput as i, type PurchaseItemResult as j, type ItemHistoryQuery as k, type ItemOrderQuery as l };
|
package/dist/index.cjs
CHANGED
|
@@ -18,6 +18,32 @@ var InvoError = class _InvoError extends Error {
|
|
|
18
18
|
get isTokenExpired() {
|
|
19
19
|
return this.code === "SDK_TOKEN_EXPIRED";
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
23
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
24
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
25
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
26
|
+
*/
|
|
27
|
+
get isInsufficientBalance() {
|
|
28
|
+
if (this.status !== 400) return false;
|
|
29
|
+
const b = this.bodyObject();
|
|
30
|
+
return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
|
|
31
|
+
}
|
|
32
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
33
|
+
get isDuplicateRequest() {
|
|
34
|
+
return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
|
|
35
|
+
}
|
|
36
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
37
|
+
get retryAfter() {
|
|
38
|
+
const v = this.bodyObject()["retry_after"];
|
|
39
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
40
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
41
|
+
}
|
|
42
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
43
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
44
|
+
bodyObject() {
|
|
45
|
+
return this.body && typeof this.body === "object" ? this.body : {};
|
|
46
|
+
}
|
|
21
47
|
};
|
|
22
48
|
function errorFromResponse(status, body) {
|
|
23
49
|
let message = `INVO request failed (HTTP ${status})`;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-DV5QsftP.cjs';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.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 ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-DV5QsftP.js';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.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-
|
|
2
|
-
export { InvoError } from './chunk-
|
|
1
|
+
import { assertSecureBaseUrl, Http, InvoError } from './chunk-A44O4KC3.js';
|
|
2
|
+
export { InvoError } from './chunk-A44O4KC3.js';
|
|
3
3
|
|
|
4
4
|
// src/shared/webauthn.ts
|
|
5
5
|
function b64urlToBuffer(value) {
|
package/dist/server.cjs
CHANGED
|
@@ -18,6 +18,32 @@ var InvoError = class _InvoError extends Error {
|
|
|
18
18
|
get isTokenExpired() {
|
|
19
19
|
return this.code === "SDK_TOKEN_EXPIRED";
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
23
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
24
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
25
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
26
|
+
*/
|
|
27
|
+
get isInsufficientBalance() {
|
|
28
|
+
if (this.status !== 400) return false;
|
|
29
|
+
const b = this.bodyObject();
|
|
30
|
+
return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
|
|
31
|
+
}
|
|
32
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
33
|
+
get isDuplicateRequest() {
|
|
34
|
+
return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
|
|
35
|
+
}
|
|
36
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
37
|
+
get retryAfter() {
|
|
38
|
+
const v = this.bodyObject()["retry_after"];
|
|
39
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
40
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
41
|
+
}
|
|
42
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
43
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
44
|
+
bodyObject() {
|
|
45
|
+
return this.body && typeof this.body === "object" ? this.body : {};
|
|
46
|
+
}
|
|
21
47
|
};
|
|
22
48
|
function errorFromResponse(status, body) {
|
|
23
49
|
let message = `INVO request failed (HTTP ${status})`;
|
|
@@ -121,29 +147,35 @@ var Http = class {
|
|
|
121
147
|
// src/server.ts
|
|
122
148
|
var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
|
|
123
149
|
var MAX_USD_AMOUNT = 999.99;
|
|
124
|
-
|
|
150
|
+
var MAX_ITEM_PRICE = 999999.99;
|
|
151
|
+
function invalidInput(label, value, why) {
|
|
125
152
|
return new InvoError({
|
|
126
|
-
message:
|
|
153
|
+
message: `${label} ${why} (got ${JSON.stringify(value)}).`,
|
|
127
154
|
code: "INVALID_INPUT",
|
|
128
155
|
status: 0
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
|
-
|
|
158
|
+
var PLAIN_DECIMAL = /^\d+(\.\d{1,2})?$/;
|
|
159
|
+
function parseMoney(value, max, label) {
|
|
132
160
|
let n;
|
|
133
|
-
if (typeof
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw invalidAmount(usdAmount, "must have at most 2 decimal places");
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
if (!Number.isFinite(value) || !PLAIN_DECIMAL.test(String(value))) {
|
|
163
|
+
throw invalidInput(label, value, "must be a positive decimal with at most 2 places");
|
|
137
164
|
}
|
|
165
|
+
n = value;
|
|
138
166
|
} else {
|
|
139
|
-
if (
|
|
140
|
-
throw
|
|
167
|
+
if (!PLAIN_DECIMAL.test(value)) {
|
|
168
|
+
throw invalidInput(label, value, "must be a plain decimal value with at most 2 places");
|
|
141
169
|
}
|
|
142
|
-
n = Number(
|
|
170
|
+
n = Number(value);
|
|
143
171
|
}
|
|
144
|
-
if (!Number.isFinite(n) || n <= 0 || n >
|
|
145
|
-
throw
|
|
172
|
+
if (!Number.isFinite(n) || n <= 0 || n > max) {
|
|
173
|
+
throw invalidInput(label, value, `must be > 0 and <= ${max}`);
|
|
146
174
|
}
|
|
175
|
+
return n;
|
|
176
|
+
}
|
|
177
|
+
function assertUsdAmount(usdAmount) {
|
|
178
|
+
parseMoney(usdAmount, MAX_USD_AMOUNT, "usdAmount");
|
|
147
179
|
}
|
|
148
180
|
function requireField(value, field, raw) {
|
|
149
181
|
const s = value == null ? "" : String(value);
|
|
@@ -322,6 +354,104 @@ var InvoServer = class {
|
|
|
322
354
|
this.auth
|
|
323
355
|
);
|
|
324
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
359
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
360
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
361
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
362
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
363
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
364
|
+
*/
|
|
365
|
+
async purchaseItem(input) {
|
|
366
|
+
const required = [
|
|
367
|
+
["clientRequestId", input.clientRequestId],
|
|
368
|
+
["playerEmail", input.playerEmail],
|
|
369
|
+
["playerName", input.playerName],
|
|
370
|
+
["itemId", input.itemId],
|
|
371
|
+
["itemName", input.itemName]
|
|
372
|
+
];
|
|
373
|
+
for (const [k, v] of required) {
|
|
374
|
+
if (typeof v !== "string" || !v.trim()) throw invalidInput(k, v, "is required");
|
|
375
|
+
}
|
|
376
|
+
if (!Number.isInteger(input.itemQuantity) || input.itemQuantity < 1 || input.itemQuantity > 1e3) {
|
|
377
|
+
throw invalidInput("itemQuantity", input.itemQuantity, "must be an integer 1..1000");
|
|
378
|
+
}
|
|
379
|
+
const unit = parseMoney(input.unitPrice, MAX_ITEM_PRICE, "unitPrice");
|
|
380
|
+
const total = parseMoney(input.totalPrice, MAX_ITEM_PRICE, "totalPrice");
|
|
381
|
+
const expectedCents = Math.round(unit * input.itemQuantity * 100);
|
|
382
|
+
const totalCents = Math.round(total * 100);
|
|
383
|
+
if (Math.abs(totalCents - expectedCents) > 1) {
|
|
384
|
+
throw invalidInput(
|
|
385
|
+
"totalPrice",
|
|
386
|
+
input.totalPrice,
|
|
387
|
+
`must equal unitPrice \xD7 itemQuantity (\xB10.01): ${unit} \xD7 ${input.itemQuantity}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const body = {
|
|
391
|
+
client_request_id: input.clientRequestId,
|
|
392
|
+
player_email: input.playerEmail,
|
|
393
|
+
player_name: input.playerName,
|
|
394
|
+
item_id: input.itemId,
|
|
395
|
+
item_name: input.itemName,
|
|
396
|
+
item_quantity: input.itemQuantity,
|
|
397
|
+
unit_price: input.unitPrice,
|
|
398
|
+
total_price: input.totalPrice
|
|
399
|
+
};
|
|
400
|
+
if (input.playerPhone) body["player_phone"] = input.playerPhone;
|
|
401
|
+
if (input.itemDescription) body["item_description"] = input.itemDescription;
|
|
402
|
+
if (input.itemCategory) body["item_category"] = input.itemCategory;
|
|
403
|
+
const raw = await this.http.post(
|
|
404
|
+
"/api/item-purchases/purchase-item",
|
|
405
|
+
body,
|
|
406
|
+
this.auth
|
|
407
|
+
);
|
|
408
|
+
return {
|
|
409
|
+
status: String(raw["status"] ?? ""),
|
|
410
|
+
transactionId: requireField(raw["transaction_id"], "transaction_id", raw),
|
|
411
|
+
orderId: requireField(raw["order_id"], "order_id", raw),
|
|
412
|
+
newBalance: raw["new_balance"] ?? null,
|
|
413
|
+
previousBalance: raw["previous_balance"] ?? null,
|
|
414
|
+
currencyName: String(raw["currency_name"] ?? ""),
|
|
415
|
+
financialBreakdown: raw["financial_breakdown"],
|
|
416
|
+
raw
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
420
|
+
async getItemPurchaseHistory(query) {
|
|
421
|
+
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
422
|
+
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
423
|
+
}
|
|
424
|
+
const q = new URLSearchParams();
|
|
425
|
+
q.set("player_email", query.playerEmail);
|
|
426
|
+
if (query.limit != null) q.set("limit", String(query.limit));
|
|
427
|
+
if (query.offset != null) q.set("offset", String(query.offset));
|
|
428
|
+
return this.http.get(
|
|
429
|
+
`/api/item-purchases/player-purchase-history?${q.toString()}`,
|
|
430
|
+
this.auth
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
434
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
435
|
+
async getItemOrderDetails(query) {
|
|
436
|
+
const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
|
|
437
|
+
(v) => typeof v === "string" && v.trim()
|
|
438
|
+
);
|
|
439
|
+
if (provided.length !== 1) {
|
|
440
|
+
throw new InvoError({
|
|
441
|
+
message: "getItemOrderDetails requires EXACTLY ONE of orderId, transactionId, or clientRequestId.",
|
|
442
|
+
code: "INVALID_INPUT",
|
|
443
|
+
status: 0
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const q = new URLSearchParams();
|
|
447
|
+
if (query.orderId) q.set("order_id", query.orderId);
|
|
448
|
+
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
449
|
+
if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
|
|
450
|
+
return this.http.get(
|
|
451
|
+
`/api/item-purchases/order-details?${q.toString()}`,
|
|
452
|
+
this.auth
|
|
453
|
+
);
|
|
454
|
+
}
|
|
325
455
|
toInitiateResult(raw) {
|
|
326
456
|
const vm = raw["verification_method"];
|
|
327
457
|
const guardian = raw["guardian_approval"];
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult, i as PurchaseItemInput, j as PurchaseItemResult, k as ItemHistoryQuery, l as ItemOrderQuery } from './errors-DV5QsftP.cjs';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.cjs';
|
|
3
3
|
|
|
4
4
|
declare class InvoServer {
|
|
5
5
|
private readonly http;
|
|
@@ -36,7 +36,21 @@ declare class InvoServer {
|
|
|
36
36
|
orderId?: string;
|
|
37
37
|
transactionId?: string;
|
|
38
38
|
}): Promise<Record<string, unknown>>;
|
|
39
|
+
/**
|
|
40
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
41
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
42
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
43
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
44
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
45
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
46
|
+
*/
|
|
47
|
+
purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
|
|
48
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
49
|
+
getItemPurchaseHistory(query: ItemHistoryQuery): Promise<Record<string, unknown>>;
|
|
50
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
51
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
52
|
+
getItemOrderDetails(query: ItemOrderQuery): Promise<Record<string, unknown>>;
|
|
39
53
|
private toInitiateResult;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, PlayerToken, PurchaseInput, PurchaseResult, ServerConfig };
|
|
56
|
+
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, ItemHistoryQuery, ItemOrderQuery, PlayerToken, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult, i as PurchaseItemInput, j as PurchaseItemResult, k as ItemHistoryQuery, l as ItemOrderQuery } from './errors-DV5QsftP.js';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.js';
|
|
3
3
|
|
|
4
4
|
declare class InvoServer {
|
|
5
5
|
private readonly http;
|
|
@@ -36,7 +36,21 @@ declare class InvoServer {
|
|
|
36
36
|
orderId?: string;
|
|
37
37
|
transactionId?: string;
|
|
38
38
|
}): Promise<Record<string, unknown>>;
|
|
39
|
+
/**
|
|
40
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
41
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
42
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
43
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
44
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
45
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
46
|
+
*/
|
|
47
|
+
purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
|
|
48
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
49
|
+
getItemPurchaseHistory(query: ItemHistoryQuery): Promise<Record<string, unknown>>;
|
|
50
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
51
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
52
|
+
getItemOrderDetails(query: ItemOrderQuery): Promise<Record<string, unknown>>;
|
|
39
53
|
private toInitiateResult;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, PlayerToken, PurchaseInput, PurchaseResult, ServerConfig };
|
|
56
|
+
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, ItemHistoryQuery, ItemOrderQuery, PlayerToken, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig };
|
package/dist/server.js
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
import { assertSecureBaseUrl, Http, InvoError } from './chunk-
|
|
2
|
-
export { InvoError } from './chunk-
|
|
1
|
+
import { assertSecureBaseUrl, Http, InvoError } from './chunk-A44O4KC3.js';
|
|
2
|
+
export { InvoError } from './chunk-A44O4KC3.js';
|
|
3
3
|
|
|
4
4
|
// src/server.ts
|
|
5
5
|
var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
|
|
6
6
|
var MAX_USD_AMOUNT = 999.99;
|
|
7
|
-
|
|
7
|
+
var MAX_ITEM_PRICE = 999999.99;
|
|
8
|
+
function invalidInput(label, value, why) {
|
|
8
9
|
return new InvoError({
|
|
9
|
-
message:
|
|
10
|
+
message: `${label} ${why} (got ${JSON.stringify(value)}).`,
|
|
10
11
|
code: "INVALID_INPUT",
|
|
11
12
|
status: 0
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
var PLAIN_DECIMAL = /^\d+(\.\d{1,2})?$/;
|
|
16
|
+
function parseMoney(value, max, label) {
|
|
15
17
|
let n;
|
|
16
|
-
if (typeof
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
throw invalidAmount(usdAmount, "must have at most 2 decimal places");
|
|
18
|
+
if (typeof value === "number") {
|
|
19
|
+
if (!Number.isFinite(value) || !PLAIN_DECIMAL.test(String(value))) {
|
|
20
|
+
throw invalidInput(label, value, "must be a positive decimal with at most 2 places");
|
|
20
21
|
}
|
|
22
|
+
n = value;
|
|
21
23
|
} else {
|
|
22
|
-
if (
|
|
23
|
-
throw
|
|
24
|
+
if (!PLAIN_DECIMAL.test(value)) {
|
|
25
|
+
throw invalidInput(label, value, "must be a plain decimal value with at most 2 places");
|
|
24
26
|
}
|
|
25
|
-
n = Number(
|
|
27
|
+
n = Number(value);
|
|
26
28
|
}
|
|
27
|
-
if (!Number.isFinite(n) || n <= 0 || n >
|
|
28
|
-
throw
|
|
29
|
+
if (!Number.isFinite(n) || n <= 0 || n > max) {
|
|
30
|
+
throw invalidInput(label, value, `must be > 0 and <= ${max}`);
|
|
29
31
|
}
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
function assertUsdAmount(usdAmount) {
|
|
35
|
+
parseMoney(usdAmount, MAX_USD_AMOUNT, "usdAmount");
|
|
30
36
|
}
|
|
31
37
|
function requireField(value, field, raw) {
|
|
32
38
|
const s = value == null ? "" : String(value);
|
|
@@ -205,6 +211,104 @@ var InvoServer = class {
|
|
|
205
211
|
this.auth
|
|
206
212
|
);
|
|
207
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
216
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
217
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
218
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
219
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
220
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
221
|
+
*/
|
|
222
|
+
async purchaseItem(input) {
|
|
223
|
+
const required = [
|
|
224
|
+
["clientRequestId", input.clientRequestId],
|
|
225
|
+
["playerEmail", input.playerEmail],
|
|
226
|
+
["playerName", input.playerName],
|
|
227
|
+
["itemId", input.itemId],
|
|
228
|
+
["itemName", input.itemName]
|
|
229
|
+
];
|
|
230
|
+
for (const [k, v] of required) {
|
|
231
|
+
if (typeof v !== "string" || !v.trim()) throw invalidInput(k, v, "is required");
|
|
232
|
+
}
|
|
233
|
+
if (!Number.isInteger(input.itemQuantity) || input.itemQuantity < 1 || input.itemQuantity > 1e3) {
|
|
234
|
+
throw invalidInput("itemQuantity", input.itemQuantity, "must be an integer 1..1000");
|
|
235
|
+
}
|
|
236
|
+
const unit = parseMoney(input.unitPrice, MAX_ITEM_PRICE, "unitPrice");
|
|
237
|
+
const total = parseMoney(input.totalPrice, MAX_ITEM_PRICE, "totalPrice");
|
|
238
|
+
const expectedCents = Math.round(unit * input.itemQuantity * 100);
|
|
239
|
+
const totalCents = Math.round(total * 100);
|
|
240
|
+
if (Math.abs(totalCents - expectedCents) > 1) {
|
|
241
|
+
throw invalidInput(
|
|
242
|
+
"totalPrice",
|
|
243
|
+
input.totalPrice,
|
|
244
|
+
`must equal unitPrice \xD7 itemQuantity (\xB10.01): ${unit} \xD7 ${input.itemQuantity}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const body = {
|
|
248
|
+
client_request_id: input.clientRequestId,
|
|
249
|
+
player_email: input.playerEmail,
|
|
250
|
+
player_name: input.playerName,
|
|
251
|
+
item_id: input.itemId,
|
|
252
|
+
item_name: input.itemName,
|
|
253
|
+
item_quantity: input.itemQuantity,
|
|
254
|
+
unit_price: input.unitPrice,
|
|
255
|
+
total_price: input.totalPrice
|
|
256
|
+
};
|
|
257
|
+
if (input.playerPhone) body["player_phone"] = input.playerPhone;
|
|
258
|
+
if (input.itemDescription) body["item_description"] = input.itemDescription;
|
|
259
|
+
if (input.itemCategory) body["item_category"] = input.itemCategory;
|
|
260
|
+
const raw = await this.http.post(
|
|
261
|
+
"/api/item-purchases/purchase-item",
|
|
262
|
+
body,
|
|
263
|
+
this.auth
|
|
264
|
+
);
|
|
265
|
+
return {
|
|
266
|
+
status: String(raw["status"] ?? ""),
|
|
267
|
+
transactionId: requireField(raw["transaction_id"], "transaction_id", raw),
|
|
268
|
+
orderId: requireField(raw["order_id"], "order_id", raw),
|
|
269
|
+
newBalance: raw["new_balance"] ?? null,
|
|
270
|
+
previousBalance: raw["previous_balance"] ?? null,
|
|
271
|
+
currencyName: String(raw["currency_name"] ?? ""),
|
|
272
|
+
financialBreakdown: raw["financial_breakdown"],
|
|
273
|
+
raw
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
277
|
+
async getItemPurchaseHistory(query) {
|
|
278
|
+
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
279
|
+
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
280
|
+
}
|
|
281
|
+
const q = new URLSearchParams();
|
|
282
|
+
q.set("player_email", query.playerEmail);
|
|
283
|
+
if (query.limit != null) q.set("limit", String(query.limit));
|
|
284
|
+
if (query.offset != null) q.set("offset", String(query.offset));
|
|
285
|
+
return this.http.get(
|
|
286
|
+
`/api/item-purchases/player-purchase-history?${q.toString()}`,
|
|
287
|
+
this.auth
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
291
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
292
|
+
async getItemOrderDetails(query) {
|
|
293
|
+
const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
|
|
294
|
+
(v) => typeof v === "string" && v.trim()
|
|
295
|
+
);
|
|
296
|
+
if (provided.length !== 1) {
|
|
297
|
+
throw new InvoError({
|
|
298
|
+
message: "getItemOrderDetails requires EXACTLY ONE of orderId, transactionId, or clientRequestId.",
|
|
299
|
+
code: "INVALID_INPUT",
|
|
300
|
+
status: 0
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const q = new URLSearchParams();
|
|
304
|
+
if (query.orderId) q.set("order_id", query.orderId);
|
|
305
|
+
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
306
|
+
if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
|
|
307
|
+
return this.http.get(
|
|
308
|
+
`/api/item-purchases/order-details?${q.toString()}`,
|
|
309
|
+
this.auth
|
|
310
|
+
);
|
|
311
|
+
}
|
|
208
312
|
toInitiateResult(raw) {
|
|
209
313
|
const vm = raw["verification_method"];
|
|
210
314
|
const guardian = raw["guardian_approval"];
|
package/package.json
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@invonetwork/web-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "INVO Web SDK — currency purchase + passkey (WebAuthn) verification for partner web platforms.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"private": false,
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/Invo-Technologies/invo-web-sdk.git"
|
|
10
|
-
},
|
|
11
|
-
"homepage": "https://docs.invo.network",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/Invo-Technologies/invo-web-sdk/issues"
|
|
14
|
-
},
|
|
15
|
-
"type": "module",
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=18"
|
|
18
|
-
},
|
|
19
|
-
"sideEffects": false,
|
|
20
|
-
"files": [
|
|
21
|
-
"dist",
|
|
22
|
-
"!dist/**/*.map",
|
|
23
|
-
"README.md",
|
|
24
|
-
"LICENSE",
|
|
25
|
-
"CHANGELOG.md"
|
|
26
|
-
],
|
|
27
|
-
"exports": {
|
|
28
|
-
".": {
|
|
29
|
-
"types": "./dist/index.d.ts",
|
|
30
|
-
"import": "./dist/index.js",
|
|
31
|
-
"require": "./dist/index.cjs"
|
|
32
|
-
},
|
|
33
|
-
"./server": {
|
|
34
|
-
"types": "./dist/server.d.ts",
|
|
35
|
-
"import": "./dist/server.js",
|
|
36
|
-
"require": "./dist/server.cjs"
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
"main": "./dist/index.cjs",
|
|
40
|
-
"module": "./dist/index.js",
|
|
41
|
-
"types": "./dist/index.d.ts",
|
|
42
|
-
"scripts": {
|
|
43
|
-
"build": "tsup",
|
|
44
|
-
"dev": "tsup --watch",
|
|
45
|
-
"typecheck": "tsc --noEmit",
|
|
46
|
-
"test": "vitest run",
|
|
47
|
-
"test:watch": "vitest",
|
|
48
|
-
"clean": "rimraf dist",
|
|
49
|
-
"prepublishOnly": "npm run clean && npm run build"
|
|
50
|
-
},
|
|
51
|
-
"keywords": [
|
|
52
|
-
"invo",
|
|
53
|
-
"webauthn",
|
|
54
|
-
"passkey",
|
|
55
|
-
"payments",
|
|
56
|
-
"game-currency",
|
|
57
|
-
"sdk"
|
|
58
|
-
],
|
|
59
|
-
"publishConfig": {
|
|
60
|
-
"access": "public"
|
|
61
|
-
},
|
|
62
|
-
"devDependencies": {
|
|
63
|
-
"rimraf": "^5.0.5",
|
|
64
|
-
"tsup": "^8.0.1",
|
|
65
|
-
"typescript": "^5.4.5",
|
|
66
|
-
"vitest": "^1.5.0"
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@invonetwork/web-sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "INVO Web SDK — currency purchase + passkey (WebAuthn) verification for partner web platforms.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Invo-Technologies/invo-web-sdk.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.invo.network",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Invo-Technologies/invo-web-sdk/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"!dist/**/*.map",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./server": {
|
|
34
|
+
"types": "./dist/server.d.ts",
|
|
35
|
+
"import": "./dist/server.js",
|
|
36
|
+
"require": "./dist/server.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.cjs",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"dev": "tsup --watch",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"clean": "rimraf dist",
|
|
49
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"invo",
|
|
53
|
+
"webauthn",
|
|
54
|
+
"passkey",
|
|
55
|
+
"payments",
|
|
56
|
+
"game-currency",
|
|
57
|
+
"sdk"
|
|
58
|
+
],
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"rimraf": "^5.0.5",
|
|
64
|
+
"tsup": "^8.0.1",
|
|
65
|
+
"typescript": "^5.4.5",
|
|
66
|
+
"vitest": "^1.5.0"
|
|
67
|
+
}
|
|
68
|
+
}
|