@invonetwork/web-sdk 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,71 @@
1
+ /**
2
+ * Typed error for every INVO backend failure.
3
+ *
4
+ * The backend error envelope is usually `{ "error": <message>, "code": <STABLE_CODE> }`,
5
+ * but a few txn-state/claim errors carry only `error` (no `code`) and some use
6
+ * `{ "status": "error", "message": ... }`. So `code` may be undefined — branch on
7
+ * `code` when present, else fall back to `message`. See the handoff doc §8.
8
+ */
9
+ declare class InvoError extends Error {
10
+ /** Stable machine code from the backend (`code`), when present. */
11
+ readonly code: string | undefined;
12
+ /** HTTP status. */
13
+ readonly status: number;
14
+ /** Raw parsed body, for debugging / fields not surfaced here. */
15
+ readonly body: unknown;
16
+ /** Backend request id (from the response headers), when present — quote it in support tickets. */
17
+ readonly requestId: string | undefined;
18
+ constructor(args: {
19
+ message: string;
20
+ code?: string;
21
+ status: number;
22
+ body?: unknown;
23
+ requestId?: string;
24
+ });
25
+ /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
26
+ get isReceiverNotEnrolled(): boolean;
27
+ /** True if the session/SDK token has expired and the caller should re-mint + retry. */
28
+ get isTokenExpired(): boolean;
29
+ /**
30
+ * True if an item purchase failed because the player's balance was too low (§4.8 → 400).
31
+ * The backend carries `required_amount` + `current_balance` on the body for the UI.
32
+ * Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
33
+ * is a rate-limit, not a top-up condition) is NOT misclassified as this.
34
+ */
35
+ get isInsufficientBalance(): boolean;
36
+ /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
37
+ get isDuplicateRequest(): boolean;
38
+ /** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
39
+ get retryAfter(): number | undefined;
40
+ /** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
41
+ * The `in` operator throws on primitives, so callers must go through this. */
42
+ private bodyObject;
43
+ }
44
+
45
+ interface InvoRequestInfo {
46
+ method: string;
47
+ url: string;
48
+ /** 0-based attempt number (0 = first try, 1 = first retry, …). */
49
+ attempt: number;
50
+ }
51
+ interface InvoResponseInfo extends InvoRequestInfo {
52
+ status: number;
53
+ durationMs: number;
54
+ /** Backend request id from the response headers, when present. */
55
+ requestId?: string;
56
+ }
57
+ interface InvoErrorInfo extends InvoRequestInfo {
58
+ error: InvoError;
59
+ /** Whether the SDK will retry after this error. */
60
+ willRetry: boolean;
61
+ }
62
+ /** Optional observability callbacks. All are best-effort — a throwing hook is swallowed. */
63
+ interface InvoHooks {
64
+ onRequest?: (info: InvoRequestInfo) => void;
65
+ onResponse?: (info: InvoResponseInfo) => void;
66
+ onError?: (info: InvoErrorInfo) => void;
67
+ }
68
+
1
69
  /** Public, neutral payment-rail names. Provider/processor names are an internal
2
70
  * backend routing detail and are deliberately NOT exposed here. */
3
71
  type Rail = "platform" | "game" | "steam";
@@ -10,6 +78,16 @@ interface InvoConfig {
10
78
  timeoutMs?: number;
11
79
  /** Optional fetch override (e.g. a custom agent). Defaults to global fetch. */
12
80
  fetch?: typeof fetch;
81
+ /**
82
+ * Max automatic retries on transient failures — network error/timeout, `429`
83
+ * (honoring `retry_after`), and `5xx`. Default 2. Set 0 to disable. Mutating
84
+ * calls carry idempotency keys, so retries are safe.
85
+ */
86
+ maxRetries?: number;
87
+ /** Base backoff delay in ms (exponential + jitter). Default 250. */
88
+ retryBaseDelayMs?: number;
89
+ /** Optional observability callbacks: onRequest / onResponse / onError (best-effort). */
90
+ hooks?: InvoHooks;
13
91
  }
