@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/dist/index.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { assertSecureBaseUrl, Http, InvoError } from './chunk-KUQVVH2P.js';
|
|
2
|
+
export { InvoError } from './chunk-KUQVVH2P.js';
|
|
3
|
+
|
|
4
|
+
// src/shared/webauthn.ts
|
|
5
|
+
function b64urlToBuffer(value) {
|
|
6
|
+
const pad = value.length % 4 === 0 ? "" : "=".repeat(4 - value.length % 4);
|
|
7
|
+
const b64 = value.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
8
|
+
const bin = atob(b64);
|
|
9
|
+
const bytes = new Uint8Array(bin.length);
|
|
10
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
11
|
+
return bytes.buffer;
|
|
12
|
+
}
|
|
13
|
+
function bufferToB64url(buf) {
|
|
14
|
+
const bytes = new Uint8Array(buf);
|
|
15
|
+
let bin = "";
|
|
16
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
17
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
18
|
+
}
|
|
19
|
+
function descriptorsToBinary(list) {
|
|
20
|
+
return (list ?? []).map((d) => {
|
|
21
|
+
const out = {
|
|
22
|
+
id: b64urlToBuffer(d.id),
|
|
23
|
+
type: d.type
|
|
24
|
+
};
|
|
25
|
+
if (d.transports) out.transports = d.transports;
|
|
26
|
+
return out;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function toCreationOptions(json) {
|
|
30
|
+
const opts = {
|
|
31
|
+
...json,
|
|
32
|
+
challenge: b64urlToBuffer(json["challenge"]),
|
|
33
|
+
user: {
|
|
34
|
+
...json["user"],
|
|
35
|
+
id: b64urlToBuffer(json["user"].id)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
if (json["excludeCredentials"]) {
|
|
39
|
+
opts.excludeCredentials = descriptorsToBinary(json["excludeCredentials"]);
|
|
40
|
+
}
|
|
41
|
+
return opts;
|
|
42
|
+
}
|
|
43
|
+
function toRequestOptions(json) {
|
|
44
|
+
const opts = {
|
|
45
|
+
...json,
|
|
46
|
+
challenge: b64urlToBuffer(json["challenge"])
|
|
47
|
+
};
|
|
48
|
+
if (json["allowCredentials"]) {
|
|
49
|
+
opts.allowCredentials = descriptorsToBinary(json["allowCredentials"]);
|
|
50
|
+
}
|
|
51
|
+
return opts;
|
|
52
|
+
}
|
|
53
|
+
function registrationToJSON(cred) {
|
|
54
|
+
const r = cred.response;
|
|
55
|
+
return {
|
|
56
|
+
id: cred.id,
|
|
57
|
+
rawId: bufferToB64url(cred.rawId),
|
|
58
|
+
type: cred.type,
|
|
59
|
+
authenticatorAttachment: cred.authenticatorAttachment ?? void 0,
|
|
60
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
61
|
+
response: {
|
|
62
|
+
clientDataJSON: bufferToB64url(r.clientDataJSON),
|
|
63
|
+
attestationObject: bufferToB64url(r.attestationObject),
|
|
64
|
+
transports: typeof r.getTransports === "function" ? r.getTransports() : void 0
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function assertionToJSON(cred) {
|
|
69
|
+
const r = cred.response;
|
|
70
|
+
return {
|
|
71
|
+
id: cred.id,
|
|
72
|
+
rawId: bufferToB64url(cred.rawId),
|
|
73
|
+
type: cred.type,
|
|
74
|
+
authenticatorAttachment: cred.authenticatorAttachment ?? void 0,
|
|
75
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
76
|
+
response: {
|
|
77
|
+
clientDataJSON: bufferToB64url(r.clientDataJSON),
|
|
78
|
+
authenticatorData: bufferToB64url(r.authenticatorData),
|
|
79
|
+
signature: bufferToB64url(r.signature),
|
|
80
|
+
userHandle: r.userHandle ? bufferToB64url(r.userHandle) : null
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/index.ts
|
|
86
|
+
var InvoClient = class {
|
|
87
|
+
constructor(config) {
|
|
88
|
+
if (!config.token) throw new Error("InvoClient requires a player `token`.");
|
|
89
|
+
if (!config.baseUrl) throw new Error("InvoClient requires a `baseUrl`.");
|
|
90
|
+
assertSecureBaseUrl(config.baseUrl);
|
|
91
|
+
this.http = new Http({
|
|
92
|
+
baseUrl: config.baseUrl,
|
|
93
|
+
timeoutMs: config.timeoutMs,
|
|
94
|
+
fetchImpl: config.fetch
|
|
95
|
+
// Browser: do NOT set User-Agent (forbidden header); the browser's own UA is fine.
|
|
96
|
+
});
|
|
97
|
+
this.auth = { kind: "bearer", token: config.token };
|
|
98
|
+
this.refreshToken = config.refreshToken;
|
|
99
|
+
}
|
|
100
|
+
assertWebAuthn() {
|
|
101
|
+
if (typeof navigator === "undefined" || !navigator.credentials) {
|
|
102
|
+
throw new Error("WebAuthn is not available in this environment (browser required).");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Enroll a passkey for the token's identity (register/begin -> create() -> register/complete). */
|
|
106
|
+
async enrollPasskey() {
|
|
107
|
+
this.assertWebAuthn();
|
|
108
|
+
return this.withTokenRetry(async () => {
|
|
109
|
+
const options = await this.post(
|
|
110
|
+
"/api/sdk/webauthn/register/begin"
|
|
111
|
+
);
|
|
112
|
+
const cred = await navigator.credentials.create({
|
|
113
|
+
publicKey: toCreationOptions(options)
|
|
114
|
+
});
|
|
115
|
+
if (!cred) throw new Error("Passkey creation was cancelled or returned no credential.");
|
|
116
|
+
const raw = await this.post(
|
|
117
|
+
"/api/sdk/webauthn/register/complete",
|
|
118
|
+
{ credential: registrationToJSON(cred) }
|
|
119
|
+
);
|
|
120
|
+
return { status: String(raw["status"] ?? ""), device: raw["device"] ?? null, raw };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Approve a SEND with the player's passkey. */
|
|
124
|
+
async approveSend(transactionId) {
|
|
125
|
+
return this.approve("send", transactionId);
|
|
126
|
+
}
|
|
127
|
+
/** Approve a TRANSFER with the player's passkey (returns the sender's claim code). */
|
|
128
|
+
async approveTransfer(transactionId) {
|
|
129
|
+
return this.approve("transfers", transactionId);
|
|
130
|
+
}
|
|
131
|
+
/** Recipient self-claims a SEND with their passkey. */
|
|
132
|
+
async confirmReceiptSend(transactionId) {
|
|
133
|
+
return this.confirmReceipt("send", transactionId);
|
|
134
|
+
}
|
|
135
|
+
/** Recipient self-claims a TRANSFER with their passkey. */
|
|
136
|
+
async confirmReceiptTransfer(transactionId) {
|
|
137
|
+
return this.confirmReceipt("transfers", transactionId);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Interchangeable methods (§4.6): prove an *already-enrolled* method (e.g. the
|
|
141
|
+
* INVO app device key) to authorize adding a new partner passkey. The returned
|
|
142
|
+
* single-use grant lets a subsequent enrollPasskey() succeed instead of being
|
|
143
|
+
* blocked with ENROLLMENT_REQUIRES_PROOF.
|
|
144
|
+
*
|
|
145
|
+
* begin -> navigator.credentials.get() -> complete with { link_id, webauthn_assertion }.
|
|
146
|
+
*/
|
|
147
|
+
async linkDevice(linkId) {
|
|
148
|
+
if (!linkId) throw new Error("linkDevice requires a `linkId`.");
|
|
149
|
+
return this.withTokenRetry(async () => {
|
|
150
|
+
const assertion = await this.runAssertion("/api/sdk/device/link/webauthn/begin", {
|
|
151
|
+
link_id: linkId
|
|
152
|
+
});
|
|
153
|
+
const raw = await this.post(
|
|
154
|
+
"/api/sdk/device/link/webauthn/complete",
|
|
155
|
+
{ link_id: linkId, webauthn_assertion: assertion }
|
|
156
|
+
);
|
|
157
|
+
return { status: String(raw["status"] ?? ""), raw };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// --- internals ---
|
|
161
|
+
/** POST with the current player token. Token-expiry retry is handled one level
|
|
162
|
+
* up by withTokenRetry (which re-runs the whole ceremony, not a single call). */
|
|
163
|
+
async post(path, body) {
|
|
164
|
+
return this.http.post(path, body, this.auth);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Run a whole flow, retrying it ONCE if any call fails with SDK_TOKEN_EXPIRED
|
|
168
|
+
* and a `refreshToken` hook is configured. We re-run the entire begin→get→
|
|
169
|
+
* complete ceremony (not just the failed call) so the retry always uses a fresh,
|
|
170
|
+
* unconsumed challenge — never replaying a single-use WebAuthn assertion (§4.4).
|
|
171
|
+
* The refresh is single-flighted so concurrent expiries trigger one re-mint.
|
|
172
|
+
*/
|
|
173
|
+
async withTokenRetry(run) {
|
|
174
|
+
try {
|
|
175
|
+
return await run();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof InvoError && err.isTokenExpired && await this.tryRefresh()) {
|
|
178
|
+
return await run();
|
|
179
|
+
}
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async tryRefresh() {
|
|
184
|
+
if (!this.refreshToken) return false;
|
|
185
|
+
if (!this.refreshInFlight) {
|
|
186
|
+
this.refreshInFlight = Promise.resolve(this.refreshToken()).finally(() => {
|
|
187
|
+
this.refreshInFlight = void 0;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const fresh = await this.refreshInFlight;
|
|
191
|
+
if (!fresh) return false;
|
|
192
|
+
this.auth = { kind: "bearer", token: fresh };
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
async runAssertion(beginPath, beginBody) {
|
|
196
|
+
this.assertWebAuthn();
|
|
197
|
+
const options = await this.post(beginPath, beginBody);
|
|
198
|
+
const cred = await navigator.credentials.get({
|
|
199
|
+
publicKey: toRequestOptions(options)
|
|
200
|
+
});
|
|
201
|
+
if (!cred) throw new Error("Passkey assertion was cancelled or returned no credential.");
|
|
202
|
+
return assertionToJSON(cred);
|
|
203
|
+
}
|
|
204
|
+
async approve(flow, transactionId) {
|
|
205
|
+
const id = encodeURIComponent(transactionId);
|
|
206
|
+
return this.withTokenRetry(async () => {
|
|
207
|
+
const assertion = await this.runAssertion(`/api/sdk/${flow}/${id}/approve/webauthn/begin`);
|
|
208
|
+
const raw = await this.post(
|
|
209
|
+
`/api/sdk/${flow}/${id}/approve`,
|
|
210
|
+
{ webauthn_assertion: assertion }
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
status: String(raw["status"] ?? ""),
|
|
214
|
+
next: String(raw["next"] ?? ""),
|
|
215
|
+
transactionId: String(raw["transaction_id"] ?? transactionId),
|
|
216
|
+
claimCode: raw["claim_code"],
|
|
217
|
+
claimCodeExpiresAt: raw["claim_code_expires_at"],
|
|
218
|
+
raw
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async confirmReceipt(flow, transactionId) {
|
|
223
|
+
const id = encodeURIComponent(transactionId);
|
|
224
|
+
return this.withTokenRetry(async () => {
|
|
225
|
+
const assertion = await this.runAssertion(
|
|
226
|
+
`/api/sdk/${flow}/${id}/confirm-receipt/webauthn/begin`
|
|
227
|
+
);
|
|
228
|
+
const raw = await this.post(
|
|
229
|
+
`/api/sdk/${flow}/${id}/confirm-receipt`,
|
|
230
|
+
{ webauthn_assertion: assertion }
|
|
231
|
+
);
|
|
232
|
+
return { status: String(raw["status"] ?? ""), raw };
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export { InvoClient };
|
|
238
|
+
//# sourceMappingURL=index.js.map
|
|
239
|
+
//# sourceMappingURL=index.js.map
|
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/shared/errors.ts
|
|
4
|
+
var InvoError = class _InvoError extends Error {
|
|
5
|
+
constructor(args) {
|
|
6
|
+
super(args.message);
|
|
7
|
+
this.name = "InvoError";
|
|
8
|
+
this.code = args.code;
|
|
9
|
+
this.status = args.status;
|
|
10
|
+
this.body = args.body ?? null;
|
|
11
|
+
Object.setPrototypeOf(this, _InvoError.prototype);
|
|
12
|
+
}
|
|
13
|
+
/** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
|
|
14
|
+
get isReceiverNotEnrolled() {
|
|
15
|
+
return this.code === "receiver_not_enrolled_use_claim_code" || /receiver_not_enrolled_use_claim_code/i.test(this.message);
|
|
16
|
+
}
|
|
17
|
+
/** True if the session/SDK token has expired and the caller should re-mint + retry. */
|
|
18
|
+
get isTokenExpired() {
|
|
19
|
+
return this.code === "SDK_TOKEN_EXPIRED";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function errorFromResponse(status, body) {
|
|
23
|
+
let message = `INVO request failed (HTTP ${status})`;
|
|
24
|
+
let code;
|
|
25
|
+
if (body && typeof body === "object") {
|
|
26
|
+
const b = body;
|
|
27
|
+
if (typeof b["code"] === "string") code = b["code"];
|
|
28
|
+
if (typeof b["error"] === "string") message = b["error"];
|
|
29
|
+
else if (typeof b["message"] === "string") message = b["message"];
|
|
30
|
+
} else if (typeof body === "string" && body.trim()) {
|
|
31
|
+
message = body;
|
|
32
|
+
}
|
|
33
|
+
return new InvoError({ message, code, status, body });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/shared/http.ts
|
|
37
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
38
|
+
function assertSecureBaseUrl(baseUrl) {
|
|
39
|
+
let u;
|
|
40
|
+
try {
|
|
41
|
+
u = new URL(baseUrl);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(`Invalid baseUrl: ${baseUrl}`);
|
|
44
|
+
}
|
|
45
|
+
const isLocal = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
46
|
+
if (u.protocol === "https:" || u.protocol === "http:" && isLocal) return;
|
|
47
|
+
throw new Error(
|
|
48
|
+
`baseUrl must use https:// (got "${u.protocol}//"). Plaintext would expose the token/secret on the wire.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
var Http = class {
|
|
52
|
+
constructor(opts) {
|
|
53
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
54
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
55
|
+
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
56
|
+
if (typeof f !== "function") {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"No fetch implementation available. Use Node >=18, or pass `fetch` in the config."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
this.fetchImpl = f;
|
|
62
|
+
this.userAgent = opts.userAgent;
|
|
63
|
+
}
|
|
64
|
+
async post(path, body, auth) {
|
|
65
|
+
return this.request("POST", path, body, auth);
|
|
66
|
+
}
|
|
67
|
+
async get(path, auth) {
|
|
68
|
+
return this.request("GET", path, void 0, auth);
|
|
69
|
+
}
|
|
70
|
+
authHeaders(auth) {
|
|
71
|
+
switch (auth.kind) {
|
|
72
|
+
case "game-secret":
|
|
73
|
+
return { "X-Game-Secret-Key": auth.secret };
|
|
74
|
+
case "bearer":
|
|
75
|
+
return { Authorization: `Bearer ${auth.token}` };
|
|
76
|
+
case "none":
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async request(method, path, body, auth) {
|
|
81
|
+
const url = `${this.baseUrl}${path}`;
|
|
82
|
+
const headers = {
|
|
83
|
+
Accept: "application/json",
|
|
84
|
+
...this.authHeaders(auth)
|
|
85
|
+
};
|
|
86
|
+
if (this.userAgent) headers["User-Agent"] = this.userAgent;
|
|
87
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
90
|
+
let res;
|
|
91
|
+
try {
|
|
92
|
+
res = await this.fetchImpl(url, {
|
|
93
|
+
method,
|
|
94
|
+
headers,
|
|
95
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
96
|
+
signal: controller.signal
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw new InvoError({
|
|
100
|
+
message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
|
|
101
|
+
status: 0,
|
|
102
|
+
body: null
|
|
103
|
+
});
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
const text = await res.text();
|
|
108
|
+
let parsed = null;
|
|
109
|
+
if (text) {
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(text);
|
|
112
|
+
} catch {
|
|
113
|
+
parsed = text;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!res.ok) throw errorFromResponse(res.status, parsed);
|
|
117
|
+
return parsed ?? {};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/server.ts
|
|
122
|
+
var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
|
|
123
|
+
var MAX_USD_AMOUNT = 999.99;
|
|
124
|
+
function invalidAmount(usdAmount, why) {
|
|
125
|
+
return new InvoError({
|
|
126
|
+
message: `usdAmount ${why} (got ${JSON.stringify(usdAmount)}).`,
|
|
127
|
+
code: "INVALID_INPUT",
|
|
128
|
+
status: 0
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function assertUsdAmount(usdAmount) {
|
|
132
|
+
let n;
|
|
133
|
+
if (typeof usdAmount === "number") {
|
|
134
|
+
n = usdAmount;
|
|
135
|
+
if (Number.isFinite(n) && Math.abs(n * 100 - Math.round(n * 100)) > 1e-9) {
|
|
136
|
+
throw invalidAmount(usdAmount, "must have at most 2 decimal places");
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
if (!/^\d+(\.\d{1,2})?$/.test(usdAmount)) {
|
|
140
|
+
throw invalidAmount(usdAmount, "must be a plain decimal USD value with at most 2 places");
|
|
141
|
+
}
|
|
142
|
+
n = Number(usdAmount);
|
|
143
|
+
}
|
|
144
|
+
if (!Number.isFinite(n) || n <= 0 || n > MAX_USD_AMOUNT) {
|
|
145
|
+
throw invalidAmount(usdAmount, `must be > 0 and <= ${MAX_USD_AMOUNT}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function requireField(value, field, raw) {
|
|
149
|
+
const s = value == null ? "" : String(value);
|
|
150
|
+
if (!s) {
|
|
151
|
+
throw new InvoError({
|
|
152
|
+
message: `INVO response was missing the required \`${field}\` field.`,
|
|
153
|
+
code: "INVALID_RESPONSE",
|
|
154
|
+
status: 0,
|
|
155
|
+
body: raw
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return s;
|
|
159
|
+
}
|
|
160
|
+
var InvoServer = class {
|
|
161
|
+
constructor(config) {
|
|
162
|
+
if (!config.gameSecret) throw new Error("InvoServer requires a `gameSecret`.");
|
|
163
|
+
if (!config.baseUrl) throw new Error("InvoServer requires a `baseUrl`.");
|
|
164
|
+
assertSecureBaseUrl(config.baseUrl);
|
|
165
|
+
this.http = new Http({
|
|
166
|
+
baseUrl: config.baseUrl,
|
|
167
|
+
timeoutMs: config.timeoutMs,
|
|
168
|
+
fetchImpl: config.fetch,
|
|
169
|
+
userAgent: DEFAULT_UA
|
|
170
|
+
// must be a non-blocked UA (handoff doc §9)
|
|
171
|
+
});
|
|
172
|
+
this.auth = { kind: "game-secret", secret: config.gameSecret };
|
|
173
|
+
}
|
|
174
|
+
/** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
|
|
175
|
+
async mintPlayerToken(input) {
|
|
176
|
+
const raw = await this.http.post(
|
|
177
|
+
"/api/sdk/player-token",
|
|
178
|
+
{ player_email: input.playerEmail },
|
|
179
|
+
this.auth
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
token: requireField(raw["token"], "token", raw),
|
|
183
|
+
expiresAt: String(raw["expires_at"] ?? ""),
|
|
184
|
+
identityId: String(raw["identity_id"] ?? "")
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
|
|
188
|
+
async initiateSend(input) {
|
|
189
|
+
const raw = await this.http.post(
|
|
190
|
+
"/api/currency-sends/initiate-send",
|
|
191
|
+
{
|
|
192
|
+
client_request_id: input.clientRequestId,
|
|
193
|
+
sender_player_name: input.senderPlayerName,
|
|
194
|
+
sender_player_email: input.senderPlayerEmail,
|
|
195
|
+
sender_player_phone: input.senderPlayerPhone,
|
|
196
|
+
receiver_player_email: input.receiverPlayerEmail,
|
|
197
|
+
receiver_player_phone: input.receiverPlayerPhone,
|
|
198
|
+
receiving_game_id: input.receivingGameId,
|
|
199
|
+
amount: input.amount
|
|
200
|
+
},
|
|
201
|
+
this.auth
|
|
202
|
+
);
|
|
203
|
+
return this.toInitiateResult(raw);
|
|
204
|
+
}
|
|
205
|
+
/** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
|
|
206
|
+
async initiateTransfer(input) {
|
|
207
|
+
const raw = await this.http.post(
|
|
208
|
+
"/api/transfers/initiate-transfer",
|
|
209
|
+
{
|
|
210
|
+
client_request_id: input.clientRequestId,
|
|
211
|
+
source_player_name: input.sourcePlayerName,
|
|
212
|
+
source_player_email: input.sourcePlayerEmail,
|
|
213
|
+
source_player_phone: input.sourcePlayerPhone,
|
|
214
|
+
target_player_email: input.targetPlayerEmail,
|
|
215
|
+
target_player_phone: input.targetPlayerPhone,
|
|
216
|
+
target_game_id: input.targetGameId,
|
|
217
|
+
amount: input.amount
|
|
218
|
+
},
|
|
219
|
+
this.auth
|
|
220
|
+
);
|
|
221
|
+
return this.toInitiateResult(raw);
|
|
222
|
+
}
|
|
223
|
+
/** Create a hosted checkout session (the recommended purchase path). Open the
|
|
224
|
+
* returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
|
|
225
|
+
* handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
|
|
226
|
+
* purchase.completed webhook. */
|
|
227
|
+
async createCheckout(input) {
|
|
228
|
+
assertUsdAmount(input.usdAmount);
|
|
229
|
+
const body = {
|
|
230
|
+
player_email: input.playerEmail,
|
|
231
|
+
usd_amount: input.usdAmount
|
|
232
|
+
};
|
|
233
|
+
if (input.rail) body["rail"] = input.rail;
|
|
234
|
+
if (input.successUrl) body["success_url"] = input.successUrl;
|
|
235
|
+
if (input.cancelUrl) body["cancel_url"] = input.cancelUrl;
|
|
236
|
+
if (input.metadata) body["metadata"] = input.metadata;
|
|
237
|
+
const raw = await this.http.post(
|
|
238
|
+
"/api/checkout/sessions",
|
|
239
|
+
body,
|
|
240
|
+
this.auth
|
|
241
|
+
);
|
|
242
|
+
return {
|
|
243
|
+
sessionId: String(raw["session_id"] ?? ""),
|
|
244
|
+
checkoutUrl: requireField(raw["checkout_url"], "checkout_url", raw),
|
|
245
|
+
expiresAt: String(raw["expires_at"] ?? ""),
|
|
246
|
+
raw
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Direct purchase via the rail selector. Use when you need a specific rail.
|
|
251
|
+
* - rail "platform" (default): standard card. May return status "requires_action"
|
|
252
|
+
* (run client-side 3-D Secure with clientSecret, then call confirmPayment).
|
|
253
|
+
* - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
|
|
254
|
+
* - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
|
|
255
|
+
*/
|
|
256
|
+
async purchaseCurrency(input) {
|
|
257
|
+
assertUsdAmount(input.usdAmount);
|
|
258
|
+
if (!input.purchaseReference) {
|
|
259
|
+
throw new InvoError({
|
|
260
|
+
message: "purchaseReference is required (idempotency key for the purchase).",
|
|
261
|
+
code: "MISSING_PURCHASE_REFERENCE",
|
|
262
|
+
status: 0
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (input.rail === "steam") {
|
|
266
|
+
throw new InvoError({
|
|
267
|
+
message: 'rail "steam" is not a /purchase-currency flow \u2014 Steam uses the dedicated in-client Steam endpoints.',
|
|
268
|
+
code: "WRONG_RAIL_ENDPOINT",
|
|
269
|
+
status: 0
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const body = {
|
|
273
|
+
player_email: input.playerEmail,
|
|
274
|
+
usd_amount: input.usdAmount,
|
|
275
|
+
purchase_reference: input.purchaseReference
|
|
276
|
+
};
|
|
277
|
+
if (input.rail) body["rail"] = input.rail;
|
|
278
|
+
if (input.paymentMethodId) body["payment_method_id"] = input.paymentMethodId;
|
|
279
|
+
if (input.savedCardId) body["saved_card_id"] = input.savedCardId;
|
|
280
|
+
if (input.playerName) body["player_name"] = input.playerName;
|
|
281
|
+
if (input.playerPhone) body["player_phone"] = input.playerPhone;
|
|
282
|
+
const raw = await this.http.post(
|
|
283
|
+
"/api/currency-purchases/purchase-currency",
|
|
284
|
+
body,
|
|
285
|
+
this.auth
|
|
286
|
+
);
|
|
287
|
+
return {
|
|
288
|
+
status: String(raw["status"] ?? ""),
|
|
289
|
+
clientSecret: raw["client_secret"],
|
|
290
|
+
paymentIntentId: raw["payment_intent_id"],
|
|
291
|
+
paymentUrl: raw["payment_url"],
|
|
292
|
+
transactionId: raw["transaction_id"],
|
|
293
|
+
orderId: raw["order_id"],
|
|
294
|
+
newBalance: raw["new_balance"] ?? null,
|
|
295
|
+
raw
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
|
|
299
|
+
async confirmPayment(input) {
|
|
300
|
+
const body = { payment_intent_id: input.paymentIntentId };
|
|
301
|
+
if (input.orderId) body["order_id"] = input.orderId;
|
|
302
|
+
return this.http.post(
|
|
303
|
+
"/api/currency-purchases/confirm-payment",
|
|
304
|
+
body,
|
|
305
|
+
this.auth
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
/** Fetch purchase status (order + financial summary + timeline). */
|
|
309
|
+
async getOrderDetails(query) {
|
|
310
|
+
if (!query.orderId && !query.transactionId) {
|
|
311
|
+
throw new InvoError({
|
|
312
|
+
message: "getOrderDetails requires an `orderId` or `transactionId`.",
|
|
313
|
+
code: "INVALID_INPUT",
|
|
314
|
+
status: 0
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const q = new URLSearchParams();
|
|
318
|
+
if (query.orderId) q.set("order_id", query.orderId);
|
|
319
|
+
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
320
|
+
return this.http.get(
|
|
321
|
+
`/api/currency-purchases/order-details?${q.toString()}`,
|
|
322
|
+
this.auth
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
toInitiateResult(raw) {
|
|
326
|
+
const vm = raw["verification_method"];
|
|
327
|
+
const guardian = raw["guardian_approval"];
|
|
328
|
+
return {
|
|
329
|
+
transactionId: String(raw["transaction_id"] ?? ""),
|
|
330
|
+
verificationMethod: vm === "in_app" || vm === "sms" ? vm : void 0,
|
|
331
|
+
guardianApproval: guardian ?? void 0,
|
|
332
|
+
raw
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
exports.InvoError = InvoError;
|
|
338
|
+
exports.InvoServer = InvoServer;
|
|
339
|
+
//# sourceMappingURL=server.cjs.map
|
|
340
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1,42 @@
|
|
|
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-B7rVID2r.cjs';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-B7rVID2r.cjs';
|
|
3
|
+
|
|
4
|
+
declare class InvoServer {
|
|
5
|
+
private readonly http;
|
|
6
|
+
private readonly auth;
|
|
7
|
+
constructor(config: ServerConfig);
|
|
8
|
+
/** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
|
|
9
|
+
mintPlayerToken(input: {
|
|
10
|
+
playerEmail: string;
|
|
11
|
+
}): Promise<PlayerToken>;
|
|
12
|
+
/** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
|
|
13
|
+
initiateSend(input: InitiateSendInput): Promise<InitiateResult>;
|
|
14
|
+
/** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
|
|
15
|
+
initiateTransfer(input: InitiateTransferInput): Promise<InitiateResult>;
|
|
16
|
+
/** Create a hosted checkout session (the recommended purchase path). Open the
|
|
17
|
+
* returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
|
|
18
|
+
* handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
|
|
19
|
+
* purchase.completed webhook. */
|
|
20
|
+
createCheckout(input: CreateCheckoutInput): Promise<CreateCheckoutResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Direct purchase via the rail selector. Use when you need a specific rail.
|
|
23
|
+
* - rail "platform" (default): standard card. May return status "requires_action"
|
|
24
|
+
* (run client-side 3-D Secure with clientSecret, then call confirmPayment).
|
|
25
|
+
* - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
|
|
26
|
+
* - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
|
|
27
|
+
*/
|
|
28
|
+
purchaseCurrency(input: PurchaseInput): Promise<PurchaseResult>;
|
|
29
|
+
/** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
|
|
30
|
+
confirmPayment(input: {
|
|
31
|
+
paymentIntentId: string;
|
|
32
|
+
orderId?: string;
|
|
33
|
+
}): Promise<Record<string, unknown>>;
|
|
34
|
+
/** Fetch purchase status (order + financial summary + timeline). */
|
|
35
|
+
getOrderDetails(query: {
|
|
36
|
+
orderId?: string;
|
|
37
|
+
transactionId?: string;
|
|
38
|
+
}): Promise<Record<string, unknown>>;
|
|
39
|
+
private toInitiateResult;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, PlayerToken, PurchaseInput, PurchaseResult, ServerConfig };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
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-B7rVID2r.js';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-B7rVID2r.js';
|
|
3
|
+
|
|
4
|
+
declare class InvoServer {
|
|
5
|
+
private readonly http;
|
|
6
|
+
private readonly auth;
|
|
7
|
+
constructor(config: ServerConfig);
|
|
8
|
+
/** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
|
|
9
|
+
mintPlayerToken(input: {
|
|
10
|
+
playerEmail: string;
|
|
11
|
+
}): Promise<PlayerToken>;
|
|
12
|
+
/** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
|
|
13
|
+
initiateSend(input: InitiateSendInput): Promise<InitiateResult>;
|
|
14
|
+
/** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
|
|
15
|
+
initiateTransfer(input: InitiateTransferInput): Promise<InitiateResult>;
|
|
16
|
+
/** Create a hosted checkout session (the recommended purchase path). Open the
|
|
17
|
+
* returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
|
|
18
|
+
* handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
|
|
19
|
+
* purchase.completed webhook. */
|
|
20
|
+
createCheckout(input: CreateCheckoutInput): Promise<CreateCheckoutResult>;
|
|
21
|
+
/**
|
|
22
|
+
* Direct purchase via the rail selector. Use when you need a specific rail.
|
|
23
|
+
* - rail "platform" (default): standard card. May return status "requires_action"
|
|
24
|
+
* (run client-side 3-D Secure with clientSecret, then call confirmPayment).
|
|
25
|
+
* - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
|
|
26
|
+
* - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
|
|
27
|
+
*/
|
|
28
|
+
purchaseCurrency(input: PurchaseInput): Promise<PurchaseResult>;
|
|
29
|
+
/** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
|
|
30
|
+
confirmPayment(input: {
|
|
31
|
+
paymentIntentId: string;
|
|
32
|
+
orderId?: string;
|
|
33
|
+
}): Promise<Record<string, unknown>>;
|
|
34
|
+
/** Fetch purchase status (order + financial summary + timeline). */
|
|
35
|
+
getOrderDetails(query: {
|
|
36
|
+
orderId?: string;
|
|
37
|
+
transactionId?: string;
|
|
38
|
+
}): Promise<Record<string, unknown>>;
|
|
39
|
+
private toInitiateResult;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, PlayerToken, PurchaseInput, PurchaseResult, ServerConfig };
|