@invonetwork/web-sdk 0.2.1 → 0.4.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,76 @@
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
+
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
+ }
1
74
  /** Public, neutral payment-rail names. Provider/processor names are an internal
2
75
  * backend routing detail and are deliberately NOT exposed here. */
3
76
  type Rail = "platform" | "game" | "steam";
@@ -10,6 +83,16 @@ interface InvoConfig {
10
83
  timeoutMs?: number;
11
84
  /** Optional fetch override (e.g. a custom agent). Defaults to global fetch. */
12
85
  fetch?: typeof fetch;
86
+ /**
87
+ * Max automatic retries on transient failures — network error/timeout, `429`
88
+ * (honoring `retry_after`), and `5xx`. Default 2. Set 0 to disable. Mutating
89
+ * calls carry idempotency keys, so retries are safe.
90
+ */
91
+ maxRetries?: number;
92
+ /** Base backoff delay in ms (exponential + jitter). Default 250. */
93
+ retryBaseDelayMs?: number;
94
+ /** Optional observability callbacks: onRequest / onResponse / onError (best-effort). */
95
+ hooks?: InvoHooks;
13
96
  }
14
97
  interface ServerConfig extends InvoConfig {
15
98
  /** The game secret. Server-side ONLY — never ship this to a browser. */
@@ -107,6 +190,24 @@ interface PurchaseResult {
107
190
  newBalance?: string | number | null;
108
191
  raw: Record<string, unknown>;
109
192
  }
193
+ interface ConfirmPaymentResult {
194
+ status: string;
195
+ transactionId?: string;
196
+ newBalance?: string | number | null;
197
+ raw: Record<string, unknown>;
198
+ }
199
+ /** Shape of the order-details reads (currency purchase + item purchase). */
200
+ interface OrderDetailsResult {
201
+ order: Record<string, unknown>;
202
+ financialSummary: Record<string, unknown>;
203
+ statusTimeline: unknown;
204
+ raw: Record<string, unknown>;
205
+ }
206
+ interface ItemHistoryResult {
207
+ history: Record<string, unknown>[];
208
+ pagination: Record<string, unknown>;
209
+ raw: Record<string, unknown>;
210
+ }
110
211
  interface PurchaseItemInput {
111
212
  /** Idempotency key, unique per game. Required. A duplicate returns 409. */
112
213
  clientRequestId: string;
@@ -148,6 +249,56 @@ interface ItemOrderQuery {
148
249
  transactionId?: string;
149
250
  clientRequestId?: string;
150
251
  }
252
+ interface PlayerBalanceQuery {
253
+ /** Look up by email, or by playerId — provide one. */
254
+ playerEmail?: string;
255
+ playerId?: string | number;
256
+ }
257
+ interface CurrencyBalance {
258
+ currencyId: string | number;
259
+ currencyName: string;
260
+ currencySymbol: string;
261
+ availableBalance: string;
262
+ reservedBalance: string;
263
+ totalBalance: string;
264
+ lastTransaction?: string;
265
+ /** Raw balance row for any field not surfaced here. */
266
+ raw: Record<string, unknown>;
267
+ }
268
+ interface PlayerBalanceResult {
269
+ /** { player_id, player_name, player_email, date_joined, last_active }. */
270
+ player: Record<string, unknown>;
271
+ balances: CurrencyBalance[];
272
+ /** { total_currencies, total_value_usd, has_funds, last_updated }. */
273
+ summary: Record<string, unknown>;
274
+ raw: Record<string, unknown>;
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
+ }
151
302
  interface ApproveResult {
152
303
  status: string;
153
304
  next: string;
@@ -167,45 +318,4 @@ interface LinkDeviceResult {
167
318
  raw: Record<string, unknown>;
168
319
  }
169
320
 
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 };
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 };
@@ -1,3 +1,76 @@
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
+
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
+ }
1
74
  /** Public, neutral payment-rail names. Provider/processor names are an internal
2
75
  * backend routing detail and are deliberately NOT exposed here. */
3
76
  type Rail = "platform" | "game" | "steam";
