@invonetwork/web-sdk 0.1.0 → 0.2.1
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 +69 -39
- package/README.md +328 -77
- package/dist/{chunk-KUQVVH2P.js → chunk-A44O4KC3.js} +28 -2
- package/dist/{errors-B7rVID2r.d.cts → errors-DV5QsftP.d.cts} +56 -1
- package/dist/{errors-B7rVID2r.d.ts → errors-DV5QsftP.d.ts} +56 -1
- package/dist/index.cjs +26 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/server.cjs +142 -12
- package/dist/server.d.cts +17 -3
- package/dist/server.d.ts +17 -3
- package/dist/server.js +118 -14
- package/package.json +68 -68
|
@@ -107,6 +107,47 @@ interface PurchaseResult {
|
|
|
107
107
|
newBalance?: string | number | null;
|
|
108
108
|
raw: Record<string, unknown>;
|
|
109
109
|
}
|
|
110
|
+
interface PurchaseItemInput {
|
|
111
|
+
/** Idempotency key, unique per game. Required. A duplicate returns 409. */
|
|
112
|
+
clientRequestId: string;
|
|
113
|
+
playerEmail: string;
|
|
114
|
+
playerName: string;
|
|
115
|
+
itemId: string;
|
|
116
|
+
itemName: string;
|
|
117
|
+
/** Integer quantity, 1..1000. */
|
|
118
|
+
itemQuantity: number;
|
|
119
|
+
/** Price PER UNIT, in game-currency units (not USD). > 0 and <= 999999.99. */
|
|
120
|
+
unitPrice: string | number;
|
|
121
|
+
/** Must equal unitPrice × itemQuantity (±0.01). > 0 and <= 999999.99. */
|
|
122
|
+
totalPrice: string | number;
|
|
123
|
+
playerPhone?: string;
|
|
124
|
+
itemDescription?: string;
|
|
125
|
+
itemCategory?: string;
|
|
126
|
+
}
|
|
127
|
+
interface PurchaseItemResult {
|
|
128
|
+
/** "success" on a completed spend. */
|
|
129
|
+
status: string;
|
|
130
|
+
transactionId: string;
|
|
131
|
+
orderId: string;
|
|
132
|
+
/** Canonical balance AFTER the spend (game-currency units). */
|
|
133
|
+
newBalance: string | number | null;
|
|
134
|
+
previousBalance: string | number | null;
|
|
135
|
+
currencyName: string;
|
|
136
|
+
/** { total_paid, developer_revenue, platform_fee } when present. */
|
|
137
|
+
financialBreakdown?: Record<string, unknown>;
|
|
138
|
+
raw: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
interface ItemHistoryQuery {
|
|
141
|
+
playerEmail: string;
|
|
142
|
+
limit?: number;
|
|
143
|
+
offset?: number;
|
|
144
|
+
}
|
|
145
|
+
/** Look up an item order by EXACTLY ONE of these. */
|
|
146
|
+
interface ItemOrderQuery {
|
|
147
|
+
orderId?: string;
|
|
148
|
+
transactionId?: string;
|
|
149
|
+
clientRequestId?: string;
|
|
150
|
+
}
|
|
110
151
|
interface ApproveResult {
|
|
111
152
|
status: string;
|
|
112
153
|
next: string;
|
|
@@ -151,6 +192,20 @@ declare class InvoError extends Error {
|
|
|
151
192
|
get isReceiverNotEnrolled(): boolean;
|
|
152
193
|
/** True if the session/SDK token has expired and the caller should re-mint + retry. */
|
|
153
194
|
get isTokenExpired(): boolean;
|
|
195
|
+
/**
|
|
196
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
197
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
198
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
199
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
200
|
+
*/
|
|
201
|
+
get isInsufficientBalance(): boolean;
|
|
202
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
203
|
+
get isDuplicateRequest(): boolean;
|
|
204
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
205
|
+
get retryAfter(): number | undefined;
|
|
206
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
207
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
208
|
+
private bodyObject;
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h };
|
|
211
|
+
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h, type PurchaseItemInput as i, type PurchaseItemResult as j, type ItemHistoryQuery as k, type ItemOrderQuery as l };
|
|
@@ -107,6 +107,47 @@ interface PurchaseResult {
|
|
|
107
107
|
newBalance?: string | number | null;
|
|
108
108
|
raw: Record<string, unknown>;
|
|
109
109
|
}
|
|
110
|
+
interface PurchaseItemInput {
|
|
111
|
+
/** Idempotency key, unique per game. Required. A duplicate returns 409. */
|
|
112
|
+
clientRequestId: string;
|
|
113
|
+
playerEmail: string;
|
|
114
|
+
playerName: string;
|
|
115
|
+
itemId: string;
|
|
116
|
+
itemName: string;
|
|
117
|
+
/** Integer quantity, 1..1000. */
|
|
118
|
+
itemQuantity: number;
|
|
119
|
+
/** Price PER UNIT, in game-currency units (not USD). > 0 and <= 999999.99. */
|
|
120
|
+
unitPrice: string | number;
|
|
121
|
+
/** Must equal unitPrice × itemQuantity (±0.01). > 0 and <= 999999.99. */
|
|
122
|
+
totalPrice: string | number;
|
|
123
|
+
playerPhone?: string;
|
|
124
|
+
itemDescription?: string;
|
|
125
|
+
itemCategory?: string;
|
|
126
|
+
}
|
|
127
|
+
interface PurchaseItemResult {
|
|
128
|
+
/** "success" on a completed spend. */
|
|
129
|
+
status: string;
|
|
130
|
+
transactionId: string;
|
|
131
|
+
orderId: string;
|
|
132
|
+
/** Canonical balance AFTER the spend (game-currency units). */
|
|
133
|
+
newBalance: string | number | null;
|
|
134
|
+
previousBalance: string | number | null;
|
|
135
|
+
currencyName: string;
|
|
136
|
+
/** { total_paid, developer_revenue, platform_fee } when present. */
|
|
137
|
+
financialBreakdown?: Record<string, unknown>;
|
|
138
|
+
raw: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
interface ItemHistoryQuery {
|
|
141
|
+
playerEmail: string;
|
|
142
|
+
limit?: number;
|
|
143
|
+
offset?: number;
|
|
144
|
+
}
|
|
145
|
+
/** Look up an item order by EXACTLY ONE of these. */
|
|
146
|
+
interface ItemOrderQuery {
|
|
147
|
+
orderId?: string;
|
|
148
|
+
transactionId?: string;
|
|
149
|
+
clientRequestId?: string;
|
|
150
|
+
}
|
|
110
151
|
interface ApproveResult {
|
|
111
152
|
status: string;
|
|
112
153
|
next: string;
|
|
@@ -151,6 +192,20 @@ declare class InvoError extends Error {
|
|
|
151
192
|
get isReceiverNotEnrolled(): boolean;
|
|
152
193
|
/** True if the session/SDK token has expired and the caller should re-mint + retry. */
|
|
153
194
|
get isTokenExpired(): boolean;
|
|
195
|
+
/**
|
|
196
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
197
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
198
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
199
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
200
|
+
*/
|
|
201
|
+
get isInsufficientBalance(): boolean;
|
|
202
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
203
|
+
get isDuplicateRequest(): boolean;
|
|
204
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
205
|
+
get retryAfter(): number | undefined;
|
|
206
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
207
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
208
|
+
private bodyObject;
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h };
|
|
211
|
+
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type ConfirmReceiptResult as a, type InitiateSendInput as b, type InitiateResult as c, type InitiateTransferInput as d, type CreateCheckoutInput as e, type CreateCheckoutResult as f, type PurchaseInput as g, type PurchaseResult as h, type PurchaseItemInput as i, type PurchaseItemResult as j, type ItemHistoryQuery as k, type ItemOrderQuery as l };
|
package/dist/index.cjs
CHANGED
|
@@ -18,6 +18,32 @@ var InvoError = class _InvoError extends Error {
|
|
|
18
18
|
get isTokenExpired() {
|
|
19
19
|
return this.code === "SDK_TOKEN_EXPIRED";
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
23
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
24
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
25
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
26
|
+
*/
|
|
27
|
+
get isInsufficientBalance() {
|
|
28
|
+
if (this.status !== 400) return false;
|
|
29
|
+
const b = this.bodyObject();
|
|
30
|
+
return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
|
|
31
|
+
}
|
|
32
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
33
|
+
get isDuplicateRequest() {
|
|
34
|
+
return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
|
|
35
|
+
}
|
|
36
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
37
|
+
get retryAfter() {
|
|
38
|
+
const v = this.bodyObject()["retry_after"];
|
|
39
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
40
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
41
|
+
}
|
|
42
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
43
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
44
|
+
bodyObject() {
|
|
45
|
+
return this.body && typeof this.body === "object" ? this.body : {};
|
|
46
|
+
}
|
|
21
47
|
};
|
|
22
48
|
function errorFromResponse(status, body) {
|
|
23
49
|
let message = `INVO request failed (HTTP ${status})`;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-DV5QsftP.cjs';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.cjs';
|
|
3
3
|
|
|
4
4
|
declare class InvoClient {
|
|
5
5
|
private readonly http;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './errors-DV5QsftP.js';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.js';
|
|
3
3
|
|
|
4
4
|
declare class InvoClient {
|
|
5
5
|
private readonly http;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assertSecureBaseUrl, Http, InvoError } from './chunk-
|
|
2
|
-
export { InvoError } from './chunk-
|
|
1
|
+
import { assertSecureBaseUrl, Http, InvoError } from './chunk-A44O4KC3.js';
|
|
2
|
+
export { InvoError } from './chunk-A44O4KC3.js';
|
|
3
3
|
|
|
4
4
|
// src/shared/webauthn.ts
|
|
5
5
|
function b64urlToBuffer(value) {
|
package/dist/server.cjs
CHANGED
|
@@ -18,6 +18,32 @@ var InvoError = class _InvoError extends Error {
|
|
|
18
18
|
get isTokenExpired() {
|
|
19
19
|
return this.code === "SDK_TOKEN_EXPIRED";
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
23
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
24
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
25
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
26
|
+
*/
|
|
27
|
+
get isInsufficientBalance() {
|
|
28
|
+
if (this.status !== 400) return false;
|
|
29
|
+
const b = this.bodyObject();
|
|
30
|
+
return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
|
|
31
|
+
}
|
|
32
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
33
|
+
get isDuplicateRequest() {
|
|
34
|
+
return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
|
|
35
|
+
}
|
|
36
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
37
|
+
get retryAfter() {
|
|
38
|
+
const v = this.bodyObject()["retry_after"];
|
|
39
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
40
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
41
|
+
}
|
|
42
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
43
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
44
|
+
bodyObject() {
|
|
45
|
+
return this.body && typeof this.body === "object" ? this.body : {};
|
|
46
|
+
}
|
|
21
47
|
};
|
|
22
48
|
function errorFromResponse(status, body) {
|
|
23
49
|
let message = `INVO request failed (HTTP ${status})`;
|
|
@@ -121,29 +147,35 @@ var Http = class {
|
|
|
121
147
|
// src/server.ts
|
|
122
148
|
var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
|
|
123
149
|
var MAX_USD_AMOUNT = 999.99;
|
|
124
|
-
|
|
150
|
+
var MAX_ITEM_PRICE = 999999.99;
|
|
151
|
+
function invalidInput(label, value, why) {
|
|
125
152
|
return new InvoError({
|
|
126
|
-
message:
|
|
153
|
+
message: `${label} ${why} (got ${JSON.stringify(value)}).`,
|
|
127
154
|
code: "INVALID_INPUT",
|
|
128
155
|
status: 0
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
|
-
|
|
158
|
+
var PLAIN_DECIMAL = /^\d+(\.\d{1,2})?$/;
|
|
159
|
+
function parseMoney(value, max, label) {
|
|
132
160
|
let n;
|
|
133
|
-
if (typeof
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw invalidAmount(usdAmount, "must have at most 2 decimal places");
|
|
161
|
+
if (typeof value === "number") {
|
|
162
|
+
if (!Number.isFinite(value) || !PLAIN_DECIMAL.test(String(value))) {
|
|
163
|
+
throw invalidInput(label, value, "must be a positive decimal with at most 2 places");
|
|
137
164
|
}
|
|
165
|
+
n = value;
|
|
138
166
|
} else {
|
|
139
|
-
if (
|
|
140
|
-
throw
|
|
167
|
+
if (!PLAIN_DECIMAL.test(value)) {
|
|
168
|
+
throw invalidInput(label, value, "must be a plain decimal value with at most 2 places");
|
|
141
169
|
}
|
|
142
|
-
n = Number(
|
|
170
|
+
n = Number(value);
|
|
143
171
|
}
|
|
144
|
-
if (!Number.isFinite(n) || n <= 0 || n >
|
|
145
|
-
throw
|
|
172
|
+
if (!Number.isFinite(n) || n <= 0 || n > max) {
|
|
173
|
+
throw invalidInput(label, value, `must be > 0 and <= ${max}`);
|
|
146
174
|
}
|
|
175
|
+
return n;
|
|
176
|
+
}
|
|
177
|
+
function assertUsdAmount(usdAmount) {
|
|
178
|
+
parseMoney(usdAmount, MAX_USD_AMOUNT, "usdAmount");
|
|
147
179
|
}
|
|
148
180
|
function requireField(value, field, raw) {
|
|
149
181
|
const s = value == null ? "" : String(value);
|
|
@@ -322,6 +354,104 @@ var InvoServer = class {
|
|
|
322
354
|
this.auth
|
|
323
355
|
);
|
|
324
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
359
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
360
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
361
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
362
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
363
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
364
|
+
*/
|
|
365
|
+
async purchaseItem(input) {
|
|
366
|
+
const required = [
|
|
367
|
+
["clientRequestId", input.clientRequestId],
|
|
368
|
+
["playerEmail", input.playerEmail],
|
|
369
|
+
["playerName", input.playerName],
|
|
370
|
+
["itemId", input.itemId],
|
|
371
|
+
["itemName", input.itemName]
|
|
372
|
+
];
|
|
373
|
+
for (const [k, v] of required) {
|
|
374
|
+
if (typeof v !== "string" || !v.trim()) throw invalidInput(k, v, "is required");
|
|
375
|
+
}
|
|
376
|
+
if (!Number.isInteger(input.itemQuantity) || input.itemQuantity < 1 || input.itemQuantity > 1e3) {
|
|
377
|
+
throw invalidInput("itemQuantity", input.itemQuantity, "must be an integer 1..1000");
|
|
378
|
+
}
|
|
379
|
+
const unit = parseMoney(input.unitPrice, MAX_ITEM_PRICE, "unitPrice");
|
|
380
|
+
const total = parseMoney(input.totalPrice, MAX_ITEM_PRICE, "totalPrice");
|
|
381
|
+
const expectedCents = Math.round(unit * input.itemQuantity * 100);
|
|
382
|
+
const totalCents = Math.round(total * 100);
|
|
383
|
+
if (Math.abs(totalCents - expectedCents) > 1) {
|
|
384
|
+
throw invalidInput(
|
|
385
|
+
"totalPrice",
|
|
386
|
+
input.totalPrice,
|
|
387
|
+
`must equal unitPrice \xD7 itemQuantity (\xB10.01): ${unit} \xD7 ${input.itemQuantity}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const body = {
|
|
391
|
+
client_request_id: input.clientRequestId,
|
|
392
|
+
player_email: input.playerEmail,
|
|
393
|
+
player_name: input.playerName,
|
|
394
|
+
item_id: input.itemId,
|
|
395
|
+
item_name: input.itemName,
|
|
396
|
+
item_quantity: input.itemQuantity,
|
|
397
|
+
unit_price: input.unitPrice,
|
|
398
|
+
total_price: input.totalPrice
|
|
399
|
+
};
|
|
400
|
+
if (input.playerPhone) body["player_phone"] = input.playerPhone;
|
|
401
|
+
if (input.itemDescription) body["item_description"] = input.itemDescription;
|
|
402
|
+
if (input.itemCategory) body["item_category"] = input.itemCategory;
|
|
403
|
+
const raw = await this.http.post(
|
|
404
|
+
"/api/item-purchases/purchase-item",
|
|
405
|
+
body,
|
|
406
|
+
this.auth
|
|
407
|
+
);
|
|
408
|
+
return {
|
|
409
|
+
status: String(raw["status"] ?? ""),
|
|
410
|
+
transactionId: requireField(raw["transaction_id"], "transaction_id", raw),
|
|
411
|
+
orderId: requireField(raw["order_id"], "order_id", raw),
|
|
412
|
+
newBalance: raw["new_balance"] ?? null,
|
|
413
|
+
previousBalance: raw["previous_balance"] ?? null,
|
|
414
|
+
currencyName: String(raw["currency_name"] ?? ""),
|
|
415
|
+
financialBreakdown: raw["financial_breakdown"],
|
|
416
|
+
raw
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
420
|
+
async getItemPurchaseHistory(query) {
|
|
421
|
+
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
422
|
+
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
423
|
+
}
|
|
424
|
+
const q = new URLSearchParams();
|
|
425
|
+
q.set("player_email", query.playerEmail);
|
|
426
|
+
if (query.limit != null) q.set("limit", String(query.limit));
|
|
427
|
+
if (query.offset != null) q.set("offset", String(query.offset));
|
|
428
|
+
return this.http.get(
|
|
429
|
+
`/api/item-purchases/player-purchase-history?${q.toString()}`,
|
|
430
|
+
this.auth
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
434
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
435
|
+
async getItemOrderDetails(query) {
|
|
436
|
+
const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
|
|
437
|
+
(v) => typeof v === "string" && v.trim()
|
|
438
|
+
);
|
|
439
|
+
if (provided.length !== 1) {
|
|
440
|
+
throw new InvoError({
|
|
441
|
+
message: "getItemOrderDetails requires EXACTLY ONE of orderId, transactionId, or clientRequestId.",
|
|
442
|
+
code: "INVALID_INPUT",
|
|
443
|
+
status: 0
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const q = new URLSearchParams();
|
|
447
|
+
if (query.orderId) q.set("order_id", query.orderId);
|
|
448
|
+
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
449
|
+
if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
|
|
450
|
+
return this.http.get(
|
|
451
|
+
`/api/item-purchases/order-details?${q.toString()}`,
|
|
452
|
+
this.auth
|
|
453
|
+
);
|
|
454
|
+
}
|
|
325
455
|
toInitiateResult(raw) {
|
|
326
456
|
const vm = raw["verification_method"];
|
|
327
457
|
const guardian = raw["guardian_approval"];
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult, i as PurchaseItemInput, j as PurchaseItemResult, k as ItemHistoryQuery, l as ItemOrderQuery } from './errors-DV5QsftP.cjs';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.cjs';
|
|
3
3
|
|
|
4
4
|
declare class InvoServer {
|
|
5
5
|
private readonly http;
|
|
@@ -36,7 +36,21 @@ declare class InvoServer {
|
|
|
36
36
|
orderId?: string;
|
|
37
37
|
transactionId?: string;
|
|
38
38
|
}): Promise<Record<string, unknown>>;
|
|
39
|
+
/**
|
|
40
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
41
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
42
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
43
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
44
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
45
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
46
|
+
*/
|
|
47
|
+
purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
|
|
48
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
49
|
+
getItemPurchaseHistory(query: ItemHistoryQuery): Promise<Record<string, unknown>>;
|
|
50
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
51
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
52
|
+
getItemOrderDetails(query: ItemOrderQuery): Promise<Record<string, unknown>>;
|
|
39
53
|
private toInitiateResult;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, PlayerToken, PurchaseInput, PurchaseResult, ServerConfig };
|
|
56
|
+
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, ItemHistoryQuery, ItemOrderQuery, PlayerToken, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult } from './errors-
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-
|
|
1
|
+
import { S as ServerConfig, P as PlayerToken, b as InitiateSendInput, c as InitiateResult, d as InitiateTransferInput, e as CreateCheckoutInput, f as CreateCheckoutResult, g as PurchaseInput, h as PurchaseResult, i as PurchaseItemInput, j as PurchaseItemResult, k as ItemHistoryQuery, l as ItemOrderQuery } from './errors-DV5QsftP.js';
|
|
2
|
+
export { I as InvoError, R as Rail, V as VerificationMethod } from './errors-DV5QsftP.js';
|
|
3
3
|
|
|
4
4
|
declare class InvoServer {
|
|
5
5
|
private readonly http;
|
|
@@ -36,7 +36,21 @@ declare class InvoServer {
|
|
|
36
36
|
orderId?: string;
|
|
37
37
|
transactionId?: string;
|
|
38
38
|
}): Promise<Record<string, unknown>>;
|
|
39
|
+
/**
|
|
40
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
41
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
42
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
43
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
44
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
45
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
46
|
+
*/
|
|
47
|
+
purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
|
|
48
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
49
|
+
getItemPurchaseHistory(query: ItemHistoryQuery): Promise<Record<string, unknown>>;
|
|
50
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
51
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
52
|
+
getItemOrderDetails(query: ItemOrderQuery): Promise<Record<string, unknown>>;
|
|
39
53
|
private toInitiateResult;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
|
-
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, PlayerToken, PurchaseInput, PurchaseResult, ServerConfig };
|
|
56
|
+
export { CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, ItemHistoryQuery, ItemOrderQuery, PlayerToken, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig };
|
package/dist/server.js
CHANGED
|
@@ -1,32 +1,38 @@
|
|
|
1
|
-
import { assertSecureBaseUrl, Http, InvoError } from './chunk-
|
|
2
|
-
export { InvoError } from './chunk-
|
|
1
|
+
import { assertSecureBaseUrl, Http, InvoError } from './chunk-A44O4KC3.js';
|
|
2
|
+
export { InvoError } from './chunk-A44O4KC3.js';
|
|
3
3
|
|
|
4
4
|
// src/server.ts
|
|
5
5
|
var DEFAULT_UA = "invonetwork-web-sdk/0.1.0 (+https://invo.network)";
|
|
6
6
|
var MAX_USD_AMOUNT = 999.99;
|
|
7
|
-
|
|
7
|
+
var MAX_ITEM_PRICE = 999999.99;
|
|
8
|
+
function invalidInput(label, value, why) {
|
|
8
9
|
return new InvoError({
|
|
9
|
-
message:
|
|
10
|
+
message: `${label} ${why} (got ${JSON.stringify(value)}).`,
|
|
10
11
|
code: "INVALID_INPUT",
|
|
11
12
|
status: 0
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
var PLAIN_DECIMAL = /^\d+(\.\d{1,2})?$/;
|
|
16
|
+
function parseMoney(value, max, label) {
|
|
15
17
|
let n;
|
|
16
|
-
if (typeof
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
throw invalidAmount(usdAmount, "must have at most 2 decimal places");
|
|
18
|
+
if (typeof value === "number") {
|
|
19
|
+
if (!Number.isFinite(value) || !PLAIN_DECIMAL.test(String(value))) {
|
|
20
|
+
throw invalidInput(label, value, "must be a positive decimal with at most 2 places");
|
|
20
21
|
}
|
|
22
|
+
n = value;
|
|
21
23
|
} else {
|
|
22
|
-
if (
|
|
23
|
-
throw
|
|
24
|
+
if (!PLAIN_DECIMAL.test(value)) {
|
|
25
|
+
throw invalidInput(label, value, "must be a plain decimal value with at most 2 places");
|
|
24
26
|
}
|
|
25
|
-
n = Number(
|
|
27
|
+
n = Number(value);
|
|
26
28
|
}
|
|
27
|
-
if (!Number.isFinite(n) || n <= 0 || n >
|
|
28
|
-
throw
|
|
29
|
+
if (!Number.isFinite(n) || n <= 0 || n > max) {
|
|
30
|
+
throw invalidInput(label, value, `must be > 0 and <= ${max}`);
|
|
29
31
|
}
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
function assertUsdAmount(usdAmount) {
|
|
35
|
+
parseMoney(usdAmount, MAX_USD_AMOUNT, "usdAmount");
|
|
30
36
|
}
|
|
31
37
|
function requireField(value, field, raw) {
|
|
32
38
|
const s = value == null ? "" : String(value);
|
|
@@ -205,6 +211,104 @@ var InvoServer = class {
|
|
|
205
211
|
this.auth
|
|
206
212
|
);
|
|
207
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
216
|
+
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
217
|
+
* server-side by the game secret. Idempotent on clientRequestId (a duplicate throws
|
|
218
|
+
* a 409 InvoError; check err.isDuplicateRequest). Insufficient balance throws a 400
|
|
219
|
+
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
220
|
+
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
221
|
+
*/
|
|
222
|
+
async purchaseItem(input) {
|
|
223
|
+
const required = [
|
|
224
|
+
["clientRequestId", input.clientRequestId],
|
|
225
|
+
["playerEmail", input.playerEmail],
|
|
226
|
+
["playerName", input.playerName],
|
|
227
|
+
["itemId", input.itemId],
|
|
228
|
+
["itemName", input.itemName]
|
|
229
|
+
];
|
|
230
|
+
for (const [k, v] of required) {
|
|
231
|
+
if (typeof v !== "string" || !v.trim()) throw invalidInput(k, v, "is required");
|
|
232
|
+
}
|
|
233
|
+
if (!Number.isInteger(input.itemQuantity) || input.itemQuantity < 1 || input.itemQuantity > 1e3) {
|
|
234
|
+
throw invalidInput("itemQuantity", input.itemQuantity, "must be an integer 1..1000");
|
|
235
|
+
}
|
|
236
|
+
const unit = parseMoney(input.unitPrice, MAX_ITEM_PRICE, "unitPrice");
|
|
237
|
+
const total = parseMoney(input.totalPrice, MAX_ITEM_PRICE, "totalPrice");
|
|
238
|
+
const expectedCents = Math.round(unit * input.itemQuantity * 100);
|
|
239
|
+
const totalCents = Math.round(total * 100);
|
|
240
|
+
if (Math.abs(totalCents - expectedCents) > 1) {
|
|
241
|
+
throw invalidInput(
|
|
242
|
+
"totalPrice",
|
|
243
|
+
input.totalPrice,
|
|
244
|
+
`must equal unitPrice \xD7 itemQuantity (\xB10.01): ${unit} \xD7 ${input.itemQuantity}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const body = {
|
|
248
|
+
client_request_id: input.clientRequestId,
|
|
249
|
+
player_email: input.playerEmail,
|
|
250
|
+
player_name: input.playerName,
|
|
251
|
+
item_id: input.itemId,
|
|
252
|
+
item_name: input.itemName,
|
|
253
|
+
item_quantity: input.itemQuantity,
|
|
254
|
+
unit_price: input.unitPrice,
|
|
255
|
+
total_price: input.totalPrice
|
|
256
|
+
};
|
|
257
|
+
if (input.playerPhone) body["player_phone"] = input.playerPhone;
|
|
258
|
+
if (input.itemDescription) body["item_description"] = input.itemDescription;
|
|
259
|
+
if (input.itemCategory) body["item_category"] = input.itemCategory;
|
|
260
|
+
const raw = await this.http.post(
|
|
261
|
+
"/api/item-purchases/purchase-item",
|
|
262
|
+
body,
|
|
263
|
+
this.auth
|
|
264
|
+
);
|
|
265
|
+
return {
|
|
266
|
+
status: String(raw["status"] ?? ""),
|
|
267
|
+
transactionId: requireField(raw["transaction_id"], "transaction_id", raw),
|
|
268
|
+
orderId: requireField(raw["order_id"], "order_id", raw),
|
|
269
|
+
newBalance: raw["new_balance"] ?? null,
|
|
270
|
+
previousBalance: raw["previous_balance"] ?? null,
|
|
271
|
+
currencyName: String(raw["currency_name"] ?? ""),
|
|
272
|
+
financialBreakdown: raw["financial_breakdown"],
|
|
273
|
+
raw
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
277
|
+
async getItemPurchaseHistory(query) {
|
|
278
|
+
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
279
|
+
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
280
|
+
}
|
|
281
|
+
const q = new URLSearchParams();
|
|
282
|
+
q.set("player_email", query.playerEmail);
|
|
283
|
+
if (query.limit != null) q.set("limit", String(query.limit));
|
|
284
|
+
if (query.offset != null) q.set("offset", String(query.offset));
|
|
285
|
+
return this.http.get(
|
|
286
|
+
`/api/item-purchases/player-purchase-history?${q.toString()}`,
|
|
287
|
+
this.auth
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
291
|
+
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
292
|
+
async getItemOrderDetails(query) {
|
|
293
|
+
const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
|
|
294
|
+
(v) => typeof v === "string" && v.trim()
|
|
295
|
+
);
|
|
296
|
+
if (provided.length !== 1) {
|
|
297
|
+
throw new InvoError({
|
|
298
|
+
message: "getItemOrderDetails requires EXACTLY ONE of orderId, transactionId, or clientRequestId.",
|
|
299
|
+
code: "INVALID_INPUT",
|
|
300
|
+
status: 0
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
const q = new URLSearchParams();
|
|
304
|
+
if (query.orderId) q.set("order_id", query.orderId);
|
|
305
|
+
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
306
|
+
if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
|
|
307
|
+
return this.http.get(
|
|
308
|
+
`/api/item-purchases/order-details?${q.toString()}`,
|
|
309
|
+
this.auth
|
|
310
|
+
);
|
|
311
|
+
}
|
|
208
312
|
toInitiateResult(raw) {
|
|
209
313
|
const vm = raw["verification_method"];
|
|
210
314
|
const guardian = raw["guardian_approval"];
|