@kleora-io/core 0.0.0 → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,720 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { readFileSync, mkdirSync, chmodSync, writeFileSync, renameSync, rmSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { join } from 'path';
5
+
6
+ // src/errors.ts
7
+ var KleoraToolError = class extends Error {
8
+ code;
9
+ constructor(code, message, options) {
10
+ super(message, options);
11
+ this.name = "KleoraToolError";
12
+ this.code = code;
13
+ }
14
+ };
15
+ var ApiError = class extends KleoraToolError {
16
+ type;
17
+ title;
18
+ status;
19
+ detail;
20
+ problem;
21
+ constructor(type, status, title, detail, problem) {
22
+ super("api_error", detail ?? title ?? type);
23
+ this.name = "ApiError";
24
+ this.type = type;
25
+ this.status = status;
26
+ this.title = title;
27
+ this.detail = detail;
28
+ this.problem = problem;
29
+ }
30
+ /** The RFC 9457 document, verbatim plus its four standard fields. */
31
+ toProblemDocument() {
32
+ return {
33
+ type: this.type,
34
+ title: this.title,
35
+ status: this.status,
36
+ detail: this.detail,
37
+ ...this.problem.document
38
+ };
39
+ }
40
+ };
41
+ var ManagementApiUnreachable = class extends KleoraToolError {
42
+ constructor(message, options) {
43
+ super("management_api_unreachable", message, options);
44
+ this.name = "ManagementApiUnreachable";
45
+ }
46
+ };
47
+ var AuthorizationDenied = class extends KleoraToolError {
48
+ constructor() {
49
+ super("access_denied", "The device authorization was denied.");
50
+ this.name = "AuthorizationDenied";
51
+ }
52
+ };
53
+ function defaultConfigDir(name) {
54
+ return join(homedir(), `.${name}`);
55
+ }
56
+ async function createCredentialStore({
57
+ service,
58
+ configDir
59
+ }) {
60
+ const fileStore = new FileCredentialStore(configDir);
61
+ const keyring = await loadKeyring();
62
+ if (keyring === null) return fileStore;
63
+ return new KeychainCredentialStore(service, keyring, fileStore);
64
+ }
65
+ async function loadKeyring() {
66
+ try {
67
+ return await import('@napi-rs/keyring');
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ var KeychainCredentialStore = class {
73
+ backend = "keychain";
74
+ #service;
75
+ #keyring;
76
+ #fallback;
77
+ /** Set once any call fails, so a dead keychain is not retried per call. */
78
+ #broken = false;
79
+ constructor(service, keyring, fallback) {
80
+ this.#service = service;
81
+ this.#keyring = keyring;
82
+ this.#fallback = fallback;
83
+ }
84
+ async get(profile) {
85
+ if (this.#broken) return this.#fallback.get(profile);
86
+ try {
87
+ const entry = new this.#keyring.AsyncEntry(this.#service, profile);
88
+ const value = await entry.getPassword();
89
+ return value ?? null;
90
+ } catch {
91
+ this.#broken = true;
92
+ return this.#fallback.get(profile);
93
+ }
94
+ }
95
+ async set(profile, refreshToken) {
96
+ if (this.#broken) return this.#fallback.set(profile, refreshToken);
97
+ try {
98
+ const entry = new this.#keyring.AsyncEntry(this.#service, profile);
99
+ await entry.setPassword(refreshToken);
100
+ } catch {
101
+ this.#broken = true;
102
+ await this.#fallback.set(profile, refreshToken);
103
+ }
104
+ }
105
+ async delete(profile) {
106
+ if (this.#broken) return this.#fallback.delete(profile);
107
+ try {
108
+ const entry = new this.#keyring.AsyncEntry(this.#service, profile);
109
+ await entry.deleteCredential();
110
+ } catch {
111
+ this.#broken = true;
112
+ await this.#fallback.delete(profile);
113
+ }
114
+ }
115
+ };
116
+ var FileCredentialStore = class {
117
+ backend = "file";
118
+ #dir;
119
+ constructor(dir) {
120
+ this.#dir = dir;
121
+ }
122
+ // Every one of these is synchronous under the hood (`node:fs`'s sync API,
123
+ // deliberately — this runs at most once per tool call, never in a hot
124
+ // loop) but returns a `Promise` regardless, so `KeychainCredentialStore`
125
+ // can fall back to it without its callers caring which backend answered.
126
+ get(profile) {
127
+ try {
128
+ const raw = readFileSync(this.#path(profile), "utf8");
129
+ const parsed = JSON.parse(raw);
130
+ if (typeof parsed === "object" && parsed !== null && "refreshToken" in parsed) {
131
+ const refreshToken = parsed.refreshToken;
132
+ if (typeof refreshToken === "string") return Promise.resolve(refreshToken);
133
+ }
134
+ return Promise.resolve(null);
135
+ } catch (error) {
136
+ if (isNotFound(error)) return Promise.resolve(null);
137
+ return Promise.reject(asError(error));
138
+ }
139
+ }
140
+ set(profile, refreshToken) {
141
+ try {
142
+ mkdirSync(this.#dir, { recursive: true, mode: 448 });
143
+ chmodSync(this.#dir, 448);
144
+ const path = this.#path(profile);
145
+ const tmp = `${path}.${randomUUID()}.tmp`;
146
+ writeFileSync(tmp, JSON.stringify({ refreshToken }), { mode: 384 });
147
+ chmodSync(tmp, 384);
148
+ renameSync(tmp, path);
149
+ return Promise.resolve();
150
+ } catch (error) {
151
+ return Promise.reject(asError(error));
152
+ }
153
+ }
154
+ delete(profile) {
155
+ try {
156
+ rmSync(this.#path(profile));
157
+ return Promise.resolve();
158
+ } catch (error) {
159
+ if (isNotFound(error)) return Promise.resolve();
160
+ return Promise.reject(asError(error));
161
+ }
162
+ }
163
+ #path(profile) {
164
+ return join(this.#dir, `credentials-${encodeURIComponent(profile)}.json`);
165
+ }
166
+ };
167
+ function isNotFound(error) {
168
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
169
+ }
170
+ function asError(error) {
171
+ return error instanceof Error ? error : new Error(String(error));
172
+ }
173
+
174
+ // src/credentials/device-grant.ts
175
+ var OAuthError = class extends Error {
176
+ code;
177
+ constructor(code, description) {
178
+ super(description ?? code);
179
+ this.name = "OAuthError";
180
+ this.code = code;
181
+ }
182
+ };
183
+ async function postForm(url, body, fetchImpl, userAgent) {
184
+ let response;
185
+ try {
186
+ response = await fetchImpl(url, {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/x-www-form-urlencoded",
190
+ Accept: "application/json",
191
+ "User-Agent": userAgent
192
+ },
193
+ body: new URLSearchParams(body)
194
+ });
195
+ } catch (error) {
196
+ throw new ManagementApiUnreachable(`could not reach ${url}`, { cause: error });
197
+ }
198
+ let document = {};
199
+ try {
200
+ document = await response.json();
201
+ } catch {
202
+ }
203
+ if (!response.ok) {
204
+ const rawCode = typeof document["error"] === "string" ? document["error"] : "other";
205
+ const code = isKnownCode(rawCode) ? rawCode : "other";
206
+ const description = typeof document["error_description"] === "string" ? document["error_description"] : void 0;
207
+ throw new OAuthError(code, description);
208
+ }
209
+ return document;
210
+ }
211
+ function isKnownCode(value) {
212
+ return [
213
+ "authorization_pending",
214
+ "slow_down",
215
+ "access_denied",
216
+ "expired_token",
217
+ "invalid_grant",
218
+ "unauthorized_client"
219
+ ].includes(value);
220
+ }
221
+ async function startDeviceAuthorization({
222
+ issuer,
223
+ clientId,
224
+ userAgent,
225
+ fetch: fetchImpl
226
+ }) {
227
+ const document = await postForm(
228
+ `${issuer}/oauth/device_authorization`,
229
+ { client_id: clientId },
230
+ fetchImpl,
231
+ userAgent
232
+ );
233
+ return {
234
+ deviceCode: requireString(document, "device_code"),
235
+ userCode: requireString(document, "user_code"),
236
+ verificationUri: requireString(document, "verification_uri"),
237
+ verificationUriComplete: typeof document["verification_uri_complete"] === "string" ? document["verification_uri_complete"] : requireString(document, "verification_uri"),
238
+ expiresIn: requireNumber(document, "expires_in"),
239
+ interval: typeof document["interval"] === "number" ? document["interval"] : 5
240
+ };
241
+ }
242
+ async function pollDeviceToken({
243
+ issuer,
244
+ clientId,
245
+ deviceCode,
246
+ userAgent,
247
+ fetch: fetchImpl
248
+ }) {
249
+ const document = await postForm(
250
+ `${issuer}/oauth/token`,
251
+ {
252
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
253
+ device_code: deviceCode,
254
+ client_id: clientId
255
+ },
256
+ fetchImpl,
257
+ userAgent
258
+ );
259
+ return tokensOf(document);
260
+ }
261
+ async function refreshAccessToken({
262
+ issuer,
263
+ clientId,
264
+ refreshToken,
265
+ userAgent,
266
+ fetch: fetchImpl
267
+ }) {
268
+ const document = await postForm(
269
+ `${issuer}/oauth/token`,
270
+ { grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId },
271
+ fetchImpl,
272
+ userAgent
273
+ );
274
+ return tokensOf(document);
275
+ }
276
+ async function revokeRefreshToken({
277
+ issuer,
278
+ clientId,
279
+ refreshToken,
280
+ userAgent,
281
+ fetch: fetchImpl
282
+ }) {
283
+ await postForm(
284
+ `${issuer}/oauth/revoke`,
285
+ { token: refreshToken, token_type_hint: "refresh_token", client_id: clientId },
286
+ fetchImpl,
287
+ userAgent
288
+ );
289
+ }
290
+ function tokensOf(document) {
291
+ return {
292
+ accessToken: requireString(document, "access_token"),
293
+ refreshToken: typeof document["refresh_token"] === "string" ? document["refresh_token"] : void 0,
294
+ expiresIn: requireNumber(document, "expires_in")
295
+ };
296
+ }
297
+ function requireString(document, key) {
298
+ const value = document[key];
299
+ if (typeof value !== "string") throw new ManagementApiUnreachable(`the issuer's response has no '${key}'`);
300
+ return value;
301
+ }
302
+ function requireNumber(document, key) {
303
+ const value = document[key];
304
+ if (typeof value !== "number") throw new ManagementApiUnreachable(`the issuer's response has no '${key}'`);
305
+ return value;
306
+ }
307
+
308
+ // src/credentials/issuer.ts
309
+ var PLATFORM_PATH = "/api/v1/platform";
310
+ async function discoverIssuer({
311
+ baseUrl,
312
+ userAgent,
313
+ fetch: fetchImpl = globalThis.fetch
314
+ }) {
315
+ const url = `${baseUrl}${PLATFORM_PATH}`;
316
+ const hint = "Check the API base URL, or pass the issuer explicitly.";
317
+ let response;
318
+ try {
319
+ response = await fetchImpl(url, {
320
+ headers: { Accept: "application/json", "User-Agent": userAgent }
321
+ });
322
+ } catch (error) {
323
+ throw new ManagementApiUnreachable(`could not reach ${url} to find the issuer. ${hint}`, {
324
+ cause: error
325
+ });
326
+ }
327
+ if (!response.ok) {
328
+ throw new ManagementApiUnreachable(
329
+ `${url} answered ${String(response.status)}, so the issuer is unknown. ${hint}`
330
+ );
331
+ }
332
+ let document;
333
+ try {
334
+ document = await response.json();
335
+ } catch {
336
+ document = null;
337
+ }
338
+ const issuer = typeof document === "object" && document !== null ? document["issuer"] : void 0;
339
+ if (typeof issuer !== "string" || !isHttpUrl(issuer)) {
340
+ throw new ManagementApiUnreachable(`${url} did not name an issuer. ${hint}`);
341
+ }
342
+ return issuer.replace(/\/+$/, "");
343
+ }
344
+ function issuerResolver(explicit, options) {
345
+ if (explicit !== void 0) {
346
+ const value = explicit;
347
+ return () => Promise.resolve(value);
348
+ }
349
+ let pending = null;
350
+ return () => {
351
+ pending ??= discoverIssuer(options).catch((error) => {
352
+ pending = null;
353
+ throw error;
354
+ });
355
+ return pending;
356
+ };
357
+ }
358
+ function isHttpUrl(value) {
359
+ try {
360
+ const url = new URL(value);
361
+ return url.protocol === "https:" || url.protocol === "http:";
362
+ } catch {
363
+ return false;
364
+ }
365
+ }
366
+
367
+ // src/credentials/session.ts
368
+ var SessionNotReady = class extends Error {
369
+ state;
370
+ constructor(state) {
371
+ super(`session not ready: ${state.status}`);
372
+ this.name = "SessionNotReady";
373
+ this.state = state;
374
+ }
375
+ };
376
+ var Session = class {
377
+ #issuer;
378
+ #clientId;
379
+ #profile;
380
+ #store;
381
+ #userAgent;
382
+ #fetch;
383
+ #skewMs;
384
+ #accessToken = null;
385
+ #accessTokenExpiresAt = 0;
386
+ #pending = null;
387
+ constructor(options) {
388
+ const issuer = options.issuer;
389
+ this.#issuer = typeof issuer === "string" ? () => Promise.resolve(issuer) : issuer;
390
+ this.#clientId = options.clientId;
391
+ this.#profile = options.profile;
392
+ this.#store = options.store;
393
+ this.#userAgent = options.userAgent;
394
+ this.#fetch = options.fetch ?? globalThis.fetch;
395
+ this.#skewMs = (options.clockSkewSeconds ?? 30) * 1e3;
396
+ }
397
+ /**
398
+ * The access token to call the management API with, or the instructions to
399
+ * show the person instead of a result: "not an error — this is the
400
+ * sign-in."
401
+ */
402
+ async ensureAccessToken() {
403
+ if (this.#accessToken !== null && Date.now() < this.#accessTokenExpiresAt - this.#skewMs) {
404
+ return { status: "ready", accessToken: this.#accessToken };
405
+ }
406
+ const fromRefresh = await this.#tryStoredCredential();
407
+ if (fromRefresh !== null) return fromRefresh;
408
+ return this.#advanceDeviceFlow();
409
+ }
410
+ /**
411
+ * An access token from the **stored** credential only — never starting a
412
+ * device flow.
413
+ *
414
+ * This is the CLI's entry point rather than `ensureAccessToken` above, and
415
+ * the difference is a deliberate behavioural split rather than a
416
+ * convenience. With no credential for the profile, a CLI command says so
417
+ * and names `kleora login`, exiting `3`. **It does not start a device flow
418
+ * unbidden**: an interactive sign-in in the middle of a piped command is a
419
+ * hang.
420
+ *
421
+ * An MCP host, by contrast, has a place to show a verification URL and a
422
+ * turn to show it on, which is why `ensureAccessToken` may begin one there.
423
+ * `null` means "nothing stored, or what was stored is dead and has been
424
+ * deleted" — both of which the caller answers with the same sentence.
425
+ */
426
+ async accessTokenFromStoredCredential() {
427
+ if (this.#accessToken !== null && Date.now() < this.#accessTokenExpiresAt - this.#skewMs) {
428
+ return this.#accessToken;
429
+ }
430
+ const state = await this.#tryStoredCredential();
431
+ return state !== null && state.status === "ready" ? state.accessToken : null;
432
+ }
433
+ /** Forces the next call to re-authorise, even if a token is cached in memory. */
434
+ invalidate() {
435
+ this.#accessToken = null;
436
+ this.#accessTokenExpiresAt = 0;
437
+ }
438
+ /** `null` means "no usable stored credential — try the device flow instead". */
439
+ async #tryStoredCredential() {
440
+ const stored = await this.#store.get(this.#profile);
441
+ if (stored === null) return null;
442
+ let tokens;
443
+ try {
444
+ tokens = await refreshAccessToken({
445
+ issuer: await this.#issuer(),
446
+ clientId: this.#clientId,
447
+ refreshToken: stored,
448
+ userAgent: this.#userAgent,
449
+ fetch: this.#fetch
450
+ });
451
+ } catch (error) {
452
+ if (error instanceof OAuthError) {
453
+ await this.#store.delete(this.#profile);
454
+ return null;
455
+ }
456
+ throw error;
457
+ }
458
+ await this.#storeTokens(tokens);
459
+ return { status: "ready", accessToken: tokens.accessToken };
460
+ }
461
+ /** Starts a device flow if none is pending, or polls the one already running. */
462
+ async #advanceDeviceFlow() {
463
+ if (this.#pending === null || Date.now() >= this.#pending.expiresAt) {
464
+ const auth = await startDeviceAuthorization({
465
+ issuer: await this.#issuer(),
466
+ clientId: this.#clientId,
467
+ userAgent: this.#userAgent,
468
+ fetch: this.#fetch
469
+ });
470
+ this.#pending = {
471
+ deviceCode: auth.deviceCode,
472
+ interval: auth.interval,
473
+ expiresAt: Date.now() + auth.expiresIn * 1e3,
474
+ verificationUri: auth.verificationUri,
475
+ verificationUriComplete: auth.verificationUriComplete,
476
+ userCode: auth.userCode,
477
+ lastPollAt: 0
478
+ };
479
+ return this.#pendingState(
480
+ "No Kleora credential is authorised yet. Open the verification URL, enter the code, and approve. Then call this tool again."
481
+ );
482
+ }
483
+ const pending = this.#pending;
484
+ pending.lastPollAt = Date.now();
485
+ try {
486
+ const tokens = await pollDeviceToken({
487
+ issuer: await this.#issuer(),
488
+ clientId: this.#clientId,
489
+ deviceCode: pending.deviceCode,
490
+ userAgent: this.#userAgent,
491
+ fetch: this.#fetch
492
+ });
493
+ this.#pending = null;
494
+ await this.#storeTokens(tokens);
495
+ return { status: "ready", accessToken: tokens.accessToken };
496
+ } catch (error) {
497
+ if (!(error instanceof OAuthError)) throw error;
498
+ switch (error.code) {
499
+ case "authorization_pending":
500
+ return this.#pendingState("Still waiting for approval in the browser.");
501
+ case "slow_down":
502
+ pending.interval += 5;
503
+ return this.#pendingState(
504
+ `Polling too fast; the issuer asked for a wider interval. Wait a little longer before retrying.`
505
+ );
506
+ case "access_denied":
507
+ this.#pending = null;
508
+ return { status: "denied" };
509
+ case "expired_token":
510
+ this.#pending = null;
511
+ return this.#advanceDeviceFlow();
512
+ default:
513
+ throw new ManagementApiUnreachable(`the issuer refused the poll: ${error.code}`, {
514
+ cause: error
515
+ });
516
+ }
517
+ }
518
+ }
519
+ async #storeTokens(tokens) {
520
+ this.#accessToken = tokens.accessToken;
521
+ this.#accessTokenExpiresAt = Date.now() + tokens.expiresIn * 1e3;
522
+ if (tokens.refreshToken !== void 0) {
523
+ await this.#store.set(this.#profile, tokens.refreshToken);
524
+ }
525
+ }
526
+ #pendingState(message) {
527
+ const pending = this.#pending;
528
+ if (pending === null) throw new Error("invariant: #pendingState called with no pending flow");
529
+ return {
530
+ status: "authorization_required",
531
+ verificationUri: pending.verificationUri,
532
+ verificationUriComplete: pending.verificationUriComplete,
533
+ userCode: pending.userCode,
534
+ message
535
+ };
536
+ }
537
+ };
538
+
539
+ // src/management-client.ts
540
+ var DEFAULT_TIMEOUT_MS = 1e4;
541
+ var DEFAULT_MAX_CONCURRENT = 4;
542
+ var MAX_RETRIES = 3;
543
+ var ManagementClient = class {
544
+ baseUrl;
545
+ #getAccessToken;
546
+ #userAgent;
547
+ #fetch;
548
+ #timeoutMs;
549
+ #maxConcurrent;
550
+ #onTrace;
551
+ #inFlight = 0;
552
+ #queue = [];
553
+ constructor(options) {
554
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
555
+ this.#getAccessToken = options.getAccessToken;
556
+ this.#userAgent = options.userAgent;
557
+ this.#fetch = options.fetch ?? globalThis.fetch;
558
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
559
+ this.#maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
560
+ this.#onTrace = options.onTrace;
561
+ }
562
+ /**
563
+ * Calls one operation and parses the body. `args` is a flat argument
564
+ * object; this method is what splits it back into path, query and body using
565
+ * the operation's own metadata — never a lookup this package invents.
566
+ */
567
+ async call(operation, args, options = {}) {
568
+ const response = await this.request(operation, args, options);
569
+ if (response.status === 204 || response.text === "") return void 0;
570
+ return JSON.parse(response.text);
571
+ }
572
+ /** The same call, with the body left as the bytes that arrived. */
573
+ async request(operation, args, options = {}) {
574
+ await this.#acquire();
575
+ try {
576
+ return await this.#withRetry(operation, args, options, 0);
577
+ } finally {
578
+ this.#release();
579
+ }
580
+ }
581
+ async #withRetry(operation, args, options, attempt) {
582
+ const url = this.#urlFor(operation, args);
583
+ const body = this.#bodyFor(operation, args);
584
+ const token = await this.#getAccessToken();
585
+ const headers = {
586
+ Accept: "application/json, application/problem+json",
587
+ ...options.headers ?? {},
588
+ Authorization: `Bearer ${token}`,
589
+ "User-Agent": this.#userAgent
590
+ };
591
+ if (body !== void 0) headers["Content-Type"] = "application/json";
592
+ const method = operation.method.toUpperCase();
593
+ const startedAt = Date.now();
594
+ let response;
595
+ try {
596
+ response = await this.#fetch(url, {
597
+ method,
598
+ headers,
599
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
600
+ signal: AbortSignal.timeout(this.#timeoutMs)
601
+ });
602
+ } catch (error) {
603
+ this.#onTrace?.({
604
+ method,
605
+ path: url.pathname + url.search,
606
+ status: null,
607
+ durationMs: Date.now() - startedAt,
608
+ requestId: void 0,
609
+ error: error instanceof Error ? error.message : String(error)
610
+ });
611
+ throw new ManagementApiUnreachable(`could not reach ${this.baseUrl}`, { cause: error });
612
+ }
613
+ this.#onTrace?.({
614
+ method,
615
+ path: url.pathname + url.search,
616
+ status: response.status,
617
+ durationMs: Date.now() - startedAt,
618
+ requestId: response.headers.get("x-request-id") ?? void 0
619
+ });
620
+ if (response.status === 429 && attempt < MAX_RETRIES && options.idempotent !== false) {
621
+ const retryAfter = retryAfterSeconds(response);
622
+ if (retryAfter !== null) {
623
+ await sleep(retryAfter * 1e3);
624
+ return this.#withRetry(operation, args, options, attempt + 1);
625
+ }
626
+ }
627
+ if (!response.ok) throw await toApiError(response);
628
+ return {
629
+ status: response.status,
630
+ text: response.status === 204 ? "" : await response.text(),
631
+ headers: response.headers
632
+ };
633
+ }
634
+ #urlFor(operation, args) {
635
+ const path = operation.path.replace(/\{([^}]+)\}/g, (_, name) => {
636
+ const value = args[name];
637
+ if (typeof value !== "string" || value === "") {
638
+ throw new Error(`operation '${operation.operationId}' needs a path parameter '${name}'`);
639
+ }
640
+ return encodeURIComponent(value);
641
+ });
642
+ const url = new URL(this.baseUrl + path);
643
+ for (const name of operation.queryParams) {
644
+ const value = args[name];
645
+ if (value === void 0 || value === null) continue;
646
+ for (const item of Array.isArray(value) ? value : [value]) {
647
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
648
+ url.searchParams.append(name, String(item));
649
+ }
650
+ }
651
+ }
652
+ return url;
653
+ }
654
+ #bodyFor(operation, args) {
655
+ if (!operation.hasBody) return void 0;
656
+ const excluded = /* @__PURE__ */ new Set([...operation.pathParams, ...operation.queryParams]);
657
+ const body = {};
658
+ for (const [name, value] of Object.entries(args)) {
659
+ if (excluded.has(name)) continue;
660
+ if (value !== void 0) body[name] = value;
661
+ }
662
+ return body;
663
+ }
664
+ /** A tiny counting semaphore — no dependency for something this small. */
665
+ #acquire() {
666
+ if (this.#inFlight < this.#maxConcurrent) {
667
+ this.#inFlight++;
668
+ return Promise.resolve();
669
+ }
670
+ return new Promise((resolve) => {
671
+ this.#queue.push(() => {
672
+ this.#inFlight++;
673
+ resolve();
674
+ });
675
+ });
676
+ }
677
+ #release() {
678
+ this.#inFlight--;
679
+ const next = this.#queue.shift();
680
+ if (next) next();
681
+ }
682
+ };
683
+ function sleep(ms) {
684
+ return new Promise((resolve) => setTimeout(resolve, ms));
685
+ }
686
+ function retryAfterSeconds(response) {
687
+ const raw = response.headers.get("retry-after")?.trim();
688
+ if (!raw) return null;
689
+ const seconds = Number(raw);
690
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(seconds, 60);
691
+ const at = Date.parse(raw);
692
+ if (Number.isFinite(at)) return Math.min(Math.max(0, Math.round((at - Date.now()) / 1e3)), 60);
693
+ return null;
694
+ }
695
+ async function toApiError(response) {
696
+ let document = {};
697
+ try {
698
+ const body = JSON.parse(await response.text());
699
+ if (typeof body === "object" && body !== null && !Array.isArray(body)) {
700
+ document = body;
701
+ }
702
+ } catch {
703
+ }
704
+ const problem = {
705
+ document,
706
+ retryAfter: retryAfterSeconds(response),
707
+ requestId: typeof document["request_id"] === "string" ? document["request_id"] : response.headers.get("x-request-id") ?? void 0
708
+ };
709
+ return new ApiError(
710
+ typeof document["type"] === "string" ? document["type"] : "about:blank",
711
+ response.status,
712
+ typeof document["title"] === "string" ? document["title"] : void 0,
713
+ typeof document["detail"] === "string" ? document["detail"] : void 0,
714
+ problem
715
+ );
716
+ }
717
+
718
+ export { ApiError, AuthorizationDenied, FileCredentialStore, KleoraToolError, ManagementApiUnreachable, ManagementClient, OAuthError, PLATFORM_PATH, Session, SessionNotReady, createCredentialStore, defaultConfigDir, discoverIssuer, issuerResolver, pollDeviceToken, refreshAccessToken, retryAfterSeconds, revokeRefreshToken, startDeviceAuthorization };
719
+ //# sourceMappingURL=index.js.map
720
+ //# sourceMappingURL=index.js.map