@invonetwork/web-sdk 0.3.0 → 0.4.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 +122 -96
- package/README.md +42 -20
- package/dist/{chunk-DV3WZGMH.js → chunk-EEWOAUXO.js} +28 -10
- package/dist/index.cjs +74 -36
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.js +50 -30
- package/dist/server.cjs +210 -39
- package/dist/server.d.cts +78 -19
- package/dist/server.d.ts +78 -19
- package/dist/server.js +185 -34
- package/dist/{types-CBkoUymV.d.cts → types-CBMLNwbe.d.cts} +32 -1
- package/dist/{types-CBkoUymV.d.ts → types-CBMLNwbe.d.ts} +32 -1
- package/package.json +76 -68
package/dist/server.cjs
CHANGED
|
@@ -100,11 +100,11 @@ var _Http = class _Http {
|
|
|
100
100
|
* (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
|
|
101
101
|
*/
|
|
102
102
|
async post(path, body, auth, opts) {
|
|
103
|
-
return this.request("POST", path, body, auth, opts?.idempotent ?? false);
|
|
103
|
+
return this.request("POST", path, body, auth, opts?.idempotent ?? false, opts?.signal);
|
|
104
104
|
}
|
|
105
105
|
// GET is always idempotent → safe to retry.
|
|
106
|
-
async get(path, auth) {
|
|
107
|
-
return this.request("GET", path, void 0, auth, true);
|
|
106
|
+
async get(path, auth, opts) {
|
|
107
|
+
return this.request("GET", path, void 0, auth, true, opts?.signal);
|
|
108
108
|
}
|
|
109
109
|
authHeaders(auth) {
|
|
110
110
|
switch (auth.kind) {
|
|
@@ -116,7 +116,7 @@ var _Http = class _Http {
|
|
|
116
116
|
return {};
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
async request(method, path, body, auth, idempotent) {
|
|
119
|
+
async request(method, path, body, auth, idempotent, signal) {
|
|
120
120
|
const url = `${this.baseUrl}${path}`;
|
|
121
121
|
const headers = {
|
|
122
122
|
Accept: "application/json",
|
|
@@ -126,10 +126,13 @@ var _Http = class _Http {
|
|
|
126
126
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
127
127
|
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
128
128
|
for (let attempt = 0; ; attempt++) {
|
|
129
|
+
if (signal?.aborted) throw abortError(path);
|
|
129
130
|
const start = Date.now();
|
|
130
131
|
this.fire("onRequest", { method, url, attempt });
|
|
131
132
|
const controller = new AbortController();
|
|
132
133
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
134
|
+
const onAbort = () => controller.abort();
|
|
135
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
133
136
|
let res;
|
|
134
137
|
let networkError;
|
|
135
138
|
try {
|
|
@@ -142,12 +145,14 @@ var _Http = class _Http {
|
|
|
142
145
|
});
|
|
143
146
|
} finally {
|
|
144
147
|
clearTimeout(timer);
|
|
148
|
+
signal?.removeEventListener("abort", onAbort);
|
|
145
149
|
}
|
|
146
150
|
if (networkError) {
|
|
151
|
+
if (signal?.aborted) throw abortError(path);
|
|
147
152
|
const willRetry = idempotent && attempt < this.maxRetries;
|
|
148
153
|
this.fire("onError", { method, url, attempt, error: networkError, willRetry });
|
|
149
154
|
if (willRetry) {
|
|
150
|
-
await sleep(this.backoff(attempt));
|
|
155
|
+
await sleep(this.backoff(attempt), signal);
|
|
151
156
|
continue;
|
|
152
157
|
}
|
|
153
158
|
throw networkError;
|
|
@@ -183,7 +188,7 @@ var _Http = class _Http {
|
|
|
183
188
|
}
|
|
184
189
|
this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
|
|
185
190
|
if (wait !== void 0) {
|
|
186
|
-
await sleep(wait);
|
|
191
|
+
await sleep(wait, signal);
|
|
187
192
|
continue;
|
|
188
193
|
}
|
|
189
194
|
throw err;
|
|
@@ -209,8 +214,21 @@ var _Http = class _Http {
|
|
|
209
214
|
/** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
|
|
210
215
|
_Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
211
216
|
var Http = _Http;
|
|
212
|
-
function sleep(ms) {
|
|
213
|
-
return new Promise((resolve) =>
|
|
217
|
+
function sleep(ms, signal) {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
if (signal?.aborted) return resolve();
|
|
220
|
+
const timer = setTimeout(done, ms);
|
|
221
|
+
const onAbort = () => done();
|
|
222
|
+
function done() {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
signal?.removeEventListener("abort", onAbort);
|
|
225
|
+
resolve();
|
|
226
|
+
}
|
|
227
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function abortError(path) {
|
|
231
|
+
return new InvoError({ message: `Request to ${path} was aborted`, code: "ABORTED", status: 0 });
|
|
214
232
|
}
|
|
215
233
|
function pickRequestId(headers) {
|
|
216
234
|
if (!headers || typeof headers.get !== "function") return void 0;
|
|
@@ -265,7 +283,7 @@ function safeEqualHex(a, b) {
|
|
|
265
283
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
266
284
|
return diff === 0;
|
|
267
285
|
}
|
|
268
|
-
function
|
|
286
|
+
function prepareVerification(rawBody, signatureHeader, secret, opts) {
|
|
269
287
|
if (!signatureHeader) {
|
|
270
288
|
throw webhookError("Missing X-Invo-Signature header.", "WEBHOOK_SIGNATURE_MISSING");
|
|
271
289
|
}
|
|
@@ -288,13 +306,9 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
288
306
|
}
|
|
289
307
|
const bodyBytes = toBytes(rawBody);
|
|
290
308
|
const message = concatBytes(ENCODER.encode(`${t}.`), bodyBytes);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
});
|
|
295
|
-
if (!matched) {
|
|
296
|
-
throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
|
|
297
|
-
}
|
|
309
|
+
return { secrets, sigs, message, bodyBytes };
|
|
310
|
+
}
|
|
311
|
+
function finalizeEvent(bodyBytes) {
|
|
298
312
|
let parsed;
|
|
299
313
|
try {
|
|
300
314
|
parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
@@ -306,9 +320,93 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
306
320
|
}
|
|
307
321
|
return parsed;
|
|
308
322
|
}
|
|
323
|
+
function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
324
|
+
const { secrets, sigs, message, bodyBytes } = prepareVerification(
|
|
325
|
+
rawBody,
|
|
326
|
+
signatureHeader,
|
|
327
|
+
secret,
|
|
328
|
+
opts
|
|
329
|
+
);
|
|
330
|
+
const matched = secrets.some((s) => {
|
|
331
|
+
const expected = hmacHex(s, message);
|
|
332
|
+
return sigs.some((sig) => safeEqualHex(sig, expected));
|
|
333
|
+
});
|
|
334
|
+
if (!matched) {
|
|
335
|
+
throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
|
|
336
|
+
}
|
|
337
|
+
return finalizeEvent(bodyBytes);
|
|
338
|
+
}
|
|
339
|
+
async function verifyWebhookAsync(rawBody, signatureHeader, secret, opts = {}) {
|
|
340
|
+
const { secrets, sigs, message, bodyBytes } = prepareVerification(
|
|
341
|
+
rawBody,
|
|
342
|
+
signatureHeader,
|
|
343
|
+
secret,
|
|
344
|
+
opts
|
|
345
|
+
);
|
|
346
|
+
for (const s of secrets) {
|
|
347
|
+
const expected = await hmacHexSubtle(s, message);
|
|
348
|
+
if (sigs.some((sig) => safeEqualHex(sig, expected))) {
|
|
349
|
+
return finalizeEvent(bodyBytes);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
|
|
353
|
+
}
|
|
354
|
+
function createWebhookHandler(opts) {
|
|
355
|
+
return async (request) => {
|
|
356
|
+
const raw = await request.text();
|
|
357
|
+
let event;
|
|
358
|
+
try {
|
|
359
|
+
event = await verifyWebhookAsync(raw, request.headers.get("x-invo-signature"), opts.secret, {
|
|
360
|
+
toleranceSec: opts.toleranceSec
|
|
361
|
+
});
|
|
362
|
+
} catch (e) {
|
|
363
|
+
const err = e instanceof InvoError ? e : new InvoError({ message: String(e), status: 0 });
|
|
364
|
+
if (opts.onError) {
|
|
365
|
+
try {
|
|
366
|
+
const r = await opts.onError(err, request);
|
|
367
|
+
if (r instanceof Response) return r;
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return new Response(err.code ?? "invalid_signature", { status: 400 });
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await opts.onEvent(event, {
|
|
375
|
+
// Prefer the signature-covered body field over the (unauthenticated, optional)
|
|
376
|
+
// header — the body value is HMAC-verified and always present.
|
|
377
|
+
idempotencyKey: event.idempotency_key ?? request.headers.get("x-invo-idempotency-key"),
|
|
378
|
+
request
|
|
379
|
+
});
|
|
380
|
+
} catch {
|
|
381
|
+
return new Response("handler_error", { status: 500 });
|
|
382
|
+
}
|
|
383
|
+
return new Response(null, { status: 200 });
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function hmacHexSubtle(secret, message) {
|
|
387
|
+
const subtle = globalThis.crypto?.subtle;
|
|
388
|
+
if (!subtle) {
|
|
389
|
+
throw webhookError(
|
|
390
|
+
"Web Crypto (crypto.subtle) is unavailable in this runtime.",
|
|
391
|
+
"WEBHOOK_UNSUPPORTED"
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const key = await subtle.importKey(
|
|
395
|
+
"raw",
|
|
396
|
+
ENCODER.encode(secret),
|
|
397
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
398
|
+
false,
|
|
399
|
+
["sign"]
|
|
400
|
+
);
|
|
401
|
+
const sig = await subtle.sign("HMAC", key, message);
|
|
402
|
+
const bytes = new Uint8Array(sig);
|
|
403
|
+
let hex = "";
|
|
404
|
+
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
405
|
+
return hex;
|
|
406
|
+
}
|
|
309
407
|
|
|
310
408
|
// src/server.ts
|
|
311
|
-
var DEFAULT_UA = "invonetwork-web-sdk/0.
|
|
409
|
+
var DEFAULT_UA = "invonetwork-web-sdk/0.4.0 (+https://invo.network)";
|
|
312
410
|
var MAX_USD_AMOUNT = 999.99;
|
|
313
411
|
var MAX_ITEM_PRICE = 999999.99;
|
|
314
412
|
function invalidInput(label, value, why) {
|
|
@@ -348,6 +446,21 @@ function toOrderDetails(raw) {
|
|
|
348
446
|
raw
|
|
349
447
|
};
|
|
350
448
|
}
|
|
449
|
+
function toInboundPendingItem(row) {
|
|
450
|
+
return {
|
|
451
|
+
transactionId: String(row["transaction_id"] ?? ""),
|
|
452
|
+
flow: String(row["flow"] ?? ""),
|
|
453
|
+
amount: String(row["amount"] ?? ""),
|
|
454
|
+
netAmount: String(row["net_amount"] ?? ""),
|
|
455
|
+
sourceGameId: row["source_game_id"] ?? "",
|
|
456
|
+
sourceGame: String(row["source_game"] ?? ""),
|
|
457
|
+
toPhone: String(row["to_phone"] ?? ""),
|
|
458
|
+
toIdentityId: row["to_identity_id"] ?? null,
|
|
459
|
+
createdAt: String(row["created_at"] ?? ""),
|
|
460
|
+
claimCodeExpiresAt: String(row["claim_code_expires_at"] ?? ""),
|
|
461
|
+
raw: row
|
|
462
|
+
};
|
|
463
|
+
}
|
|
351
464
|
function toCurrencyBalance(row) {
|
|
352
465
|
return {
|
|
353
466
|
currencyId: row["currency_id"] ?? "",
|
|
@@ -390,7 +503,7 @@ var InvoServer = class {
|
|
|
390
503
|
this.auth = { kind: "game-secret", secret: config.gameSecret };
|
|
391
504
|
}
|
|
392
505
|
/** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
|
|
393
|
-
async mintPlayerToken(input) {
|
|
506
|
+
async mintPlayerToken(input, opts) {
|
|
394
507
|
if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
|
|
395
508
|
throw invalidInput("playerEmail", input.playerEmail, "is required");
|
|
396
509
|
}
|
|
@@ -398,7 +511,7 @@ var InvoServer = class {
|
|
|
398
511
|
"/api/sdk/player-token",
|
|
399
512
|
{ player_email: input.playerEmail },
|
|
400
513
|
this.auth,
|
|
401
|
-
{ idempotent: true }
|
|
514
|
+
{ idempotent: true, signal: opts?.signal }
|
|
402
515
|
// safe to retry: re-mint just issues a fresh token
|
|
403
516
|
);
|
|
404
517
|
return {
|
|
@@ -408,7 +521,7 @@ var InvoServer = class {
|
|
|
408
521
|
};
|
|
409
522
|
}
|
|
410
523
|
/** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
|
|
411
|
-
async initiateSend(input) {
|
|
524
|
+
async initiateSend(input, opts) {
|
|
412
525
|
const raw = await this.http.post(
|
|
413
526
|
"/api/currency-sends/initiate-send",
|
|
414
527
|
{
|
|
@@ -422,13 +535,13 @@ var InvoServer = class {
|
|
|
422
535
|
amount: input.amount
|
|
423
536
|
},
|
|
424
537
|
this.auth,
|
|
425
|
-
{ idempotent: true }
|
|
538
|
+
{ idempotent: true, signal: opts?.signal }
|
|
426
539
|
// client_request_id makes this safe to retry (backend dedupes)
|
|
427
540
|
);
|
|
428
541
|
return this.toInitiateResult(raw);
|
|
429
542
|
}
|
|
430
543
|
/** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
|
|
431
|
-
async initiateTransfer(input) {
|
|
544
|
+
async initiateTransfer(input, opts) {
|
|
432
545
|
const raw = await this.http.post(
|
|
433
546
|
"/api/transfers/initiate-transfer",
|
|
434
547
|
{
|
|
@@ -442,7 +555,7 @@ var InvoServer = class {
|
|
|
442
555
|
amount: input.amount
|
|
443
556
|
},
|
|
444
557
|
this.auth,
|
|
445
|
-
{ idempotent: true }
|
|
558
|
+
{ idempotent: true, signal: opts?.signal }
|
|
446
559
|
// client_request_id makes this safe to retry (backend dedupes)
|
|
447
560
|
);
|
|
448
561
|
return this.toInitiateResult(raw);
|
|
@@ -451,7 +564,7 @@ var InvoServer = class {
|
|
|
451
564
|
* returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
|
|
452
565
|
* handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
|
|
453
566
|
* purchase.completed webhook. */
|
|
454
|
-
async createCheckout(input) {
|
|
567
|
+
async createCheckout(input, opts) {
|
|
455
568
|
if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
|
|
456
569
|
throw invalidInput("playerEmail", input.playerEmail, "is required");
|
|
457
570
|
}
|
|
@@ -467,7 +580,9 @@ var InvoServer = class {
|
|
|
467
580
|
const raw = await this.http.post(
|
|
468
581
|
"/api/checkout/sessions",
|
|
469
582
|
body,
|
|
470
|
-
this.auth
|
|
583
|
+
this.auth,
|
|
584
|
+
{ signal: opts?.signal }
|
|
585
|
+
// not idempotent — never auto-retried
|
|
471
586
|
);
|
|
472
587
|
return {
|
|
473
588
|
sessionId: String(raw["session_id"] ?? ""),
|
|
@@ -483,7 +598,7 @@ var InvoServer = class {
|
|
|
483
598
|
* - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
|
|
484
599
|
* - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
|
|
485
600
|
*/
|
|
486
|
-
async purchaseCurrency(input) {
|
|
601
|
+
async purchaseCurrency(input, opts) {
|
|
487
602
|
if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
|
|
488
603
|
throw invalidInput("playerEmail", input.playerEmail, "is required");
|
|
489
604
|
}
|
|
@@ -516,7 +631,7 @@ var InvoServer = class {
|
|
|
516
631
|
"/api/currency-purchases/purchase-currency",
|
|
517
632
|
body,
|
|
518
633
|
this.auth,
|
|
519
|
-
{ idempotent: true }
|
|
634
|
+
{ idempotent: true, signal: opts?.signal }
|
|
520
635
|
// purchase_reference makes this safe to retry (backend dedupes)
|
|
521
636
|
);
|
|
522
637
|
return {
|
|
@@ -531,14 +646,14 @@ var InvoServer = class {
|
|
|
531
646
|
};
|
|
532
647
|
}
|
|
533
648
|
/** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
|
|
534
|
-
async confirmPayment(input) {
|
|
649
|
+
async confirmPayment(input, opts) {
|
|
535
650
|
const body = { payment_intent_id: input.paymentIntentId };
|
|
536
651
|
if (input.orderId) body["order_id"] = input.orderId;
|
|
537
652
|
const raw = await this.http.post(
|
|
538
653
|
"/api/currency-purchases/confirm-payment",
|
|
539
654
|
body,
|
|
540
655
|
this.auth,
|
|
541
|
-
{ idempotent: true }
|
|
656
|
+
{ idempotent: true, signal: opts?.signal }
|
|
542
657
|
// keyed by payment_intent_id — safe to retry
|
|
543
658
|
);
|
|
544
659
|
return {
|
|
@@ -549,7 +664,7 @@ var InvoServer = class {
|
|
|
549
664
|
};
|
|
550
665
|
}
|
|
551
666
|
/** Fetch purchase status (order + financial summary + timeline). */
|
|
552
|
-
async getOrderDetails(query) {
|
|
667
|
+
async getOrderDetails(query, opts) {
|
|
553
668
|
if (!query.orderId && !query.transactionId) {
|
|
554
669
|
throw new InvoError({
|
|
555
670
|
message: "getOrderDetails requires an `orderId` or `transactionId`.",
|
|
@@ -562,7 +677,8 @@ var InvoServer = class {
|
|
|
562
677
|
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
563
678
|
const raw = await this.http.get(
|
|
564
679
|
`/api/currency-purchases/order-details?${q.toString()}`,
|
|
565
|
-
this.auth
|
|
680
|
+
this.auth,
|
|
681
|
+
{ signal: opts?.signal }
|
|
566
682
|
);
|
|
567
683
|
return toOrderDetails(raw);
|
|
568
684
|
}
|
|
@@ -574,7 +690,7 @@ var InvoServer = class {
|
|
|
574
690
|
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
575
691
|
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
576
692
|
*/
|
|
577
|
-
async purchaseItem(input) {
|
|
693
|
+
async purchaseItem(input, opts) {
|
|
578
694
|
const required = [
|
|
579
695
|
["clientRequestId", input.clientRequestId],
|
|
580
696
|
["playerEmail", input.playerEmail],
|
|
@@ -616,7 +732,7 @@ var InvoServer = class {
|
|
|
616
732
|
"/api/item-purchases/purchase-item",
|
|
617
733
|
body,
|
|
618
734
|
this.auth,
|
|
619
|
-
{ idempotent: true }
|
|
735
|
+
{ idempotent: true, signal: opts?.signal }
|
|
620
736
|
// client_request_id makes this safe to retry (dup → 409)
|
|
621
737
|
);
|
|
622
738
|
return {
|
|
@@ -631,7 +747,7 @@ var InvoServer = class {
|
|
|
631
747
|
};
|
|
632
748
|
}
|
|
633
749
|
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
634
|
-
async getItemPurchaseHistory(query) {
|
|
750
|
+
async getItemPurchaseHistory(query, opts) {
|
|
635
751
|
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
636
752
|
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
637
753
|
}
|
|
@@ -641,7 +757,8 @@ var InvoServer = class {
|
|
|
641
757
|
if (query.offset != null) q.set("offset", String(query.offset));
|
|
642
758
|
const raw = await this.http.get(
|
|
643
759
|
`/api/item-purchases/player-purchase-history?${q.toString()}`,
|
|
644
|
-
this.auth
|
|
760
|
+
this.auth,
|
|
761
|
+
{ signal: opts?.signal }
|
|
645
762
|
);
|
|
646
763
|
return {
|
|
647
764
|
history: Array.isArray(raw["item_purchase_history"]) ? raw["item_purchase_history"] : [],
|
|
@@ -651,7 +768,7 @@ var InvoServer = class {
|
|
|
651
768
|
}
|
|
652
769
|
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
653
770
|
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
654
|
-
async getItemOrderDetails(query) {
|
|
771
|
+
async getItemOrderDetails(query, opts) {
|
|
655
772
|
const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
|
|
656
773
|
(v) => typeof v === "string" && v.trim()
|
|
657
774
|
);
|
|
@@ -668,12 +785,40 @@ var InvoServer = class {
|
|
|
668
785
|
if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
|
|
669
786
|
const raw = await this.http.get(
|
|
670
787
|
`/api/item-purchases/order-details?${q.toString()}`,
|
|
671
|
-
this.auth
|
|
788
|
+
this.auth,
|
|
789
|
+
{ signal: opts?.signal }
|
|
672
790
|
);
|
|
673
791
|
return toOrderDetails(raw);
|
|
674
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* Async iterator over a player's entire item-purchase history — pages through
|
|
795
|
+
* `getItemPurchaseHistory` (limit/offset) until exhausted, yielding one row at a time.
|
|
796
|
+
*
|
|
797
|
+
* ```ts
|
|
798
|
+
* for await (const row of invo.iterateItemPurchaseHistory({ playerEmail })) { … }
|
|
799
|
+
* ```
|
|
800
|
+
*/
|
|
801
|
+
async *iterateItemPurchaseHistory(query, opts) {
|
|
802
|
+
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
803
|
+
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
804
|
+
}
|
|
805
|
+
const pageSize = typeof query.pageSize === "number" && query.pageSize > 0 ? Math.floor(query.pageSize) : 50;
|
|
806
|
+
let yielded = 0;
|
|
807
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
808
|
+
const { history, pagination } = await this.getItemPurchaseHistory(
|
|
809
|
+
{ playerEmail: query.playerEmail, limit: pageSize, offset },
|
|
810
|
+
opts
|
|
811
|
+
);
|
|
812
|
+
if (history.length === 0) return;
|
|
813
|
+
for (const row of history) yield row;
|
|
814
|
+
yielded += history.length;
|
|
815
|
+
if (history.length < pageSize) return;
|
|
816
|
+
const total = Number(pagination?.["total"]);
|
|
817
|
+
if (Number.isFinite(total) && yielded >= total) return;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
675
820
|
/** Read a player's currency balances, by email or playerId (game-secret). */
|
|
676
|
-
async getPlayerBalance(query) {
|
|
821
|
+
async getPlayerBalance(query, opts) {
|
|
677
822
|
let path;
|
|
678
823
|
if (query.playerId != null && String(query.playerId).trim()) {
|
|
679
824
|
path = `/api/player-balances/player/${encodeURIComponent(String(query.playerId))}`;
|
|
@@ -686,7 +831,7 @@ var InvoServer = class {
|
|
|
686
831
|
status: 0
|
|
687
832
|
});
|
|
688
833
|
}
|
|
689
|
-
const raw = await this.http.get(path, this.auth);
|
|
834
|
+
const raw = await this.http.get(path, this.auth, { signal: opts?.signal });
|
|
690
835
|
const rows = Array.isArray(raw["balances"]) ? raw["balances"] : [];
|
|
691
836
|
return {
|
|
692
837
|
player: raw["player"] ?? {},
|
|
@@ -695,6 +840,30 @@ var InvoServer = class {
|
|
|
695
840
|
raw
|
|
696
841
|
};
|
|
697
842
|
}
|
|
843
|
+
/**
|
|
844
|
+
* List LIVE, unclaimed inbound sends/transfers addressed to a player at YOUR game —
|
|
845
|
+
* the source of truth for a "you have X to collect" badge. Pass the player's email
|
|
846
|
+
* or phone. Match `toPhone` to the logged-in player (don't require `toIdentityId`,
|
|
847
|
+
* which is null when the phone maps to more than one of your players). Pairs with the
|
|
848
|
+
* `transfer.claim_pending` webhook (the webhook is the wake-up; this is the list).
|
|
849
|
+
*/
|
|
850
|
+
async getInboundPending(query, opts) {
|
|
851
|
+
const hasEmail = typeof query.playerEmail === "string" && query.playerEmail.trim();
|
|
852
|
+
const hasPhone = typeof query.playerPhone === "string" && query.playerPhone.trim();
|
|
853
|
+
if (!hasEmail && !hasPhone) {
|
|
854
|
+
throw invalidInput("query", query, "requires a playerEmail or playerPhone");
|
|
855
|
+
}
|
|
856
|
+
const q = new URLSearchParams();
|
|
857
|
+
if (hasEmail) q.set("player_email", query.playerEmail.trim());
|
|
858
|
+
if (hasPhone) q.set("player_phone", query.playerPhone.trim());
|
|
859
|
+
const raw = await this.http.get(
|
|
860
|
+
`/api/transfers/inbound-pending?${q.toString()}`,
|
|
861
|
+
this.auth,
|
|
862
|
+
{ signal: opts?.signal }
|
|
863
|
+
);
|
|
864
|
+
const rows = Array.isArray(raw["inbound_pending"]) ? raw["inbound_pending"] : [];
|
|
865
|
+
return { inboundPending: rows.map(toInboundPendingItem), raw };
|
|
866
|
+
}
|
|
698
867
|
toInitiateResult(raw) {
|
|
699
868
|
const vm = raw["verification_method"];
|
|
700
869
|
const guardian = raw["guardian_approval"];
|
|
@@ -709,6 +878,8 @@ var InvoServer = class {
|
|
|
709
878
|
|
|
710
879
|
exports.InvoError = InvoError;
|
|
711
880
|
exports.InvoServer = InvoServer;
|
|
881
|
+
exports.createWebhookHandler = createWebhookHandler;
|
|
712
882
|
exports.verifyWebhook = verifyWebhook;
|
|
883
|
+
exports.verifyWebhookAsync = verifyWebhookAsync;
|
|
713
884
|
//# sourceMappingURL=server.cjs.map
|
|
714
885
|
//# sourceMappingURL=server.cjs.map
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as ServerConfig, P as PlayerToken,
|
|
2
|
-
export {
|
|
1
|
+
import { I as InvoError, S as ServerConfig, a as CallOptions, P as PlayerToken, g as InitiateSendInput, h as InitiateResult, i as InitiateTransferInput, j as CreateCheckoutInput, k as CreateCheckoutResult, l as PurchaseInput, m as PurchaseResult, n as ConfirmPaymentResult, O as OrderDetailsResult, o as PurchaseItemInput, p as PurchaseItemResult, q as ItemHistoryQuery, r as ItemHistoryResult, s as ItemOrderQuery, t as PlayerBalanceQuery, u as PlayerBalanceResult, v as InboundPendingQuery, w as InboundPendingResult } from './types-CBMLNwbe.cjs';
|
|
2
|
+
export { x as CurrencyBalance, y as InboundPendingItem, c as InvoErrorInfo, d as InvoHooks, e as InvoRequestInfo, f as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBMLNwbe.cjs';
|
|
3
3
|
|
|
4
4
|
interface VerifyWebhookOptions {
|
|
5
5
|
/** Max age of the signed timestamp, in seconds. Default 300 (5 min). */
|
|
@@ -94,15 +94,54 @@ type InvoWebhookEvent = (WebhookBase & {
|
|
|
94
94
|
data: Record<string, unknown>;
|
|
95
95
|
});
|
|
96
96
|
/**
|
|
97
|
-
* Verify an Invo webhook and return the parsed, typed event.
|
|
97
|
+
* Verify an Invo webhook and return the parsed, typed event. **Synchronous; uses
|
|
98
|
+
* `node:crypto`** — for non-Node runtimes (Cloudflare Workers, Deno, Vercel/Netlify
|
|
99
|
+
* Edge, Bun edge) use {@link verifyWebhookAsync}.
|
|
98
100
|
*
|
|
99
|
-
* @param rawBody The exact raw request body (
|
|
101
|
+
* @param rawBody The exact raw request body (bytes or string) — never a re-serialized object.
|
|
100
102
|
* @param signatureHeader The `X-Invo-Signature` header value.
|
|
101
|
-
* @param secret Your signing secret, or an array
|
|
102
|
-
* @throws InvoError (status 0)
|
|
103
|
-
* WEBHOOK_SIGNATURE_INVALID | WEBHOOK_MALFORMED
|
|
103
|
+
* @param secret Your signing secret, or an array (to accept old + new during rotation).
|
|
104
|
+
* @throws InvoError (status 0): WEBHOOK_SIGNATURE_MISSING | WEBHOOK_SECRET_MISSING |
|
|
105
|
+
* WEBHOOK_TIMESTAMP_EXPIRED | WEBHOOK_SIGNATURE_INVALID | WEBHOOK_MALFORMED.
|
|
104
106
|
*/
|
|
105
107
|
declare function verifyWebhook(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string | string[], opts?: VerifyWebhookOptions): InvoWebhookEvent;
|
|
108
|
+
/**
|
|
109
|
+
* Cross-runtime async variant of {@link verifyWebhook}. Uses the Web Crypto API
|
|
110
|
+
* (`crypto.subtle`) instead of `node:crypto`, so it works on Cloudflare Workers,
|
|
111
|
+
* Deno, Vercel/Netlify Edge, Bun, and modern browsers (as well as Node ≥ 18).
|
|
112
|
+
* Same arguments, errors, and return value — just `await` it.
|
|
113
|
+
*/
|
|
114
|
+
declare function verifyWebhookAsync(rawBody: string | Uint8Array, signatureHeader: string | null | undefined, secret: string | string[], opts?: VerifyWebhookOptions): Promise<InvoWebhookEvent>;
|
|
115
|
+
interface WebhookHandlerOptions {
|
|
116
|
+
/** Signing secret, or an array to accept old + new during rotation. */
|
|
117
|
+
secret: string | string[];
|
|
118
|
+
/** Called with the verified, typed event after the signature passes. De-dupe on
|
|
119
|
+
* `ctx.idempotencyKey`. Throw to return a 500 (Invo will retry). */
|
|
120
|
+
onEvent: (event: InvoWebhookEvent, ctx: {
|
|
121
|
+
idempotencyKey: string | null;
|
|
122
|
+
request: Request;
|
|
123
|
+
}) => void | Promise<void>;
|
|
124
|
+
/** Optional: handle a verification failure. Return a Response to override the default 400. */
|
|
125
|
+
onError?: (error: InvoError, request: Request) => void | Response | Promise<void | Response>;
|
|
126
|
+
/** Replay tolerance (seconds). Default 300. */
|
|
127
|
+
toleranceSec?: number;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Build a webhook route handler from the Fetch API `(Request) => Promise<Response>` —
|
|
131
|
+
* works in Next.js App Router route handlers, Cloudflare Workers, Deno, Hono, and Bun.
|
|
132
|
+
* It reads the raw body, verifies with {@link verifyWebhookAsync}, and on success calls
|
|
133
|
+
* `onEvent` with the typed event and the idempotency key (de-dupe is yours). Verification
|
|
134
|
+
* failures return `400`; a throwing `onEvent` returns `500` so Invo retries.
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* // app/invo/webhooks/route.ts (Next.js)
|
|
138
|
+
* export const POST = createWebhookHandler({
|
|
139
|
+
* secret: process.env.INVO_WEBHOOK_SECRET!,
|
|
140
|
+
* onEvent: async (event, { idempotencyKey }) => { await grantValue(event); },
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function createWebhookHandler(opts: WebhookHandlerOptions): (request: Request) => Promise<Response>;
|
|
106
145
|
|
|
107
146
|
declare class InvoServer {
|
|
108
147
|
private readonly http;
|
|
@@ -111,16 +150,16 @@ declare class InvoServer {
|
|
|
111
150
|
/** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
|
|
112
151
|
mintPlayerToken(input: {
|
|
113
152
|
playerEmail: string;
|
|
114
|
-
}): Promise<PlayerToken>;
|
|
153
|
+
}, opts?: CallOptions): Promise<PlayerToken>;
|
|
115
154
|
/** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
|
|
116
|
-
initiateSend(input: InitiateSendInput): Promise<InitiateResult>;
|
|
155
|
+
initiateSend(input: InitiateSendInput, opts?: CallOptions): Promise<InitiateResult>;
|
|
117
156
|
/** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
|
|
118
|
-
initiateTransfer(input: InitiateTransferInput): Promise<InitiateResult>;
|
|
157
|
+
initiateTransfer(input: InitiateTransferInput, opts?: CallOptions): Promise<InitiateResult>;
|
|
119
158
|
/** Create a hosted checkout session (the recommended purchase path). Open the
|
|
120
159
|
* returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
|
|
121
160
|
* handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
|
|
122
161
|
* purchase.completed webhook. */
|
|
123
|
-
createCheckout(input: CreateCheckoutInput): Promise<CreateCheckoutResult>;
|
|
162
|
+
createCheckout(input: CreateCheckoutInput, opts?: CallOptions): Promise<CreateCheckoutResult>;
|
|
124
163
|
/**
|
|
125
164
|
* Direct purchase via the rail selector. Use when you need a specific rail.
|
|
126
165
|
* - rail "platform" (default): standard card. May return status "requires_action"
|
|
@@ -128,17 +167,17 @@ declare class InvoServer {
|
|
|
128
167
|
* - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
|
|
129
168
|
* - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
|
|
130
169
|
*/
|
|
131
|
-
purchaseCurrency(input: PurchaseInput): Promise<PurchaseResult>;
|
|
170
|
+
purchaseCurrency(input: PurchaseInput, opts?: CallOptions): Promise<PurchaseResult>;
|
|
132
171
|
/** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
|
|
133
172
|
confirmPayment(input: {
|
|
134
173
|
paymentIntentId: string;
|
|
135
174
|
orderId?: string;
|
|
136
|
-
}): Promise<ConfirmPaymentResult>;
|
|
175
|
+
}, opts?: CallOptions): Promise<ConfirmPaymentResult>;
|
|
137
176
|
/** Fetch purchase status (order + financial summary + timeline). */
|
|
138
177
|
getOrderDetails(query: {
|
|
139
178
|
orderId?: string;
|
|
140
179
|
transactionId?: string;
|
|
141
|
-
}): Promise<OrderDetailsResult>;
|
|
180
|
+
}, opts?: CallOptions): Promise<OrderDetailsResult>;
|
|
142
181
|
/**
|
|
143
182
|
* Buy an in-game item by SPENDING the player's existing game currency (§4.8).
|
|
144
183
|
* No real money, no payment rail, no passkey — it's a balance debit, authenticated
|
|
@@ -147,15 +186,35 @@ declare class InvoServer {
|
|
|
147
186
|
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
148
187
|
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
149
188
|
*/
|
|
150
|
-
purchaseItem(input: PurchaseItemInput): Promise<PurchaseItemResult>;
|
|
189
|
+
purchaseItem(input: PurchaseItemInput, opts?: CallOptions): Promise<PurchaseItemResult>;
|
|
151
190
|
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
152
|
-
getItemPurchaseHistory(query: ItemHistoryQuery): Promise<ItemHistoryResult>;
|
|
191
|
+
getItemPurchaseHistory(query: ItemHistoryQuery, opts?: CallOptions): Promise<ItemHistoryResult>;
|
|
153
192
|
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
154
193
|
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
155
|
-
getItemOrderDetails(query: ItemOrderQuery): Promise<OrderDetailsResult>;
|
|
194
|
+
getItemOrderDetails(query: ItemOrderQuery, opts?: CallOptions): Promise<OrderDetailsResult>;
|
|
195
|
+
/**
|
|
196
|
+
* Async iterator over a player's entire item-purchase history — pages through
|
|
197
|
+
* `getItemPurchaseHistory` (limit/offset) until exhausted, yielding one row at a time.
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* for await (const row of invo.iterateItemPurchaseHistory({ playerEmail })) { … }
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
iterateItemPurchaseHistory(query: {
|
|
204
|
+
playerEmail: string;
|
|
205
|
+
pageSize?: number;
|
|
206
|
+
}, opts?: CallOptions): AsyncGenerator<Record<string, unknown>, void, unknown>;
|
|
156
207
|
/** Read a player's currency balances, by email or playerId (game-secret). */
|
|
157
|
-
getPlayerBalance(query: PlayerBalanceQuery): Promise<PlayerBalanceResult>;
|
|
208
|
+
getPlayerBalance(query: PlayerBalanceQuery, opts?: CallOptions): Promise<PlayerBalanceResult>;
|
|
209
|
+
/**
|
|
210
|
+
* List LIVE, unclaimed inbound sends/transfers addressed to a player at YOUR game —
|
|
211
|
+
* the source of truth for a "you have X to collect" badge. Pass the player's email
|
|
212
|
+
* or phone. Match `toPhone` to the logged-in player (don't require `toIdentityId`,
|
|
213
|
+
* which is null when the phone maps to more than one of your players). Pairs with the
|
|
214
|
+
* `transfer.claim_pending` webhook (the webhook is the wake-up; this is the list).
|
|
215
|
+
*/
|
|
216
|
+
getInboundPending(query: InboundPendingQuery, opts?: CallOptions): Promise<InboundPendingResult>;
|
|
158
217
|
private toInitiateResult;
|
|
159
218
|
}
|
|
160
219
|
|
|
161
|
-
export { ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, OrderDetailsResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, type TransferEventData, type VerifyWebhookOptions, verifyWebhook };
|
|
220
|
+
export { CallOptions, ConfirmPaymentResult, CreateCheckoutInput, CreateCheckoutResult, InboundPendingQuery, InboundPendingResult, InitiateResult, InitiateSendInput, InitiateTransferInput, InvoError, InvoServer, type InvoWebhookEvent, ItemHistoryQuery, ItemHistoryResult, ItemOrderQuery, type ItemPurchasedData, OrderDetailsResult, PlayerBalanceQuery, PlayerBalanceResult, PlayerToken, type PurchaseCompletedData, type PurchaseEventData, PurchaseInput, PurchaseItemInput, PurchaseItemResult, PurchaseResult, ServerConfig, type TransferEventData, type VerifyWebhookOptions, type WebhookHandlerOptions, createWebhookHandler, verifyWebhook, verifyWebhookAsync };
|