@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 +39 -0
- package/LICENSE +17 -0
- package/README.md +142 -0
- package/dist/chunk-KUQVVH2P.js +121 -0
- package/dist/errors-B7rVID2r.d.cts +156 -0
- package/dist/errors-B7rVID2r.d.ts +156 -0
- package/dist/index.cjs +357 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +239 -0
- package/dist/server.cjs +340 -0
- package/dist/server.d.cts +42 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.js +222 -0
- package/package.json +68 -0
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 };
|