@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.d.ts
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.js';
|
|
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.js';
|
|
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 };
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assertSecureBaseUrl, Http
|
|
2
|
-
export { InvoError } from './chunk-
|
|
1
|
+
import { InvoError, assertSecureBaseUrl, Http } from './chunk-EEWOAUXO.js';
|
|
2
|
+
export { InvoError } from './chunk-EEWOAUXO.js';
|
|
3
3
|
import { createHmac } from 'crypto';
|
|
4
4
|
|
|
5
5
|
var DEFAULT_TOLERANCE_SEC = 300;
|
|
@@ -38,7 +38,7 @@ function safeEqualHex(a, b) {
|
|
|
38
38
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
39
39
|
return diff === 0;
|
|
40
40
|
}
|
|
41
|
-
function
|
|
41
|
+
function prepareVerification(rawBody, signatureHeader, secret, opts) {
|
|
42
42
|
if (!signatureHeader) {
|
|
43
43
|
throw webhookError("Missing X-Invo-Signature header.", "WEBHOOK_SIGNATURE_MISSING");
|
|
44
44
|
}
|
|
@@ -61,13 +61,9 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
const bodyBytes = toBytes(rawBody);
|
|
63
63
|
const message = concatBytes(ENCODER.encode(`${t}.`), bodyBytes);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
if (!matched) {
|
|
69
|
-
throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
|
|
70
|
-
}
|
|
64
|
+
return { secrets, sigs, message, bodyBytes };
|
|
65
|
+
}
|
|
66
|
+
function finalizeEvent(bodyBytes) {
|
|
71
67
|
let parsed;
|
|
72
68
|
try {
|
|
73
69
|
parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
|
|
@@ -79,9 +75,93 @@ function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
|
79
75
|
}
|
|
80
76
|
return parsed;
|
|
81
77
|
}
|
|
78
|
+
function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
79
|
+
const { secrets, sigs, message, bodyBytes } = prepareVerification(
|
|
80
|
+
rawBody,
|
|
81
|
+
signatureHeader,
|
|
82
|
+
secret,
|
|
83
|
+
opts
|
|
84
|
+
);
|
|
85
|
+
const matched = secrets.some((s) => {
|
|
86
|
+
const expected = hmacHex(s, message);
|
|
87
|
+
return sigs.some((sig) => safeEqualHex(sig, expected));
|
|
88
|
+
});
|
|
89
|
+
if (!matched) {
|
|
90
|
+
throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
|
|
91
|
+
}
|
|
92
|
+
return finalizeEvent(bodyBytes);
|
|
93
|
+
}
|
|
94
|
+
async function verifyWebhookAsync(rawBody, signatureHeader, secret, opts = {}) {
|
|
95
|
+
const { secrets, sigs, message, bodyBytes } = prepareVerification(
|
|
96
|
+
rawBody,
|
|
97
|
+
signatureHeader,
|
|
98
|
+
secret,
|
|
99
|
+
opts
|
|
100
|
+
);
|
|
101
|
+
for (const s of secrets) {
|
|
102
|
+
const expected = await hmacHexSubtle(s, message);
|
|
103
|
+
if (sigs.some((sig) => safeEqualHex(sig, expected))) {
|
|
104
|
+
return finalizeEvent(bodyBytes);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw webhookError("Webhook signature verification failed.", "WEBHOOK_SIGNATURE_INVALID");
|
|
108
|
+
}
|
|
109
|
+
function createWebhookHandler(opts) {
|
|
110
|
+
return async (request) => {
|
|
111
|
+
const raw = await request.text();
|
|
112
|
+
let event;
|
|
113
|
+
try {
|
|
114
|
+
event = await verifyWebhookAsync(raw, request.headers.get("x-invo-signature"), opts.secret, {
|
|
115
|
+
toleranceSec: opts.toleranceSec
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
const err = e instanceof InvoError ? e : new InvoError({ message: String(e), status: 0 });
|
|
119
|
+
if (opts.onError) {
|
|
120
|
+
try {
|
|
121
|
+
const r = await opts.onError(err, request);
|
|
122
|
+
if (r instanceof Response) return r;
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return new Response(err.code ?? "invalid_signature", { status: 400 });
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await opts.onEvent(event, {
|
|
130
|
+
// Prefer the signature-covered body field over the (unauthenticated, optional)
|
|
131
|
+
// header — the body value is HMAC-verified and always present.
|
|
132
|
+
idempotencyKey: event.idempotency_key ?? request.headers.get("x-invo-idempotency-key"),
|
|
133
|
+
request
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
return new Response("handler_error", { status: 500 });
|
|
137
|
+
}
|
|
138
|
+
return new Response(null, { status: 200 });
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function hmacHexSubtle(secret, message) {
|
|
142
|
+
const subtle = globalThis.crypto?.subtle;
|
|
143
|
+
if (!subtle) {
|
|
144
|
+
throw webhookError(
|
|
145
|
+
"Web Crypto (crypto.subtle) is unavailable in this runtime.",
|
|
146
|
+
"WEBHOOK_UNSUPPORTED"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const key = await subtle.importKey(
|
|
150
|
+
"raw",
|
|
151
|
+
ENCODER.encode(secret),
|
|
152
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
153
|
+
false,
|
|
154
|
+
["sign"]
|
|
155
|
+
);
|
|
156
|
+
const sig = await subtle.sign("HMAC", key, message);
|
|
157
|
+
const bytes = new Uint8Array(sig);
|
|
158
|
+
let hex = "";
|
|
159
|
+
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
160
|
+
return hex;
|
|
161
|
+
}
|
|
82
162
|
|
|
83
163
|
// src/server.ts
|
|
84
|
-
var DEFAULT_UA = "invonetwork-web-sdk/0.
|
|
164
|
+
var DEFAULT_UA = "invonetwork-web-sdk/0.4.0 (+https://invo.network)";
|
|
85
165
|
var MAX_USD_AMOUNT = 999.99;
|
|
86
166
|
var MAX_ITEM_PRICE = 999999.99;
|
|
87
167
|
function invalidInput(label, value, why) {
|
|
@@ -121,6 +201,21 @@ function toOrderDetails(raw) {
|
|
|
121
201
|
raw
|
|
122
202
|
};
|
|
123
203
|
}
|
|
204
|
+
function toInboundPendingItem(row) {
|
|
205
|
+
return {
|
|
206
|
+
transactionId: String(row["transaction_id"] ?? ""),
|
|
207
|
+
flow: String(row["flow"] ?? ""),
|
|
208
|
+
amount: String(row["amount"] ?? ""),
|
|
209
|
+
netAmount: String(row["net_amount"] ?? ""),
|
|
210
|
+
sourceGameId: row["source_game_id"] ?? "",
|
|
211
|
+
sourceGame: String(row["source_game"] ?? ""),
|
|
212
|
+
toPhone: String(row["to_phone"] ?? ""),
|
|
213
|
+
toIdentityId: row["to_identity_id"] ?? null,
|
|
214
|
+
createdAt: String(row["created_at"] ?? ""),
|
|
215
|
+
claimCodeExpiresAt: String(row["claim_code_expires_at"] ?? ""),
|
|
216
|
+
raw: row
|
|
217
|
+
};
|
|
218
|
+
}
|
|
124
219
|
function toCurrencyBalance(row) {
|
|
125
220
|
return {
|
|
126
221
|
currencyId: row["currency_id"] ?? "",
|
|
@@ -163,7 +258,7 @@ var InvoServer = class {
|
|
|
163
258
|
this.auth = { kind: "game-secret", secret: config.gameSecret };
|
|
164
259
|
}
|
|
165
260
|
/** Mint a short-lived, game-scoped player token to hand to the browser InvoClient. */
|
|
166
|
-
async mintPlayerToken(input) {
|
|
261
|
+
async mintPlayerToken(input, opts) {
|
|
167
262
|
if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
|
|
168
263
|
throw invalidInput("playerEmail", input.playerEmail, "is required");
|
|
169
264
|
}
|
|
@@ -171,7 +266,7 @@ var InvoServer = class {
|
|
|
171
266
|
"/api/sdk/player-token",
|
|
172
267
|
{ player_email: input.playerEmail },
|
|
173
268
|
this.auth,
|
|
174
|
-
{ idempotent: true }
|
|
269
|
+
{ idempotent: true, signal: opts?.signal }
|
|
175
270
|
// safe to retry: re-mint just issues a fresh token
|
|
176
271
|
);
|
|
177
272
|
return {
|
|
@@ -181,7 +276,7 @@ var InvoServer = class {
|
|
|
181
276
|
};
|
|
182
277
|
}
|
|
183
278
|
/** Initiate a cross-game currency SEND. Inspect result.verificationMethod. */
|
|
184
|
-
async initiateSend(input) {
|
|
279
|
+
async initiateSend(input, opts) {
|
|
185
280
|
const raw = await this.http.post(
|
|
186
281
|
"/api/currency-sends/initiate-send",
|
|
187
282
|
{
|
|
@@ -195,13 +290,13 @@ var InvoServer = class {
|
|
|
195
290
|
amount: input.amount
|
|
196
291
|
},
|
|
197
292
|
this.auth,
|
|
198
|
-
{ idempotent: true }
|
|
293
|
+
{ idempotent: true, signal: opts?.signal }
|
|
199
294
|
// client_request_id makes this safe to retry (backend dedupes)
|
|
200
295
|
);
|
|
201
296
|
return this.toInitiateResult(raw);
|
|
202
297
|
}
|
|
203
298
|
/** Initiate a cross-game TRANSFER. Inspect result.verificationMethod. */
|
|
204
|
-
async initiateTransfer(input) {
|
|
299
|
+
async initiateTransfer(input, opts) {
|
|
205
300
|
const raw = await this.http.post(
|
|
206
301
|
"/api/transfers/initiate-transfer",
|
|
207
302
|
{
|
|
@@ -215,7 +310,7 @@ var InvoServer = class {
|
|
|
215
310
|
amount: input.amount
|
|
216
311
|
},
|
|
217
312
|
this.auth,
|
|
218
|
-
{ idempotent: true }
|
|
313
|
+
{ idempotent: true, signal: opts?.signal }
|
|
219
314
|
// client_request_id makes this safe to retry (backend dedupes)
|
|
220
315
|
);
|
|
221
316
|
return this.toInitiateResult(raw);
|
|
@@ -224,7 +319,7 @@ var InvoServer = class {
|
|
|
224
319
|
* returned checkoutUrl in a WebView/redirect or an iframe; the INVO-hosted page
|
|
225
320
|
* handles cards, saved cards, and 3-D Secure. Crediting is server-side via the
|
|
226
321
|
* purchase.completed webhook. */
|
|
227
|
-
async createCheckout(input) {
|
|
322
|
+
async createCheckout(input, opts) {
|
|
228
323
|
if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
|
|
229
324
|
throw invalidInput("playerEmail", input.playerEmail, "is required");
|
|
230
325
|
}
|
|
@@ -240,7 +335,9 @@ var InvoServer = class {
|
|
|
240
335
|
const raw = await this.http.post(
|
|
241
336
|
"/api/checkout/sessions",
|
|
242
337
|
body,
|
|
243
|
-
this.auth
|
|
338
|
+
this.auth,
|
|
339
|
+
{ signal: opts?.signal }
|
|
340
|
+
// not idempotent — never auto-retried
|
|
244
341
|
);
|
|
245
342
|
return {
|
|
246
343
|
sessionId: String(raw["session_id"] ?? ""),
|
|
@@ -256,7 +353,7 @@ var InvoServer = class {
|
|
|
256
353
|
* - rail "game": returns status "pending_payment" + paymentUrl (redirect the browser).
|
|
257
354
|
* - rail "steam": NOT a browser flow — the backend returns WRONG_RAIL_ENDPOINT.
|
|
258
355
|
*/
|
|
259
|
-
async purchaseCurrency(input) {
|
|
356
|
+
async purchaseCurrency(input, opts) {
|
|
260
357
|
if (typeof input.playerEmail !== "string" || !input.playerEmail.trim()) {
|
|
261
358
|
throw invalidInput("playerEmail", input.playerEmail, "is required");
|
|
262
359
|
}
|
|
@@ -289,7 +386,7 @@ var InvoServer = class {
|
|
|
289
386
|
"/api/currency-purchases/purchase-currency",
|
|
290
387
|
body,
|
|
291
388
|
this.auth,
|
|
292
|
-
{ idempotent: true }
|
|
389
|
+
{ idempotent: true, signal: opts?.signal }
|
|
293
390
|
// purchase_reference makes this safe to retry (backend dedupes)
|
|
294
391
|
);
|
|
295
392
|
return {
|
|
@@ -304,14 +401,14 @@ var InvoServer = class {
|
|
|
304
401
|
};
|
|
305
402
|
}
|
|
306
403
|
/** Complete the Stripe-rail 3-D Secure step after the client finished card action. */
|
|
307
|
-
async confirmPayment(input) {
|
|
404
|
+
async confirmPayment(input, opts) {
|
|
308
405
|
const body = { payment_intent_id: input.paymentIntentId };
|
|
309
406
|
if (input.orderId) body["order_id"] = input.orderId;
|
|
310
407
|
const raw = await this.http.post(
|
|
311
408
|
"/api/currency-purchases/confirm-payment",
|
|
312
409
|
body,
|
|
313
410
|
this.auth,
|
|
314
|
-
{ idempotent: true }
|
|
411
|
+
{ idempotent: true, signal: opts?.signal }
|
|
315
412
|
// keyed by payment_intent_id — safe to retry
|
|
316
413
|
);
|
|
317
414
|
return {
|
|
@@ -322,7 +419,7 @@ var InvoServer = class {
|
|
|
322
419
|
};
|
|
323
420
|
}
|
|
324
421
|
/** Fetch purchase status (order + financial summary + timeline). */
|
|
325
|
-
async getOrderDetails(query) {
|
|
422
|
+
async getOrderDetails(query, opts) {
|
|
326
423
|
if (!query.orderId && !query.transactionId) {
|
|
327
424
|
throw new InvoError({
|
|
328
425
|
message: "getOrderDetails requires an `orderId` or `transactionId`.",
|
|
@@ -335,7 +432,8 @@ var InvoServer = class {
|
|
|
335
432
|
if (query.transactionId) q.set("transaction_id", query.transactionId);
|
|
336
433
|
const raw = await this.http.get(
|
|
337
434
|
`/api/currency-purchases/order-details?${q.toString()}`,
|
|
338
|
-
this.auth
|
|
435
|
+
this.auth,
|
|
436
|
+
{ signal: opts?.signal }
|
|
339
437
|
);
|
|
340
438
|
return toOrderDetails(raw);
|
|
341
439
|
}
|
|
@@ -347,7 +445,7 @@ var InvoServer = class {
|
|
|
347
445
|
* (err.isInsufficientBalance; required_amount/current_balance on err.body). Grant the
|
|
348
446
|
* item to your inventory off the `item.purchased` webhook, not just this response.
|
|
349
447
|
*/
|
|
350
|
-
async purchaseItem(input) {
|
|
448
|
+
async purchaseItem(input, opts) {
|
|
351
449
|
const required = [
|
|
352
450
|
["clientRequestId", input.clientRequestId],
|
|
353
451
|
["playerEmail", input.playerEmail],
|
|
@@ -389,7 +487,7 @@ var InvoServer = class {
|
|
|
389
487
|
"/api/item-purchases/purchase-item",
|
|
390
488
|
body,
|
|
391
489
|
this.auth,
|
|
392
|
-
{ idempotent: true }
|
|
490
|
+
{ idempotent: true, signal: opts?.signal }
|
|
393
491
|
// client_request_id makes this safe to retry (dup → 409)
|
|
394
492
|
);
|
|
395
493
|
return {
|
|
@@ -404,7 +502,7 @@ var InvoServer = class {
|
|
|
404
502
|
};
|
|
405
503
|
}
|
|
406
504
|
/** Paginated item-purchase history for a player (§4.8 companion read). */
|
|
407
|
-
async getItemPurchaseHistory(query) {
|
|
505
|
+
async getItemPurchaseHistory(query, opts) {
|
|
408
506
|
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
409
507
|
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
410
508
|
}
|
|
@@ -414,7 +512,8 @@ var InvoServer = class {
|
|
|
414
512
|
if (query.offset != null) q.set("offset", String(query.offset));
|
|
415
513
|
const raw = await this.http.get(
|
|
416
514
|
`/api/item-purchases/player-purchase-history?${q.toString()}`,
|
|
417
|
-
this.auth
|
|
515
|
+
this.auth,
|
|
516
|
+
{ signal: opts?.signal }
|
|
418
517
|
);
|
|
419
518
|
return {
|
|
420
519
|
history: Array.isArray(raw["item_purchase_history"]) ? raw["item_purchase_history"] : [],
|
|
@@ -424,7 +523,7 @@ var InvoServer = class {
|
|
|
424
523
|
}
|
|
425
524
|
/** Look up one item order by EXACTLY ONE of orderId | transactionId | clientRequestId
|
|
426
525
|
* (§4.8). Use clientRequestId for saga/recovery: "did this purchase actually complete?" */
|
|
427
|
-
async getItemOrderDetails(query) {
|
|
526
|
+
async getItemOrderDetails(query, opts) {
|
|
428
527
|
const provided = [query.orderId, query.transactionId, query.clientRequestId].filter(
|
|
429
528
|
(v) => typeof v === "string" && v.trim()
|
|
430
529
|
);
|
|
@@ -441,12 +540,40 @@ var InvoServer = class {
|
|
|
441
540
|
if (query.clientRequestId) q.set("client_request_id", query.clientRequestId);
|
|
442
541
|
const raw = await this.http.get(
|
|
443
542
|
`/api/item-purchases/order-details?${q.toString()}`,
|
|
444
|
-
this.auth
|
|
543
|
+
this.auth,
|
|
544
|
+
{ signal: opts?.signal }
|
|
445
545
|
);
|
|
446
546
|
return toOrderDetails(raw);
|
|
447
547
|
}
|
|
548
|
+
/**
|
|
549
|
+
* Async iterator over a player's entire item-purchase history — pages through
|
|
550
|
+
* `getItemPurchaseHistory` (limit/offset) until exhausted, yielding one row at a time.
|
|
551
|
+
*
|
|
552
|
+
* ```ts
|
|
553
|
+
* for await (const row of invo.iterateItemPurchaseHistory({ playerEmail })) { … }
|
|
554
|
+
* ```
|
|
555
|
+
*/
|
|
556
|
+
async *iterateItemPurchaseHistory(query, opts) {
|
|
557
|
+
if (typeof query.playerEmail !== "string" || !query.playerEmail.trim()) {
|
|
558
|
+
throw invalidInput("playerEmail", query.playerEmail, "is required");
|
|
559
|
+
}
|
|
560
|
+
const pageSize = typeof query.pageSize === "number" && query.pageSize > 0 ? Math.floor(query.pageSize) : 50;
|
|
561
|
+
let yielded = 0;
|
|
562
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
563
|
+
const { history, pagination } = await this.getItemPurchaseHistory(
|
|
564
|
+
{ playerEmail: query.playerEmail, limit: pageSize, offset },
|
|
565
|
+
opts
|
|
566
|
+
);
|
|
567
|
+
if (history.length === 0) return;
|
|
568
|
+
for (const row of history) yield row;
|
|
569
|
+
yielded += history.length;
|
|
570
|
+
if (history.length < pageSize) return;
|
|
571
|
+
const total = Number(pagination?.["total"]);
|
|
572
|
+
if (Number.isFinite(total) && yielded >= total) return;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
448
575
|
/** Read a player's currency balances, by email or playerId (game-secret). */
|
|
449
|
-
async getPlayerBalance(query) {
|
|
576
|
+
async getPlayerBalance(query, opts) {
|
|
450
577
|
let path;
|
|
451
578
|
if (query.playerId != null && String(query.playerId).trim()) {
|
|
452
579
|
path = `/api/player-balances/player/${encodeURIComponent(String(query.playerId))}`;
|
|
@@ -459,7 +586,7 @@ var InvoServer = class {
|
|
|
459
586
|
status: 0
|
|
460
587
|
});
|
|
461
588
|
}
|
|
462
|
-
const raw = await this.http.get(path, this.auth);
|
|
589
|
+
const raw = await this.http.get(path, this.auth, { signal: opts?.signal });
|
|
463
590
|
const rows = Array.isArray(raw["balances"]) ? raw["balances"] : [];
|
|
464
591
|
return {
|
|
465
592
|
player: raw["player"] ?? {},
|
|
@@ -468,6 +595,30 @@ var InvoServer = class {
|
|
|
468
595
|
raw
|
|
469
596
|
};
|
|
470
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* List LIVE, unclaimed inbound sends/transfers addressed to a player at YOUR game —
|
|
600
|
+
* the source of truth for a "you have X to collect" badge. Pass the player's email
|
|
601
|
+
* or phone. Match `toPhone` to the logged-in player (don't require `toIdentityId`,
|
|
602
|
+
* which is null when the phone maps to more than one of your players). Pairs with the
|
|
603
|
+
* `transfer.claim_pending` webhook (the webhook is the wake-up; this is the list).
|
|
604
|
+
*/
|
|
605
|
+
async getInboundPending(query, opts) {
|
|
606
|
+
const hasEmail = typeof query.playerEmail === "string" && query.playerEmail.trim();
|
|
607
|
+
const hasPhone = typeof query.playerPhone === "string" && query.playerPhone.trim();
|
|
608
|
+
if (!hasEmail && !hasPhone) {
|
|
609
|
+
throw invalidInput("query", query, "requires a playerEmail or playerPhone");
|
|
610
|
+
}
|
|
611
|
+
const q = new URLSearchParams();
|
|
612
|
+
if (hasEmail) q.set("player_email", query.playerEmail.trim());
|
|
613
|
+
if (hasPhone) q.set("player_phone", query.playerPhone.trim());
|
|
614
|
+
const raw = await this.http.get(
|
|
615
|
+
`/api/transfers/inbound-pending?${q.toString()}`,
|
|
616
|
+
this.auth,
|
|
617
|
+
{ signal: opts?.signal }
|
|
618
|
+
);
|
|
619
|
+
const rows = Array.isArray(raw["inbound_pending"]) ? raw["inbound_pending"] : [];
|
|
620
|
+
return { inboundPending: rows.map(toInboundPendingItem), raw };
|
|
621
|
+
}
|
|
471
622
|
toInitiateResult(raw) {
|
|
472
623
|
const vm = raw["verification_method"];
|
|
473
624
|
const guardian = raw["guardian_approval"];
|
|
@@ -480,6 +631,6 @@ var InvoServer = class {
|
|
|
480
631
|
}
|
|
481
632
|
};
|
|
482
633
|
|
|
483
|
-
export { InvoServer, verifyWebhook };
|
|
634
|
+
export { InvoServer, createWebhookHandler, verifyWebhook, verifyWebhookAsync };
|
|
484
635
|
//# sourceMappingURL=server.js.map
|
|
485
636
|
//# sourceMappingURL=server.js.map
|
|
@@ -66,6 +66,11 @@ interface InvoHooks {
|
|
|
66
66
|
onError?: (info: InvoErrorInfo) => void;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Per-call options (e.g. cancellation). Accepted as an optional last arg on SDK methods. */
|
|
70
|
+
interface CallOptions {
|
|
71
|
+
/** Cancel this call. Aborting throws an InvoError (code "ABORTED"); aborted calls are never retried. */
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
}
|
|
69
74
|
/** Public, neutral payment-rail names. Provider/processor names are an internal
|
|
70
75
|
* backend routing detail and are deliberately NOT exposed here. */
|
|
71
76
|
type Rail = "platform" | "game" | "steam";
|
|
@@ -268,6 +273,32 @@ interface PlayerBalanceResult {
|
|
|
268
273
|
summary: Record<string, unknown>;
|
|
269
274
|
raw: Record<string, unknown>;
|
|
270
275
|
}
|
|
276
|
+
interface InboundPendingQuery {
|
|
277
|
+
/** Provide one: the player's email or phone. */
|
|
278
|
+
playerEmail?: string;
|
|
279
|
+
playerPhone?: string;
|
|
280
|
+
}
|
|
281
|
+
interface InboundPendingItem {
|
|
282
|
+
transactionId: string;
|
|
283
|
+
/** "transfer" | "send". */
|
|
284
|
+
flow: string;
|
|
285
|
+
amount: string;
|
|
286
|
+
/** What the recipient receives (after fees). */
|
|
287
|
+
netAmount: string;
|
|
288
|
+
sourceGameId: string | number;
|
|
289
|
+
sourceGame: string;
|
|
290
|
+
/** Match to the logged-in player's phone. */
|
|
291
|
+
toPhone: string;
|
|
292
|
+
/** Recipient identity; null when the phone maps to >1 of your players. */
|
|
293
|
+
toIdentityId: string | null;
|
|
294
|
+
createdAt: string;
|
|
295
|
+
claimCodeExpiresAt: string;
|
|
296
|
+
raw: Record<string, unknown>;
|
|
297
|
+
}
|
|
298
|
+
interface InboundPendingResult {
|
|
299
|
+
inboundPending: InboundPendingItem[];
|
|
300
|
+
raw: Record<string, unknown>;
|
|
301
|
+
}
|
|
271
302
|
interface ApproveResult {
|
|
272
303
|
status: string;
|
|
273
304
|
next: string;
|
|
@@ -287,4 +318,4 @@ interface LinkDeviceResult {
|
|
|
287
318
|
raw: Record<string, unknown>;
|
|
288
319
|
}
|
|
289
320
|
|
|
290
|
-
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type OrderDetailsResult as O, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type
|
|
321
|
+
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type OrderDetailsResult as O, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type CallOptions as a, type ConfirmReceiptResult as b, type InvoErrorInfo as c, type InvoHooks as d, type InvoRequestInfo as e, type InvoResponseInfo as f, type InitiateSendInput as g, type InitiateResult as h, type InitiateTransferInput as i, type CreateCheckoutInput as j, type CreateCheckoutResult as k, type PurchaseInput as l, type PurchaseResult as m, type ConfirmPaymentResult as n, type PurchaseItemInput as o, type PurchaseItemResult as p, type ItemHistoryQuery as q, type ItemHistoryResult as r, type ItemOrderQuery as s, type PlayerBalanceQuery as t, type PlayerBalanceResult as u, type InboundPendingQuery as v, type InboundPendingResult as w, type CurrencyBalance as x, type InboundPendingItem as y };
|
|
@@ -66,6 +66,11 @@ interface InvoHooks {
|
|
|
66
66
|
onError?: (info: InvoErrorInfo) => void;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Per-call options (e.g. cancellation). Accepted as an optional last arg on SDK methods. */
|
|
70
|
+
interface CallOptions {
|
|
71
|
+
/** Cancel this call. Aborting throws an InvoError (code "ABORTED"); aborted calls are never retried. */
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
}
|
|
69
74
|
/** Public, neutral payment-rail names. Provider/processor names are an internal
|
|
70
75
|
* backend routing detail and are deliberately NOT exposed here. */
|
|
71
76
|
type Rail = "platform" | "game" | "steam";
|
|
@@ -268,6 +273,32 @@ interface PlayerBalanceResult {
|
|
|
268
273
|
summary: Record<string, unknown>;
|
|
269
274
|
raw: Record<string, unknown>;
|
|
270
275
|
}
|
|
276
|
+
interface InboundPendingQuery {
|
|
277
|
+
/** Provide one: the player's email or phone. */
|
|
278
|
+
playerEmail?: string;
|
|
279
|
+
playerPhone?: string;
|
|
280
|
+
}
|
|
281
|
+
interface InboundPendingItem {
|
|
282
|
+
transactionId: string;
|
|
283
|
+
/** "transfer" | "send". */
|
|
284
|
+
flow: string;
|
|
285
|
+
amount: string;
|
|
286
|
+
/** What the recipient receives (after fees). */
|
|
287
|
+
netAmount: string;
|
|
288
|
+
sourceGameId: string | number;
|
|
289
|
+
sourceGame: string;
|
|
290
|
+
/** Match to the logged-in player's phone. */
|
|
291
|
+
toPhone: string;
|
|
292
|
+
/** Recipient identity; null when the phone maps to >1 of your players. */
|
|
293
|
+
toIdentityId: string | null;
|
|
294
|
+
createdAt: string;
|
|
295
|
+
claimCodeExpiresAt: string;
|
|
296
|
+
raw: Record<string, unknown>;
|
|
297
|
+
}
|
|
298
|
+
interface InboundPendingResult {
|
|
299
|
+
inboundPending: InboundPendingItem[];
|
|
300
|
+
raw: Record<string, unknown>;
|
|
301
|
+
}
|
|
271
302
|
interface ApproveResult {
|
|
272
303
|
status: string;
|
|
273
304
|
next: string;
|
|
@@ -287,4 +318,4 @@ interface LinkDeviceResult {
|
|
|
287
318
|
raw: Record<string, unknown>;
|
|
288
319
|
}
|
|
289
320
|
|
|
290
|
-
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type OrderDetailsResult as O, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type
|
|
321
|
+
export { type ApproveResult as A, type ClientConfig as C, InvoError as I, type LinkDeviceResult as L, type OrderDetailsResult as O, type PlayerToken as P, type Rail as R, type ServerConfig as S, type VerificationMethod as V, type CallOptions as a, type ConfirmReceiptResult as b, type InvoErrorInfo as c, type InvoHooks as d, type InvoRequestInfo as e, type InvoResponseInfo as f, type InitiateSendInput as g, type InitiateResult as h, type InitiateTransferInput as i, type CreateCheckoutInput as j, type CreateCheckoutResult as k, type PurchaseInput as l, type PurchaseResult as m, type ConfirmPaymentResult as n, type PurchaseItemInput as o, type PurchaseItemResult as p, type ItemHistoryQuery as q, type ItemHistoryResult as r, type ItemOrderQuery as s, type PlayerBalanceQuery as t, type PlayerBalanceResult as u, type InboundPendingQuery as v, type InboundPendingResult as w, type CurrencyBalance as x, type InboundPendingItem as y };
|