14
92
  interface ServerConfig extends InvoConfig {
15
93
  /** The game secret. Server-side ONLY — never ship this to a browser. */
@@ -107,6 +185,24 @@ interface PurchaseResult {
107
185
  newBalance?: string | number | null;
108
186
  raw: Record<string, unknown>;
109
187
  }
188
+ interface ConfirmPaymentResult {
189
+ status: string;
190
+ transactionId?: string;
191
+ newBalance?: string | number | null;
192
+ raw: Record<string, unknown>;
193
+ }
194
+ /** Shape of the order-details reads (currency purchase + item purchase). */
195
+ interface OrderDetailsResult {
196
+ order: Record<string, unknown>;
197
+ financialSummary: Record<string, unknown>;
198
+ statusTimeline: unknown;
199
+ raw: Record<string, unknown>;
200
+ }
201
+ interface ItemHistoryResult {
202
+ history: Record<string, unknown>[];
203
+ pagination: Record<string, unknown>;
204
+ raw: Record<string, unknown>;
205
+ }
110
206
  interface PurchaseItemInput {
111
207
  /** Idempotency key, unique per game. Required. A duplicate returns 409. */
112
208
  clientRequestId: string;
@@ -148,6 +244,30 @@ interface ItemOrderQuery {
148
244
  transactionId?: string;
149
245
  clientRequestId?: string;
150
246
  }
247
+ interface PlayerBalanceQuery {
248
+ /** Look up by email, or by playerId — provide one. */
249
+ playerEmail?: string;
250
+ playerId?: string | number;
251
+ }
252
+ interface CurrencyBalance {
253
+ currencyId: string | number;
254
+ currencyName: string;
255
+ currencySymbol: string;
256
+ availableBalance: string;
257
+ reservedBalance: string;
258
+ totalBalance: string;
259
+ lastTransaction?: string;
260
+ /** Raw balance row for any field not surfaced here. */
261
+ raw: Record<string, unknown>;
262
+ }
263
+ interface PlayerBalanceResult {
264
+ /** { player_id, player_name, player_email, date_joined, last_active }. */
265
+ player: Record<string, unknown>;
266
+ balances: CurrencyBalance[];
267
+ /** { total_currencies, total_value_usd, has_funds, last_updated }. */
268
+ summary: Record<string, unknown>;
269
+ raw: Record<string, unknown>;
270
+ }
151
271
  interface ApproveResult {
152
272
  status: string;
153
273
  next: string;
@@ -167,45 +287,4 @@ interface LinkDeviceResult {
167
287
  raw: Record<string, unknown>;
168
288
  }
169
289
 
170
- /**
171
- * Typed error for every INVO backend failure.
172
- *
173
- * The backend error envelope is usually `{ "error": <message>, "code": <STABLE_CODE> }`,
174
- * but a few txn-state/claim errors carry only `error` (no `code`) and some use
175
- * `{ "status": "error", "message": ... }`. So `code` may be undefined — branch on
176
- * `code` when present, else fall back to `message`. See the handoff doc §8.
177
- */
178
- declare class InvoError extends Error {
179
- /** Stable machine code from the backend (`code`), when present. */
180
- readonly code: string | undefined;
181
- /** HTTP status. */
182
- readonly status: number;
183
- /** Raw parsed body, for debugging / fields not surfaced here. */
184
- readonly body: unknown;
185
- constructor(args: {
186
- message: string;
187
- code?: string;
188
- status: number;
189
- body?: unknown;
190
- });
191
- /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
192
- get isReceiverNotEnrolled(): boolean;
193
- /** True if the session/SDK token has expired and the caller should re-mint + retry. */
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;
209
- }
210
-
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 };
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 ConfirmReceiptResult as a, type InvoErrorInfo as b, type InvoHooks as c, type InvoRequestInfo as d, type InvoResponseInfo as e, type InitiateSendInput as f, type InitiateResult as g, type InitiateTransferInput as h, type CreateCheckoutInput as i, type CreateCheckoutResult as j, type PurchaseInput as k, type PurchaseResult as l, type ConfirmPaymentResult as m, type PurchaseItemInput as n, type PurchaseItemResult as o, type ItemHistoryQuery as p, type ItemHistoryResult as q, type ItemOrderQuery as r, type PlayerBalanceQuery as s, type PlayerBalanceResult as t, type CurrencyBalance as u };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@invonetwork/web-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "INVO Web SDK — currency purchase + passkey (WebAuthn) verification for partner web platforms.",
5
- "license": "UNLICENSED",
5
+ "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,
7
7
  "repository": {
8
8
  "type": "git",
@@ -1,147 +0,0 @@
1
- // src/shared/errors.ts
2
- var InvoError = class _InvoError extends Error {
3
- constructor(args) {
4
- super(args.message);
5
- this.name = "InvoError";
6
- this.code = args.code;
7
- this.status = args.status;
8
- this.body = args.body ?? null;
9
- Object.setPrototypeOf(this, _InvoError.prototype);
10
- }
11
- /** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
12
- get isReceiverNotEnrolled() {
13
- return this.code === "receiver_not_enrolled_use_claim_code" || /receiver_not_enrolled_use_claim_code/i.test(this.message);
14
- }
15
- /** True if the session/SDK token has expired and the caller should re-mint + retry. */
16
- get isTokenExpired() {
17
- return this.code === "SDK_TOKEN_EXPIRED";
18
- }
19
- /**
20
- * True if an item purchase failed because the player's balance was too low (§4.8 → 400).
21
- * The backend carries `required_amount` + `current_balance` on the body for the UI.
22
- * Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
23
- * is a rate-limit, not a top-up condition) is NOT misclassified as this.
24
- */
25
- get isInsufficientBalance() {
26
- if (this.status !== 400) return false;
27
- const b = this.bodyObject();
28
- return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
29
- }
30
- /** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
31
- get isDuplicateRequest() {
32
- return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
33
- }
34
- /** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
35
- get retryAfter() {
36
- const v = this.bodyObject()["retry_after"];
37
- const n = typeof v === "string" ? Number(v) : v;
38
- return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
39
- }
40
- /** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
41
- * The `in` operator throws on primitives, so callers must go through this. */
42
- bodyObject() {
43
- return this.body && typeof this.body === "object" ? this.body : {};
44
- }
45
- };
46
- function errorFromResponse(status, body) {
47
- let message = `INVO request failed (HTTP ${status})`;
48
- let code;
49
- if (body && typeof body === "object") {
50
- const b = body;
51
- if (typeof b["code"] === "string") code = b["code"];
52
- if (typeof b["error"] === "string") message = b["error"];
53
- else if (typeof b["message"] === "string") message = b["message"];
54
- } else if (typeof body === "string" && body.trim()) {
55
- message = body;
56
- }
57
- return new InvoError({ message, code, status, body });
58
- }
59
-
60
- // src/shared/http.ts
61
- var DEFAULT_TIMEOUT = 3e4;
62
- function assertSecureBaseUrl(baseUrl) {
63
- let u;
64
- try {
65
- u = new URL(baseUrl);
66
- } catch {
67
- throw new Error(`Invalid baseUrl: ${baseUrl}`);
68
- }
69
- const isLocal = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
70
- if (u.protocol === "https:" || u.protocol === "http:" && isLocal) return;
71
- throw new Error(
72
- `baseUrl must use https:// (got "${u.protocol}//"). Plaintext would expose the token/secret on the wire.`
73
- );
74
- }
75
- var Http = class {
76
- constructor(opts) {
77
- this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
78
- this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT;
79
- const f = opts.fetchImpl ?? globalThis.fetch;
80
- if (typeof f !== "function") {
81
- throw new Error(
82
- "No fetch implementation available. Use Node >=18, or pass `fetch` in the config."
83
- );
84
- }
85
- this.fetchImpl = f;
86
- this.userAgent = opts.userAgent;
87
- }
88
- async post(path, body, auth) {
89
- return this.request("POST", path, body, auth);
90
- }
91
- async get(path, auth) {
92
- return this.request("GET", path, void 0, auth);
93
- }
94
- authHeaders(auth) {
95
- switch (auth.kind) {
96
- case "game-secret":
97
- return { "X-Game-Secret-Key": auth.secret };
98
- case "bearer":
99
- return { Authorization: `Bearer ${auth.token}` };
100
- case "none":
101
- return {};
102
- }
103
- }
104
- async request(method, path, body, auth) {
105
- const url = `${this.baseUrl}${path}`;
106
- const headers = {
107
- Accept: "application/json",
108
- ...this.authHeaders(auth)
109
- };
110
- if (this.userAgent) headers["User-Agent"] = this.userAgent;
111
- if (body !== void 0) headers["Content-Type"] = "application/json";
112
- const controller = new AbortController();
113
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
114
- let res;
115
- try {
116
- res = await this.fetchImpl(url, {
117
- method,
118
- headers,
119
- body: body !== void 0 ? JSON.stringify(body) : void 0,
120
- signal: controller.signal
121
- });
122
- } catch (err) {
123
- throw new InvoError({
124
- message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
125
- status: 0,
126
- body: null
127
- });
128
- } finally {
129
- clearTimeout(timer);
130
- }
131
- const text = await res.text();
132
- let parsed = null;
133
- if (text) {
134
- try {
135
- parsed = JSON.parse(text);
136
- } catch {
137
- parsed = text;
138
- }
139
- }
140
- if (!res.ok) throw errorFromResponse(res.status, parsed);
141
- return parsed ?? {};
142
- }
143
- };
144
-
145
- export { Http, InvoError, assertSecureBaseUrl };
146
- //# sourceMappingURL=chunk-A44O4KC3.js.map
147
- //# sourceMappingURL=chunk-A44O4KC3.js.map