@@ -10,6 +83,16 @@ interface InvoConfig {
10
83
  timeoutMs?: number;
11
84
  /** Optional fetch override (e.g. a custom agent). Defaults to global fetch. */
12
85
  fetch?: typeof fetch;
86
+ /**
87
+ * Max automatic retries on transient failures — network error/timeout, `429`
88
+ * (honoring `retry_after`), and `5xx`. Default 2. Set 0 to disable. Mutating
89
+ * calls carry idempotency keys, so retries are safe.
90
+ */
91
+ maxRetries?: number;
92
+ /** Base backoff delay in ms (exponential + jitter). Default 250. */
93
+ retryBaseDelayMs?: number;
94
+ /** Optional observability callbacks: onRequest / onResponse / onError (best-effort). */
95
+ hooks?: InvoHooks;
13
96
  }
14
97
  interface ServerConfig extends InvoConfig {
15
98
  /** The game secret. Server-side ONLY — never ship this to a browser. */
@@ -107,6 +190,24 @@ interface PurchaseResult {
107
190
  newBalance?: string | number | null;
108
191
  raw: Record<string, unknown>;
109
192
  }
193
+ interface ConfirmPaymentResult {
194
+ status: string;
195
+ transactionId?: string;
196
+ newBalance?: string | number | null;
197
+ raw: Record<string, unknown>;
198
+ }
199
+ /** Shape of the order-details reads (currency purchase + item purchase). */
200
+ interface OrderDetailsResult {
201
+ order: Record<string, unknown>;
202
+ financialSummary: Record<string, unknown>;
203
+ statusTimeline: unknown;
204
+ raw: Record<string, unknown>;
205
+ }
206
+ interface ItemHistoryResult {
207
+ history: Record<string, unknown>[];
208
+ pagination: Record<string, unknown>;
209
+ raw: Record<string, unknown>;
210
+ }
110
211
  interface PurchaseItemInput {
111
212
  /** Idempotency key, unique per game. Required. A duplicate returns 409. */
112
213
  clientRequestId: string;
@@ -148,6 +249,56 @@ interface ItemOrderQuery {
148
249
  transactionId?: string;
149
250
  clientRequestId?: string;
150
251
  }
252
+ interface PlayerBalanceQuery {
253
+ /** Look up by email, or by playerId — provide one. */
254
+ playerEmail?: string;
255
+ playerId?: string | number;
256
+ }
257
+ interface CurrencyBalance {
258
+ currencyId: string | number;
259
+ currencyName: string;
260
+ currencySymbol: string;
261
+ availableBalance: string;
262
+ reservedBalance: string;
263
+ totalBalance: string;
264
+ lastTransaction?: string;
265
+ /** Raw balance row for any field not surfaced here. */
266
+ raw: Record<string, unknown>;
267
+ }
268
+ interface PlayerBalanceResult {
269
+ /** { player_id, player_name, player_email, date_joined, last_active }. */
270
+ player: Record<string, unknown>;
271
+ balances: CurrencyBalance[];
272
+ /** { total_currencies, total_value_usd, has_funds, last_updated }. */
273
+ summary: Record<string, unknown>;
274
+ raw: Record<string, unknown>;
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
+ }
151
302
  interface ApproveResult {
152
303
  status: string;
153
304
  next: string;
@@ -167,45 +318,4 @@ interface LinkDeviceResult {
167
318
  raw: Record<string, unknown>;
168
319
  }
169
320
 
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 };
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 };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@invonetwork/web-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.4.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",
@@ -43,9 +43,13 @@
43
43
  "build": "tsup",
44
44
  "dev": "tsup --watch",
45
45
  "typecheck": "tsc --noEmit",
46
+ "lint": "eslint .",
46
47
  "test": "vitest run",
47
48
  "test:watch": "vitest",
48
49
  "clean": "rimraf dist",
50
+ "changeset": "changeset",
51
+ "version-packages": "changeset version",
52
+ "release": "npm run build && changeset publish",
49
53
  "prepublishOnly": "npm run clean && npm run build"
50
54
  },
51
55
  "keywords": [
@@ -60,9 +64,13 @@
60
64
  "access": "public"
61
65
  },
62
66
  "devDependencies": {
67
+ "@changesets/cli": "^2.31.0",
68
+ "@eslint/js": "^10.0.1",
69
+ "eslint": "^10.6.0",
63
70
  "rimraf": "^5.0.5",
64
71
  "tsup": "^8.0.1",
65
72
  "typescript": "^5.4.5",
73
+ "typescript-eslint": "^8.62.1",
66
74
  "vitest": "^1.5.0"
67
75
  }
68
76
  }
@@ -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