@invonetwork/web-sdk 0.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 ADDED
@@ -0,0 +1,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.1.0] — 2026-06-30
7
+
8
+ Initial scaffold.
9
+
10
+ - `InvoServer` (`/server`): `mintPlayerToken`, `initiateSend`, `initiateTransfer`,
11
+ `createCheckout`, `purchaseCurrency`, `confirmPayment`, `getOrderDetails`.
12
+ - Client-side purchase guards (§4.7): USD amount `0 < x ≤ 999.99`, required
13
+ `purchaseReference`, and `rail:"steam"` rejected before the network call
14
+ (`INVALID_INPUT` / `MISSING_PURCHASE_REFERENCE` / `WRONG_RAIL_ENDPOINT`).
15
+ - `InvoClient` (browser): `enrollPasskey`, `approveSend`/`approveTransfer`,
16
+ `confirmReceiptSend`/`confirmReceiptTransfer`, and `linkDevice` for the
17
+ interchangeable-methods flow (§4.6).
18
+ - Optional `refreshToken` hook: transparently re-mints and retries once on
19
+ `SDK_TOKEN_EXPIRED` (§11.2).
20
+ - Shared: typed `InvoError`, isomorphic HTTP client, WebAuthn JSON⇄binary helpers.
21
+ - Tests: HTTP layer, server request mapping + purchase guards, browser client
22
+ flows (enroll/approve/link/token-refresh), and WebAuthn serialization.
23
+ - Contracts extracted + auditor-verified against the INVO backend.
24
+
25
+ ### Hardening (independent red-team pass)
26
+ - **Guardian/minor `202` path no longer mismapped to `verificationMethod:"sms"`** —
27
+ `initiateSend`/`initiateTransfer` now return `verificationMethod: undefined` and a
28
+ `guardianApproval` block on the guardian path, so callers don't route into the
29
+ PIN UI by mistake (§4.3).
30
+ - `usdAmount` validation tightened: rejects non-plain-decimal strings
31
+ (`"0x10"`, `"1e2"`, whitespace) and >2 decimal places before any network call.
32
+ - Load-bearing response fields (`token`, `checkout_url`) now throw `INVALID_RESPONSE`
33
+ instead of silently surfacing as empty strings.
34
+ - `getOrderDetails` requires at least one of `orderId`/`transactionId`.
35
+ - Token refresh now re-runs the **whole** passkey ceremony on `SDK_TOKEN_EXPIRED`
36
+ (never replays a single-use assertion) and single-flights concurrent refreshes.
37
+ - `baseUrl` must be `https://` (localhost exempt) so the token/secret can't travel
38
+ in cleartext.
39
+ - Published tarball excludes sourcemaps (no proprietary source shipped).
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Copyright (c) 2026 Invo Tech Inc. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are the
4
+ proprietary and confidential property of Invo Tech Inc. ("INVO"). The Software
5
+ is licensed, not sold, for use solely in connection with integrating the INVO
6
+ platform under a separate written agreement with INVO.
7
+
8
+ Without a valid agreement with INVO, you may not use, copy, modify, merge,
9
+ publish, distribute, sublicense, or sell copies of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
+ IMPLIED. IN NO EVENT SHALL INVO BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
13
+ LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE.
14
+
15
+ NOTE: Licensing for a partner-distributed SDK is a business decision — if INVO
16
+ prefers a permissive license (e.g. MIT/Apache-2.0) so partners can freely
17
+ vendor the package, replace this file and the "license" field in package.json.
package/README.md ADDED
@@ -0,0 +1,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 |
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
+ ## Errors
129
+
130
+ 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.
131
+
132
+ ## Scripts
133
+
134
+ ```bash
135
+ npm run build # tsup → dist (ESM + CJS + d.ts)
136
+ npm run typecheck # tsc --noEmit
137
+ npm test # vitest
138
+ ```
139
+
140
+ ## License
141
+
142
+ Proprietary — © Invo Tech Inc. See `LICENSE` (the partner-distribution license is a business decision; swap to MIT/Apache-2.0 if preferred).
@@ -0,0 +1,121 @@
1
+ // src/shared/errors.ts
2
+ var InvoError = class _InvoError extends Error {
3
+ constructor(args) {
4
+ super(args.message);
5
+ this.name = "InvoError";
6
+ this.code = args.code;
7
+ this.status = args.status;
8
+ this.body = args.body ?? null;
9
+ Object.setPrototypeOf(this, _InvoError.prototype);
10
+ }
11
+ /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
12
+ get isReceiverNotEnrolled() {
13
+ return this.code === "receiver_not_enrolled_use_claim_code" || /receiver_not_enrolled_use_claim_code/i.test(this.message);
14
+ }
15
+ /** True if the session/SDK token has expired and the caller should re-mint + retry. */
16
+ get isTokenExpired() {
17
+ return this.code === "SDK_TOKEN_EXPIRED";
18
+ }
19
+ };
20
+ function errorFromResponse(status, body) {
21
+ let message = `INVO request failed (HTTP ${status})`;
22
+ let code;
23
+ if (body && typeof body === "object") {
24
+ const b = body;
25
+ if (typeof b["code"] === "string") code = b["code"];
26
+ if (typeof b["error"] === "string") message = b["error"];
27
+ else if (typeof b["message"] === "string") message = b["message"];
28
+ } else if (typeof body === "string" && body.trim()) {
29
+ message = body;
30
+ }
31
+ return new InvoError({ message, code, status, body });
32
+ }
33
+
34
+ // src/shared/http.ts
35
+ var DEFAULT_TIMEOUT = 3e4;
36
+ function assertSecureBaseUrl(baseUrl) {
37
+ let u;
38
+ try {
39
+ u = new URL(baseUrl);
40
+ } catch {
41
+ throw new Error(`Invalid baseUrl: ${baseUrl}`);
42
+ }
43
+ const isLocal = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
44
+ if (u.protocol === "https:" || u.protocol === "http:" && isLocal) return;
45
+ throw new Error(
46
+ `baseUrl must use https:// (got "${u.protocol}//"). Plaintext would expose the token/secret on the wire.`
47
+ );
48
+ }
49
+ var Http = class {
50
+ constructor(opts) {
51
+ this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
52
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT;
53
+ const f = opts.fetchImpl ?? globalThis.fetch;
54
+ if (typeof f !== "function") {
55
+ throw new Error(
56
+ "No fetch implementation available. Use Node >=18, or pass `fetch` in the config."
57
+ );
58
+ }
59
+ this.fetchImpl = f;
60
+ this.userAgent = opts.userAgent;
61
+ }
62
+ async post(path, body, auth) {
63
+ return this.request("POST", path, body, auth);
64
+ }
65
+ async get(path, auth) {
66
+ return this.request("GET", path, void 0, auth);
67
+ }
68
+ authHeaders(auth) {
69
+ switch (auth.kind) {
70
+ case "game-secret":
71
+ return { "X-Game-Secret-Key": auth.secret };
72
+ case "bearer":
73
+ return { Authorization: `Bearer ${auth.token}` };
74
+ case "none":
75
+ return {};
76
+ }
77
+ }
78
+ async request(method, path, body, auth) {
79
+ const url = `${this.baseUrl}${path}`;
80
+ const headers = {
81
+ Accept: "application/json",
82
+ ...this.authHeaders(auth)
83
+ };
84
+ if (this.userAgent) headers["User-Agent"] = this.userAgent;
85
+ if (body !== void 0) headers["Content-Type"] = "application/json";
86
+ const controller = new AbortController();
87
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
88
+ let res;
89
+ try {
90
+ res = await this.fetchImpl(url, {
91
+ method,
92
+ headers,
93
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
94
+ signal: controller.signal
95
+ });
96
+ } catch (err) {
97
+ throw new InvoError({
98
+ message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
99
+ status: 0,
100
+ body: null
101
+ });
102
+ } finally {
103
+ clearTimeout(timer);
104
+ }
105
+ const text = await res.text();
106
+ let parsed = null;
107
+ if (text) {
108
+ try {
109
+ parsed = JSON.parse(text);
110
+ } catch {
111
+ parsed = text;
112
+ }
113
+ }
114
+ if (!res.ok) throw errorFromResponse(res.status, parsed);
115
+ return parsed ?? {};
116
+ }
117
+ };
118
+
119
+ export { Http, InvoError, assertSecureBaseUrl };
120
+ //# sourceMappingURL=chunk-KUQVVH2P.js.map
121
+ //# sourceMappingURL=chunk-KUQVVH2P.js.map
@@ -0,0 +1,156 @@
1
+ /** Public, neutral payment-rail names. Provider/processor names are an internal
2
+ * backend routing detail and are deliberately NOT exposed here. */
3
+ type Rail = "platform" | "game" | "steam";
4
+ /** Verification method returned by initiate-send / initiate-transfer. */
5
+ type VerificationMethod = "in_app" | "sms";
6
+ interface InvoConfig {
7
+ /** API base URL. Prod: "https://invo.network". Sandbox: "https://sandbox.invo.network/sandbox". */
8
+ baseUrl: string;
9
+ /** Per-request timeout (ms). Default 30000. */
10
+ timeoutMs?: number;
11
+ /** Optional fetch override (e.g. a custom agent). Defaults to global fetch. */
12
+ fetch?: typeof fetch;
13
+ }
14
+ interface ServerConfig extends InvoConfig {
15
+ /** The game secret. Server-side ONLY — never ship this to a browser. */
16
+ gameSecret: string;
17
+ }
18
+ interface ClientConfig extends InvoConfig {
19
+ /** Short-lived, game-scoped player token minted server-side via mintPlayerToken(). */
20
+ token: string;
21
+ /**
22
+ * Optional. Called when a request fails with `SDK_TOKEN_EXPIRED` (token TTL is
23
+ * ~15 min, §11.2). Return a freshly minted token — typically by calling your own
24
+ * backend, which re-mints server-side with the game secret. The failed request
25
+ * is retried once with the new token, and the client keeps using it thereafter.
26
+ */
27
+ refreshToken?: () => Promise<string> | string;
28
+ }
29
+ interface PlayerToken {
30
+ token: string;
31
+ expiresAt: string;
32
+ identityId: string;
33
+ }
34
+ interface InitiateSendInput {
35
+ clientRequestId: string;
36
+ senderPlayerName: string;
37
+ senderPlayerEmail: string;
38
+ senderPlayerPhone: string;
39
+ receiverPlayerEmail: string;
40
+ receiverPlayerPhone: string;
41
+ receivingGameId: string | number;
42
+ amount: string | number;
43
+ }
44
+ interface InitiateTransferInput {
45
+ clientRequestId: string;
46
+ sourcePlayerName: string;
47
+ sourcePlayerEmail: string;
48
+ sourcePlayerPhone: string;
49
+ targetPlayerEmail: string;
50
+ targetPlayerPhone: string;
51
+ targetGameId: string | number;
52
+ amount: string | number;
53
+ }
54
+ interface InitiateResult {
55
+ transactionId: string;
56
+ /**
57
+ * "in_app" → proceed to the passkey approve flow; "sms" → un-enrolled fallback.
58
+ * `undefined` when the backend returned the minor/guardian path (HTTP 202) and
59
+ * no verification method applies yet — check `guardianApproval` and do NOT route
60
+ * the user into the SMS-PIN UI in that case (§4.3 / §8).
61
+ */
62
+ verificationMethod: VerificationMethod | undefined;
63
+ /** Present on the minor/guardian path (HTTP 202): the backend's guardian_approval block. */
64
+ guardianApproval?: unknown;
65
+ /** Full raw backend body (fees preview, order id, new balance, guardian block, …). */
66
+ raw: Record<string, unknown>;
67
+ }
68
+ interface CreateCheckoutInput {
69
+ playerEmail: string;
70
+ usdAmount: string | number;
71
+ /** Which rail the hosted page presents. Default "platform" (card). The page
72
+ * renders the right experience per rail — all branded as INVO. */
73
+ rail?: Rail;
74
+ successUrl?: string;
75
+ cancelUrl?: string;
76
+ metadata?: Record<string, unknown>;
77
+ }
78
+ interface CreateCheckoutResult {
79
+ sessionId: string;
80
+ /** Send the browser here; INVO's hosted page handles the processor + 3-D Secure. */
81
+ checkoutUrl: string;
82
+ expiresAt: string;
83
+ raw: Record<string, unknown>;
84
+ }
85
+ interface PurchaseInput {
86
+ playerEmail: string;
87
+ usdAmount: string | number;
88
+ /** Idempotency key. Required. */
89
+ purchaseReference: string;
90
+ rail?: Rail;
91
+ /** Required for the `platform` rail: a tokenized payment method. */
92
+ paymentMethodId?: string;
93
+ savedCardId?: string;
94
+ playerName?: string;
95
+ playerPhone?: string;
96
+ }
97
+ type PurchaseStatus = "success" | "requires_action" | "pending_payment";
98
+ interface PurchaseResult {
99
+ status: PurchaseStatus;
100
+ /** Present when status === "requires_action" (client-side 3-D Secure step). */
101
+ clientSecret?: string;
102
+ paymentIntentId?: string;
103
+ /** Present when status === "pending_payment" (redirect the browser here). */
104
+ paymentUrl?: string;
105
+ transactionId?: string;
106
+ orderId?: string;
107
+ newBalance?: string | number | null;
108
+ raw: Record<string, unknown>;
109
+ }
110
+ interface ApproveResult {
111
+ status: string;
112
+ next: string;
113
+ transactionId: string;
114
+ /** transfer-approve returns the sender's claim code; send-approve does not. */
115
+ claimCode?: string;
116
+ claimCodeExpiresAt?: string;
117
+ raw: Record<string, unknown>;
118
+ }
119
+ interface ConfirmReceiptResult {
120
+ status: string;
121
+ raw: Record<string, unknown>;
122
+ }
123
+ interface LinkDeviceResult {
124
+ /** "authorized" on success — a single-use grant that register/complete consumes. */
125
+ status: string;
126
+ raw: Record<string, unknown>;
127
+ }
128
+
129
+ /**
130
+ * Typed error for every INVO backend failure.
131
+ *
132
+ * The backend error envelope is usually `{ "error": <message>, "code": <STABLE_CODE> }`,
133
+ * but a few txn-state/claim errors carry only `error` (no `code`) and some use
134
+ * `{ "status": "error", "message": ... }`. So `code` may be undefined — branch on
135
+ * `code` when present, else fall back to `message`. See the handoff doc §8.
136
+ */
137
+ declare class InvoError extends Error {
138
+ /** Stable machine code from the backend (`code`), when present. */
139
+ readonly code: string | undefined;
140
+ /** HTTP status. */
141
+ readonly status: number;
142
+ /** Raw parsed body, for debugging / fields not surfaced here. */
143
+ readonly body: unknown;
144
+ constructor(args: {
145
+ message: string;
146
+ code?: string;
147
+ status: number;
148
+ body?: unknown;
149
+ });
150
+ /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
151
+ get isReceiverNotEnrolled(): boolean;
152
+ /** True if the session/SDK token has expired and the caller should re-mint + retry. */
153
+ get isTokenExpired(): boolean;
154
+ }
155
+
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 };
@@ -0,0 +1,156 @@
1
+ /** Public, neutral payment-rail names. Provider/processor names are an internal
2
+ * backend routing detail and are deliberately NOT exposed here. */
3
+ type Rail = "platform" | "game" | "steam";
4
+ /** Verification method returned by initiate-send / initiate-transfer. */
5
+ type VerificationMethod = "in_app" | "sms";
6
+ interface InvoConfig {
7
+ /** API base URL. Prod: "https://invo.network". Sandbox: "https://sandbox.invo.network/sandbox". */
8
+ baseUrl: string;
9
+ /** Per-request timeout (ms). Default 30000. */
10
+ timeoutMs?: number;
11
+ /** Optional fetch override (e.g. a custom agent). Defaults to global fetch. */
12
+ fetch?: typeof fetch;
13
+ }
14
+ interface ServerConfig extends InvoConfig {
15
+ /** The game secret. Server-side ONLY — never ship this to a browser. */
16
+ gameSecret: string;
17
+ }
18
+ interface ClientConfig extends InvoConfig {
19
+ /** Short-lived, game-scoped player token minted server-side via mintPlayerToken(). */
20
+ token: string;
21
+ /**
22
+ * Optional. Called when a request fails with `SDK_TOKEN_EXPIRED` (token TTL is
23
+ * ~15 min, §11.2). Return a freshly minted token — typically by calling your own
24
+ * backend, which re-mints server-side with the game secret. The failed request
25
+ * is retried once with the new token, and the client keeps using it thereafter.
26
+ */
27
+ refreshToken?: () => Promise<string> | string;
28
+ }
29
+ interface PlayerToken {
30
+ token: string;
31
+ expiresAt: string;
32
+ identityId: string;
33
+ }
34
+ interface InitiateSendInput {
35
+ clientRequestId: string;
36
+ senderPlayerName: string;
37
+ senderPlayerEmail: string;
38
+ senderPlayerPhone: string;
39
+ receiverPlayerEmail: string;
40
+ receiverPlayerPhone: string;
41
+ receivingGameId: string | number;
42
+ amount: string | number;
43
+ }
44
+ interface InitiateTransferInput {
45
+ clientRequestId: string;
46
+ sourcePlayerName: string;
47
+ sourcePlayerEmail: string;
48
+ sourcePlayerPhone: string;
49
+ targetPlayerEmail: string;
50
+ targetPlayerPhone: string;
51
+ targetGameId: string | number;
52
+ amount: string | number;
53
+ }
54
+ interface InitiateResult {
55
+ transactionId: string;
56
+ /**
57
+ * "in_app" → proceed to the passkey approve flow; "sms" → un-enrolled fallback.
58
+ * `undefined` when the backend returned the minor/guardian path (HTTP 202) and
59
+ * no verification method applies yet — check `guardianApproval` and do NOT route
60
+ * the user into the SMS-PIN UI in that case (§4.3 / §8).
61
+ */
62
+ verificationMethod: VerificationMethod | undefined;
63
+ /** Present on the minor/guardian path (HTTP 202): the backend's guardian_approval block. */
64
+ guardianApproval?: unknown;
65
+ /** Full raw backend body (fees preview, order id, new balance, guardian block, …). */
66
+ raw: Record<string, unknown>;
67
+ }
68
+ interface CreateCheckoutInput {
69
+ playerEmail: string;
70
+ usdAmount: string | number;
71
+ /** Which rail the hosted page presents. Default "platform" (card). The page
72
+ * renders the right experience per rail — all branded as INVO. */
73
+ rail?: Rail;
74
+ successUrl?: string;
75
+ cancelUrl?: string;
76
+ metadata?: Record<string, unknown>;
77
+ }
78
+ interface CreateCheckoutResult {
79
+ sessionId: string;
80
+ /** Send the browser here; INVO's hosted page handles the processor + 3-D Secure. */
81
+ checkoutUrl: string;
82
+ expiresAt: string;
83
+ raw: Record<string, unknown>;
84
+ }
85
+ interface PurchaseInput {
86
+ playerEmail: string;
87
+ usdAmount: string | number;
88
+ /** Idempotency key. Required. */
89
+ purchaseReference: string;
90
+ rail?: Rail;
91
+ /** Required for the `platform` rail: a tokenized payment method. */
92
+ paymentMethodId?: string;
93
+ savedCardId?: string;
94
+ playerName?: string;
95
+ playerPhone?: string;
96
+ }
97
+ type PurchaseStatus = "success" | "requires_action" | "pending_payment";
98
+ interface PurchaseResult {
99
+ status: PurchaseStatus;
100
+ /** Present when status === "requires_action" (client-side 3-D Secure step). */
101
+ clientSecret?: string;
102
+ paymentIntentId?: string;
103
+ /** Present when status === "pending_payment" (redirect the browser here). */
104
+ paymentUrl?: string;
105
+ transactionId?: string;
106
+ orderId?: string;
107
+ newBalance?: string | number | null;
108
+ raw: Record<string, unknown>;
109
+ }
110
+ interface ApproveResult {
111
+ status: string;
112
+ next: string;
113
+ transactionId: string;
114
+ /** transfer-approve returns the sender's claim code; send-approve does not. */
115
+ claimCode?: string;
116
+ claimCodeExpiresAt?: string;
117
+ raw: Record<string, unknown>;
118
+ }
119
+ interface ConfirmReceiptResult {
120
+ status: string;
121
+ raw: Record<string, unknown>;
122
+ }
123
+ interface LinkDeviceResult {
124
+ /** "authorized" on success — a single-use grant that register/complete consumes. */
125
+ status: string;
126
+ raw: Record<string, unknown>;
127
+ }
128
+
129
+ /**
130
+ * Typed error for every INVO backend failure.
131
+ *
132
+ * The backend error envelope is usually `{ "error": <message>, "code": <STABLE_CODE> }`,
133
+ * but a few txn-state/claim errors carry only `error` (no `code`) and some use
134
+ * `{ "status": "error", "message": ... }`. So `code` may be undefined — branch on
135
+ * `code` when present, else fall back to `message`. See the handoff doc §8.
136
+ */
137
+ declare class InvoError extends Error {
138
+ /** Stable machine code from the backend (`code`), when present. */
139
+ readonly code: string | undefined;
140
+ /** HTTP status. */
141
+ readonly status: number;
142
+ /** Raw parsed body, for debugging / fields not surfaced here. */
143
+ readonly body: unknown;
144
+ constructor(args: {
145
+ message: string;
146
+ code?: string;
147
+ status: number;
148
+ body?: unknown;
149
+ });
150
+ /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
151
+ get isReceiverNotEnrolled(): boolean;
152
+ /** True if the session/SDK token has expired and the caller should re-mint + retry. */
153
+ get isTokenExpired(): boolean;
154
+ }
155
+
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 };