@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.
- package/CHANGELOG.md +27 -0
- package/LICENSE +18 -17
- package/README.md +464 -393
- package/dist/chunk-DV3WZGMH.js +231 -0
- package/dist/index.cjs +121 -34
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -3
- package/dist/server.cjs +288 -44
- package/dist/server.d.cts +112 -7
- package/dist/server.d.ts +112 -7
- package/dist/server.js +173 -14
- package/dist/{errors-DV5QsftP.d.cts → types-CBkoUymV.d.cts} +121 -42
- package/dist/{errors-DV5QsftP.d.ts → types-CBkoUymV.d.ts} +121 -42
- package/package.json +2 -2
- package/dist/chunk-A44O4KC3.js +0 -147
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
this.requestId = args.requestId;
|
|
10
|
+
Object.setPrototypeOf(this, _InvoError.prototype);
|
|
11
|
+
}
|
|
12
|
+
/** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
|
|
13
|
+
get isReceiverNotEnrolled() {
|
|
14
|
+
return this.code === "receiver_not_enrolled_use_claim_code" || /receiver_not_enrolled_use_claim_code/i.test(this.message);
|
|
15
|
+
}
|
|
16
|
+
/** True if the session/SDK token has expired and the caller should re-mint + retry. */
|
|
17
|
+
get isTokenExpired() {
|
|
18
|
+
return this.code === "SDK_TOKEN_EXPIRED";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* True if an item purchase failed because the player's balance was too low (§4.8 → 400).
|
|
22
|
+
* The backend carries `required_amount` + `current_balance` on the body for the UI.
|
|
23
|
+
* Gated to status 400 so the `429 insufficient_balance_blocked` abuse throttle (which
|
|
24
|
+
* is a rate-limit, not a top-up condition) is NOT misclassified as this.
|
|
25
|
+
*/
|
|
26
|
+
get isInsufficientBalance() {
|
|
27
|
+
if (this.status !== 400) return false;
|
|
28
|
+
const b = this.bodyObject();
|
|
29
|
+
return this.code === "INSUFFICIENT_BALANCE" || "required_amount" in b || /insufficient[_ ]balance/i.test(this.message);
|
|
30
|
+
}
|
|
31
|
+
/** True if an idempotency-keyed request was a duplicate (item purchase → 409). */
|
|
32
|
+
get isDuplicateRequest() {
|
|
33
|
+
return this.code === "DUPLICATE_REQUEST" || this.status === 409 && /duplicate/i.test(this.message);
|
|
34
|
+
}
|
|
35
|
+
/** Seconds to wait before retrying, when the backend throttled the call (429 `retry_after`). */
|
|
36
|
+
get retryAfter() {
|
|
37
|
+
const v = this.bodyObject()["retry_after"];
|
|
38
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
39
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : void 0;
|
|
40
|
+
}
|
|
41
|
+
/** `body` as a plain object, or `{}` if it's a string/number/null (non-JSON responses).
|
|
42
|
+
* The `in` operator throws on primitives, so callers must go through this. */
|
|
43
|
+
bodyObject() {
|
|
44
|
+
return this.body && typeof this.body === "object" ? this.body : {};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function errorFromResponse(status, body, requestId) {
|
|
48
|
+
let message = `INVO request failed (HTTP ${status})`;
|
|
49
|
+
let code;
|
|
50
|
+
if (body && typeof body === "object") {
|
|
51
|
+
const b = body;
|
|
52
|
+
if (typeof b["code"] === "string") code = b["code"];
|
|
53
|
+
if (typeof b["error"] === "string") message = b["error"];
|
|
54
|
+
else if (typeof b["message"] === "string") message = b["message"];
|
|
55
|
+
} else if (typeof body === "string" && body.trim()) {
|
|
56
|
+
message = body;
|
|
57
|
+
}
|
|
58
|
+
return new InvoError({ message, code, status, body, requestId });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/shared/http.ts
|
|
62
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
63
|
+
var MAX_RETRY_AFTER_MS = 2e4;
|
|
64
|
+
function assertSecureBaseUrl(baseUrl) {
|
|
65
|
+
let u;
|
|
66
|
+
try {
|
|
67
|
+
u = new URL(baseUrl);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`Invalid baseUrl: ${baseUrl}`);
|
|
70
|
+
}
|
|
71
|
+
const isLocal = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
72
|
+
if (u.protocol === "https:" || u.protocol === "http:" && isLocal) return;
|
|
73
|
+
throw new Error(
|
|
74
|
+
`baseUrl must use https:// (got "${u.protocol}//"). Plaintext would expose the token/secret on the wire.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
var _Http = class _Http {
|
|
78
|
+
constructor(opts) {
|
|
79
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
80
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
81
|
+
const f = opts.fetchImpl ?? globalThis.fetch;
|
|
82
|
+
if (typeof f !== "function") {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"No fetch implementation available. Use Node >=18, or pass `fetch` in the config."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
this.fetchImpl = f;
|
|
88
|
+
this.userAgent = opts.userAgent;
|
|
89
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
|
|
90
|
+
this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 250;
|
|
91
|
+
this.hooks = opts.hooks;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* @param opts.idempotent - mark this POST safe to auto-retry (it carries an
|
|
95
|
+
* idempotency key, or has no side effect). Default false: non-idempotent POSTs
|
|
96
|
+
* (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
|
|
97
|
+
*/
|
|
98
|
+
async post(path, body, auth, opts) {
|
|
99
|
+
return this.request("POST", path, body, auth, opts?.idempotent ?? false);
|
|
100
|
+
}
|
|
101
|
+
// GET is always idempotent → safe to retry.
|
|
102
|
+
async get(path, auth) {
|
|
103
|
+
return this.request("GET", path, void 0, auth, true);
|
|
104
|
+
}
|
|
105
|
+
authHeaders(auth) {
|
|
106
|
+
switch (auth.kind) {
|
|
107
|
+
case "game-secret":
|
|
108
|
+
return { "X-Game-Secret-Key": auth.secret };
|
|
109
|
+
case "bearer":
|
|
110
|
+
return { Authorization: `Bearer ${auth.token}` };
|
|
111
|
+
case "none":
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async request(method, path, body, auth, idempotent) {
|
|
116
|
+
const url = `${this.baseUrl}${path}`;
|
|
117
|
+
const headers = {
|
|
118
|
+
Accept: "application/json",
|
|
119
|
+
...this.authHeaders(auth)
|
|
120
|
+
};
|
|
121
|
+
if (this.userAgent) headers["User-Agent"] = this.userAgent;
|
|
122
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
123
|
+
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
124
|
+
for (let attempt = 0; ; attempt++) {
|
|
125
|
+
const start = Date.now();
|
|
126
|
+
this.fire("onRequest", { method, url, attempt });
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
129
|
+
let res;
|
|
130
|
+
let networkError;
|
|
131
|
+
try {
|
|
132
|
+
res = await this.fetchImpl(url, { method, headers, body: payload, signal: controller.signal });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
networkError = new InvoError({
|
|
135
|
+
message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
|
|
136
|
+
status: 0,
|
|
137
|
+
body: null
|
|
138
|
+
});
|
|
139
|
+
} finally {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
}
|
|
142
|
+
if (networkError) {
|
|
143
|
+
const willRetry = idempotent && attempt < this.maxRetries;
|
|
144
|
+
this.fire("onError", { method, url, attempt, error: networkError, willRetry });
|
|
145
|
+
if (willRetry) {
|
|
146
|
+
await sleep(this.backoff(attempt));
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
throw networkError;
|
|
150
|
+
}
|
|
151
|
+
const requestId = pickRequestId(res.headers);
|
|
152
|
+
const text = await res.text();
|
|
153
|
+
let parsed = null;
|
|
154
|
+
if (text) {
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(text);
|
|
157
|
+
} catch {
|
|
158
|
+
parsed = text;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.fire("onResponse", {
|
|
162
|
+
method,
|
|
163
|
+
url,
|
|
164
|
+
attempt,
|
|
165
|
+
status: res.status,
|
|
166
|
+
durationMs: Date.now() - start,
|
|
167
|
+
requestId
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
const err = errorFromResponse(res.status, parsed, requestId);
|
|
171
|
+
let wait;
|
|
172
|
+
if (idempotent && attempt < this.maxRetries && _Http.RETRIABLE_STATUS.has(res.status)) {
|
|
173
|
+
if (res.status === 429) {
|
|
174
|
+
const ra = retryAfterMs(parsed, res.headers);
|
|
175
|
+
wait = ra === void 0 ? this.backoff(attempt) : ra <= MAX_RETRY_AFTER_MS ? ra : void 0;
|
|
176
|
+
} else {
|
|
177
|
+
wait = this.backoff(attempt);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
|
|
181
|
+
if (wait !== void 0) {
|
|
182
|
+
await sleep(wait);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
return parsed ?? {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Invoke an observability hook, swallowing any error it throws (best-effort). */
|
|
191
|
+
fire(name, info) {
|
|
192
|
+
const hook = this.hooks?.[name];
|
|
193
|
+
if (!hook) return;
|
|
194
|
+
try {
|
|
195
|
+
hook(info);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Exponential backoff with full jitter: base * 2^attempt, randomized. */
|
|
200
|
+
backoff(attempt) {
|
|
201
|
+
const ceil = this.retryBaseDelayMs * 2 ** attempt;
|
|
202
|
+
return Math.floor(Math.random() * ceil) + this.retryBaseDelayMs;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
|
|
206
|
+
_Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
207
|
+
var Http = _Http;
|
|
208
|
+
function sleep(ms) {
|
|
209
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
210
|
+
}
|
|
211
|
+
function pickRequestId(headers) {
|
|
212
|
+
if (!headers || typeof headers.get !== "function") return void 0;
|
|
213
|
+
return headers.get("x-invo-request-id") ?? headers.get("x-request-id") ?? void 0;
|
|
214
|
+
}
|
|
215
|
+
function retryAfterMs(parsed, headers) {
|
|
216
|
+
if (parsed && typeof parsed === "object") {
|
|
217
|
+
const v = parsed["retry_after"];
|
|
218
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
219
|
+
if (typeof n === "number" && Number.isFinite(n) && n >= 0) return n * 1e3;
|
|
220
|
+
}
|
|
221
|
+
const header = headers && typeof headers.get === "function" ? headers.get("retry-after") : null;
|
|
222
|
+
if (header) {
|
|
223
|
+
const n = Number(header);
|
|
224
|
+
if (Number.isFinite(n) && n >= 0) return n * 1e3;
|
|
225
|
+
}
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export { Http, InvoError, assertSecureBaseUrl };
|
|
230
|
+
//# sourceMappingURL=chunk-DV3WZGMH.js.map
|
|
231
|
+
//# sourceMappingURL=chunk-DV3WZGMH.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -8,6 +8,7 @@ var InvoError = class _InvoError extends Error {
|
|
|
8
8
|
this.code = args.code;
|
|
9
9
|
this.status = args.status;
|
|
10
10
|
this.body = args.body ?? null;
|
|
11
|
+
this.requestId = args.requestId;
|
|
11
12
|
Object.setPrototypeOf(this, _InvoError.prototype);
|
|
12
13
|
}
|
|
13
14
|
/** True if this is the "recipient isn't passkey-enrolled, fall back to claim code" signal. */
|
|
@@ -45,7 +46,7 @@ var InvoError = class _InvoError extends Error {
|
|
|
45
46
|
return this.body && typeof this.body === "object" ? this.body : {};
|
|
46
47
|
}
|
|
47
48
|
};
|
|
48
|
-
function errorFromResponse(status, body) {
|
|
49
|
+
function errorFromResponse(status, body, requestId) {
|
|
49
50
|
let message = `INVO request failed (HTTP ${status})`;
|
|
50
51
|
let code;
|
|
51
52
|
if (body && typeof body === "object") {
|
|
@@ -56,11 +57,12 @@ function errorFromResponse(status, body) {
|
|
|
56
57
|
} else if (typeof body === "string" && body.trim()) {
|
|
57
58
|
message = body;
|
|
58
59
|
}
|
|
59
|
-
return new InvoError({ message, code, status, body });
|
|
60
|
+
return new InvoError({ message, code, status, body, requestId });
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// src/shared/http.ts
|
|
63
64
|
var DEFAULT_TIMEOUT = 3e4;
|
|
65
|
+
var MAX_RETRY_AFTER_MS = 2e4;
|
|
64
66
|
function assertSecureBaseUrl(baseUrl) {
|
|
65
67
|
let u;
|
|
66
68
|
try {
|
|
@@ -74,7 +76,7 @@ function assertSecureBaseUrl(baseUrl) {
|
|
|
74
76
|
`baseUrl must use https:// (got "${u.protocol}//"). Plaintext would expose the token/secret on the wire.`
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
|
-
var
|
|
79
|
+
var _Http = class _Http {
|
|
78
80
|
constructor(opts) {
|
|
79
81
|
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
80
82
|
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
@@ -86,12 +88,21 @@ var Http = class {
|
|
|
86
88
|
}
|
|
87
89
|
this.fetchImpl = f;
|
|
88
90
|
this.userAgent = opts.userAgent;
|
|
91
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
|
|
92
|
+
this.retryBaseDelayMs = opts.retryBaseDelayMs ?? 250;
|
|
93
|
+
this.hooks = opts.hooks;
|
|
89
94
|
}
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
/**
|
|
96
|
+
* @param opts.idempotent - mark this POST safe to auto-retry (it carries an
|
|
97
|
+
* idempotency key, or has no side effect). Default false: non-idempotent POSTs
|
|
98
|
+
* (e.g. single-use WebAuthn assertions) are NEVER auto-retried.
|
|
99
|
+
*/
|
|
100
|
+
async post(path, body, auth, opts) {
|
|
101
|
+
return this.request("POST", path, body, auth, opts?.idempotent ?? false);
|
|
92
102
|
}
|
|
103
|
+
// GET is always idempotent → safe to retry.
|
|
93
104
|
async get(path, auth) {
|
|
94
|
-
return this.request("GET", path, void 0, auth);
|
|
105
|
+
return this.request("GET", path, void 0, auth, true);
|
|
95
106
|
}
|
|
96
107
|
authHeaders(auth) {
|
|
97
108
|
switch (auth.kind) {
|
|
@@ -103,7 +114,7 @@ var Http = class {
|
|
|
103
114
|
return {};
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
|
-
async request(method, path, body, auth) {
|
|
117
|
+
async request(method, path, body, auth, idempotent) {
|
|
107
118
|
const url = `${this.baseUrl}${path}`;
|
|
108
119
|
const headers = {
|
|
109
120
|
Accept: "application/json",
|
|
@@ -111,38 +122,111 @@ var Http = class {
|
|
|
111
122
|
};
|
|
112
123
|
if (this.userAgent) headers["User-Agent"] = this.userAgent;
|
|
113
124
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
const payload = body !== void 0 ? JSON.stringify(body) : void 0;
|
|
126
|
+
for (let attempt = 0; ; attempt++) {
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
this.fire("onRequest", { method, url, attempt });
|
|
129
|
+
const controller = new AbortController();
|
|
130
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
131
|
+
let res;
|
|
132
|
+
let networkError;
|
|
133
|
+
try {
|
|
134
|
+
res = await this.fetchImpl(url, { method, headers, body: payload, signal: controller.signal });
|
|
135
|
+
} catch (err) {
|
|
136
|
+
networkError = new InvoError({
|
|
137
|
+
message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
|
|
138
|
+
status: 0,
|
|
139
|
+
body: null
|
|
140
|
+
});
|
|
141
|
+
} finally {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
}
|
|
144
|
+
if (networkError) {
|
|
145
|
+
const willRetry = idempotent && attempt < this.maxRetries;
|
|
146
|
+
this.fire("onError", { method, url, attempt, error: networkError, willRetry });
|
|
147
|
+
if (willRetry) {
|
|
148
|
+
await sleep(this.backoff(attempt));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw networkError;
|
|
152
|
+
}
|
|
153
|
+
const requestId = pickRequestId(res.headers);
|
|
154
|
+
const text = await res.text();
|
|
155
|
+
let parsed = null;
|
|
156
|
+
if (text) {
|
|
157
|
+
try {
|
|
158
|
+
parsed = JSON.parse(text);
|
|
159
|
+
} catch {
|
|
160
|
+
parsed = text;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.fire("onResponse", {
|
|
119
164
|
method,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
throw new InvoError({
|
|
126
|
-
message: err instanceof Error && err.name === "AbortError" ? `Request to ${path} timed out after ${this.timeoutMs}ms` : `Network error calling ${path}: ${err?.message ?? err}`,
|
|
127
|
-
status: 0,
|
|
128
|
-
body: null
|
|
165
|
+
url,
|
|
166
|
+
attempt,
|
|
167
|
+
status: res.status,
|
|
168
|
+
durationMs: Date.now() - start,
|
|
169
|
+
requestId
|
|
129
170
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const err = errorFromResponse(res.status, parsed, requestId);
|
|
173
|
+
let wait;
|
|
174
|
+
if (idempotent && attempt < this.maxRetries && _Http.RETRIABLE_STATUS.has(res.status)) {
|
|
175
|
+
if (res.status === 429) {
|
|
176
|
+
const ra = retryAfterMs(parsed, res.headers);
|
|
177
|
+
wait = ra === void 0 ? this.backoff(attempt) : ra <= MAX_RETRY_AFTER_MS ? ra : void 0;
|
|
178
|
+
} else {
|
|
179
|
+
wait = this.backoff(attempt);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
this.fire("onError", { method, url, attempt, error: err, willRetry: wait !== void 0 });
|
|
183
|
+
if (wait !== void 0) {
|
|
184
|
+
await sleep(wait);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
throw err;
|
|
140
188
|
}
|
|
189
|
+
return parsed ?? {};
|
|
141
190
|
}
|
|
142
|
-
|
|
143
|
-
|
|
191
|
+
}
|
|
192
|
+
/** Invoke an observability hook, swallowing any error it throws (best-effort). */
|
|
193
|
+
fire(name, info) {
|
|
194
|
+
const hook = this.hooks?.[name];
|
|
195
|
+
if (!hook) return;
|
|
196
|
+
try {
|
|
197
|
+
hook(info);
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Exponential backoff with full jitter: base * 2^attempt, randomized. */
|
|
202
|
+
backoff(attempt) {
|
|
203
|
+
const ceil = this.retryBaseDelayMs * 2 ** attempt;
|
|
204
|
+
return Math.floor(Math.random() * ceil) + this.retryBaseDelayMs;
|
|
144
205
|
}
|
|
145
206
|
};
|
|
207
|
+
/** HTTP statuses worth retrying — rate limit + transient gateway/server errors. */
|
|
208
|
+
_Http.RETRIABLE_STATUS = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
209
|
+
var Http = _Http;
|
|
210
|
+
function sleep(ms) {
|
|
211
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
212
|
+
}
|
|
213
|
+
function pickRequestId(headers) {
|
|
214
|
+
if (!headers || typeof headers.get !== "function") return void 0;
|
|
215
|
+
return headers.get("x-invo-request-id") ?? headers.get("x-request-id") ?? void 0;
|
|
216
|
+
}
|
|
217
|
+
function retryAfterMs(parsed, headers) {
|
|
218
|
+
if (parsed && typeof parsed === "object") {
|
|
219
|
+
const v = parsed["retry_after"];
|
|
220
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
221
|
+
if (typeof n === "number" && Number.isFinite(n) && n >= 0) return n * 1e3;
|
|
222
|
+
}
|
|
223
|
+
const header = headers && typeof headers.get === "function" ? headers.get("retry-after") : null;
|
|
224
|
+
if (header) {
|
|
225
|
+
const n = Number(header);
|
|
226
|
+
if (Number.isFinite(n) && n >= 0) return n * 1e3;
|
|
227
|
+
}
|
|
228
|
+
return void 0;
|
|
229
|
+
}
|
|
146
230
|
|
|
147
231
|
// src/shared/webauthn.ts
|
|
148
232
|
function b64urlToBuffer(value) {
|
|
@@ -234,7 +318,10 @@ var InvoClient = class {
|
|
|
234
318
|
this.http = new Http({
|
|
235
319
|
baseUrl: config.baseUrl,
|
|
236
320
|
timeoutMs: config.timeoutMs,
|
|
237
|
-
fetchImpl: config.fetch
|
|
321
|
+
fetchImpl: config.fetch,
|
|
322
|
+
maxRetries: config.maxRetries,
|
|
323
|
+
retryBaseDelayMs: config.retryBaseDelayMs,
|
|
324
|
+
hooks: config.hooks
|
|
238
325
|
// Browser: do NOT set User-Agent (forbidden header); the browser's own UA is fine.
|
|
239
326
|
});
|
|
240
327
|
this.auth = { kind: "bearer", token: config.token };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './
|
|
1
|
+
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './types-CBkoUymV.cjs';
|
|
2
|
+
export { I as InvoError, b as InvoErrorInfo, c as InvoHooks, d as InvoRequestInfo, e as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBkoUymV.cjs';
|
|
3
3
|
|
|
4
4
|
declare class InvoClient {
|
|
5
5
|
private readonly http;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './
|
|
2
|
-
export { I as InvoError, R as Rail, V as VerificationMethod } from './
|
|
1
|
+
import { C as ClientConfig, A as ApproveResult, a as ConfirmReceiptResult, L as LinkDeviceResult } from './types-CBkoUymV.js';
|
|
2
|
+
export { I as InvoError, b as InvoErrorInfo, c as InvoHooks, d as InvoRequestInfo, e as InvoResponseInfo, R as Rail, V as VerificationMethod } from './types-CBkoUymV.js';
|
|
3
3
|
|
|
4
4
|
declare class InvoClient {
|
|
5
5
|
private readonly http;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { assertSecureBaseUrl, Http, InvoError } from './chunk-
|
|
2
|
-
export { InvoError } from './chunk-
|
|
1
|
+
import { assertSecureBaseUrl, Http, InvoError } from './chunk-DV3WZGMH.js';
|
|
2
|
+
export { InvoError } from './chunk-DV3WZGMH.js';
|
|
3
3
|
|
|
4
4
|
// src/shared/webauthn.ts
|
|
5
5
|
function b64urlToBuffer(value) {
|
|
@@ -91,7 +91,10 @@ var InvoClient = class {
|
|
|
91
91
|
this.http = new Http({
|
|
92
92
|
baseUrl: config.baseUrl,
|
|
93
93
|
timeoutMs: config.timeoutMs,
|
|
94
|
-
fetchImpl: config.fetch
|
|
94
|
+
fetchImpl: config.fetch,
|
|
95
|
+
maxRetries: config.maxRetries,
|
|
96
|
+
retryBaseDelayMs: config.retryBaseDelayMs,
|
|
97
|
+
hooks: config.hooks
|
|
95
98
|
// Browser: do NOT set User-Agent (forbidden header); the browser's own UA is fine.
|
|
96
99
|
});
|
|
97
100
|
this.auth = { kind: "bearer", token: config.token };
|