@iwas/mcp 1.0.0 → 1.0.2

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,2451 +1,25 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJS = (cb, mod) => function __require() {
9
- try {
10
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
- } catch (e) {
12
- throw mod = 0, e;
13
- }
14
- };
15
- var __export = (target, all) => {
16
- for (var name in all)
17
- __defProp(target, name, { get: all[name], enumerable: true });
18
- };
19
- var __copyProps = (to, from, except, desc) => {
20
- if (from && typeof from === "object" || typeof from === "function") {
21
- for (let key of __getOwnPropNames(from))
22
- if (!__hasOwnProp.call(to, key) && key !== except)
23
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
- }
25
- return to;
26
- };
27
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
- // If the importer is in node compatibility mode or this is not an ESM
29
- // file that has been converted to a CommonJS file using a Babel-
30
- // compatible transform (i.e. "__esModule" has not been set), then set
31
- // "default" to the CommonJS "module.exports" for node compatibility.
32
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
- mod
34
- ));
35
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
-
37
- // ../auth/dist/rbac/permissions.js
38
- var require_permissions = __commonJS({
39
- "../auth/dist/rbac/permissions.js"(exports2) {
40
- "use strict";
41
- Object.defineProperty(exports2, "__esModule", { value: true });
42
- exports2.MCP_OAUTH_SCOPES = exports2.OFFLINE_ACCESS_SCOPE = exports2.MCP_ELIGIBLE_PERMISSION_SET = exports2.MCP_ELIGIBLE_PERMISSIONS = exports2.SENSITIVE_MCP_RESOURCES = exports2.PERMISSION_SET = exports2.ALL_PERMISSIONS = exports2.CATALOG_DEFINITIONS = void 0;
43
- exports2.isPermissionString = isPermissionString;
44
- exports2.parsePermissionString = parsePermissionString;
45
- exports2.isSensitiveMcpResource = isSensitiveMcpResource;
46
- exports2.isMcpEligiblePermission = isMcpEligiblePermission;
47
- exports2.partitionMcpScopes = partitionMcpScopes;
48
- exports2.CATALOG_DEFINITIONS = {
49
- organization: {
50
- tier: "T",
51
- actions: ["update", "delete"],
52
- description: "Organization tenant profile and settings",
53
- models: ["Organization", "OrganizationSetting", "OrganizationSubscription"],
54
- mcpEligible: true
55
- },
56
- member: {
57
- tier: "T",
58
- actions: ["create", "update", "delete"],
59
- description: "Organization member management",
60
- models: ["Member"],
61
- mcpEligible: true
62
- },
63
- invitation: {
64
- tier: "T",
65
- actions: ["create", "cancel"],
66
- description: "Organization membership invitations",
67
- models: ["Invitation"],
68
- mcpEligible: true
69
- },
70
- location: {
71
- tier: "T",
72
- actions: ["read", "write", "delete"],
73
- description: "Physical hotspot location management",
74
- models: ["Location"],
75
- mcpEligible: true
76
- },
77
- router: {
78
- tier: "T",
79
- actions: ["read", "write", "delete", "control"],
80
- description: "Gateway router inventory, provisioning and control",
81
- models: ["Router", "ZtpToken", "CoaQueue", "IpamAllocationCursor"],
82
- mcpEligible: true
83
- },
84
- package: {
85
- tier: "S",
86
- actions: ["read", "write", "delete", "publish"],
87
- description: "WiFi access package catalog (tenant + shared)",
88
- models: ["Package", "VoucherPackage"],
89
- mcpEligible: true
90
- },
91
- voucher: {
92
- tier: "S",
93
- actions: ["read", "write", "publish", "issue", "revoke"],
94
- description: "Prepaid voucher codes and issuance (tenant + shared)",
95
- models: ["Voucher", "VoucherIssuance", "VoucherUser"],
96
- mcpEligible: true
97
- },
98
- session: {
99
- tier: "T",
100
- actions: ["read", "control"],
101
- description: "Active and historical WiFi guest sessions",
102
- models: ["WifiSession", "RadiusAccounting", "DeviceToken"],
103
- mcpEligible: true
104
- },
105
- wallet: {
106
- tier: "T",
107
- actions: ["read", "topup", "charge", "refund", "freeze"],
108
- description: "Prepaid wallet balances, topups and ledger transactions (SENSITIVE - MCP EXCLUDED)",
109
- models: ["Wallet", "WalletTransaction", "TopupRequest"],
110
- hasOwnQualifier: true,
111
- mcpEligible: false
112
- },
113
- payment: {
114
- tier: "T",
115
- actions: ["read", "approve", "refund"],
116
- description: "Inbound payment transactions and gateway integrations (SENSITIVE - MCP EXCLUDED)",
117
- models: ["PaymentTransaction", "PaymentGatewayIntegration"],
118
- hasOwnQualifier: true,
119
- mcpEligible: false
120
- },
121
- subscription: {
122
- tier: "T",
123
- actions: ["read", "manage"],
124
- description: "SaaS organization plan subscription and entitlements",
125
- models: ["OrganizationSubscription"],
126
- mcpEligible: true
127
- },
128
- partner: {
129
- tier: "T",
130
- actions: ["read", "manage", "payout:approve"],
131
- description: "Affiliate/partner programs and payout requests",
132
- models: ["Partner", "PartnerPayoutRequest"],
133
- mcpEligible: true
134
- },
135
- campaign: {
136
- tier: "T",
137
- actions: ["read", "write"],
138
- description: "Captive portal ad campaigns and lead capture",
139
- models: ["AdCampaign", "LeadCapture"],
140
- mcpEligible: true
141
- },
142
- notification: {
143
- tier: "T",
144
- actions: ["read", "send", "template:write"],
145
- description: "Multi-channel notifications, templates and logs",
146
- models: ["NotificationItem", "NotificationTemplate", "DeliveryLog"],
147
- hasOwnQualifier: true,
148
- mcpEligible: true
149
- },
150
- store: {
151
- tier: "P",
152
- actions: ["read", "write"],
153
- description: "Public hardware/affiliate store catalog",
154
- models: ["StoreProduct", "AffiliateClick"],
155
- mcpEligible: true
156
- },
157
- blog: {
158
- tier: "P",
159
- actions: ["read", "write", "publish"],
160
- description: "Company-wide editorial articles and CDN media",
161
- models: ["BlogPost", "Media"],
162
- hasOwnQualifier: true,
163
- mcpEligible: true
164
- },
165
- analytics: {
166
- tier: "D",
167
- actions: ["read"],
168
- description: "Aggregated usage, session and revenue metrics",
169
- models: ["WifiSession", "Package", "PaymentTransaction"],
170
- mcpEligible: true
171
- },
172
- diagnostics: {
173
- tier: "P",
174
- actions: ["read", "write"],
175
- description: "System error events, MCP tool execution spans, flow events, issue triage and audit logs",
176
- models: ["ErrorEvent", "McpToolSpan", "AuditLog", "SystemSetting", "FlowEvent", "IssueState"],
177
- mcpEligible: true
178
- }
179
- };
180
- exports2.ALL_PERMISSIONS = [
181
- "organization:update",
182
- "organization:delete",
183
- "member:create",
184
- "member:update",
185
- "member:delete",
186
- "invitation:create",
187
- "invitation:cancel",
188
- "location:read",
189
- "location:write",
190
- "location:delete",
191
- "router:read",
192
- "router:write",
193
- "router:delete",
194
- "router:control",
195
- "package:read",
196
- "package:write",
197
- "package:delete",
198
- "package:publish",
199
- "voucher:read",
200
- "voucher:write",
201
- "voucher:publish",
202
- "voucher:issue",
203
- "voucher:revoke",
204
- "session:read",
205
- "session:control",
206
- "wallet:read",
207
- "wallet:topup",
208
- "wallet:charge",
209
- "wallet:refund",
210
- "wallet:freeze",
211
- "payment:read",
212
- "payment:approve",
213
- "payment:refund",
214
- "subscription:read",
215
- "subscription:manage",
216
- "partner:read",
217
- "partner:manage",
218
- "partner:payout:approve",
219
- "campaign:read",
220
- "campaign:write",
221
- "notification:read",
222
- "notification:send",
223
- "notification:template:write",
224
- "store:read",
225
- "store:write",
226
- "blog:read",
227
- "blog:write",
228
- "blog:publish",
229
- "analytics:read",
230
- "diagnostics:read",
231
- "diagnostics:write"
232
- ];
233
- exports2.PERMISSION_SET = new Set(exports2.ALL_PERMISSIONS);
234
- function isPermissionString(value) {
235
- return typeof value === "string" && exports2.PERMISSION_SET.has(value);
236
- }
237
- function parsePermissionString(value) {
238
- if (!isPermissionString(value)) {
239
- return null;
240
- }
241
- const colonIndex = value.indexOf(":");
242
- const resource = value.slice(0, colonIndex);
243
- const action = value.slice(colonIndex + 1);
244
- return { resource, action };
245
- }
246
- exports2.SENSITIVE_MCP_RESOURCES = ["payment", "wallet"];
247
- function isSensitiveMcpResource(resource) {
248
- return resource === "payment" || resource === "wallet";
249
- }
250
- function isMcpEligiblePermission(permission) {
251
- const parsed = parsePermissionString(permission);
252
- if (!parsed)
253
- return false;
254
- return !isSensitiveMcpResource(parsed.resource);
255
- }
256
- exports2.MCP_ELIGIBLE_PERMISSIONS = exports2.ALL_PERMISSIONS.filter(isMcpEligiblePermission);
257
- exports2.MCP_ELIGIBLE_PERMISSION_SET = new Set(exports2.MCP_ELIGIBLE_PERMISSIONS);
258
- exports2.OFFLINE_ACCESS_SCOPE = "offline_access";
259
- exports2.MCP_OAUTH_SCOPES = [
260
- ...exports2.MCP_ELIGIBLE_PERMISSIONS,
261
- exports2.OFFLINE_ACCESS_SCOPE
262
- ];
263
- function partitionMcpScopes(scopes) {
264
- return {
265
- permissionScopes: scopes.filter((scope) => scope !== exports2.OFFLINE_ACCESS_SCOPE),
266
- offlineAccessRequested: scopes.includes(exports2.OFFLINE_ACCESS_SCOPE)
267
- };
268
- }
269
- }
270
- });
271
-
272
- // src/auth/index.ts
273
- var auth_exports = {};
274
- __export(auth_exports, {
275
- CredentialVault: () => CredentialVault,
276
- DEFAULT_CLI_CLIENT_ID: () => DEFAULT_CLI_CLIENT_ID,
277
- DEFAULT_KEYCHAIN_SERVICE: () => DEFAULT_KEYCHAIN_SERVICE,
278
- DEFAULT_READ_ONLY_SCOPES: () => DEFAULT_READ_ONLY_SCOPES,
279
- DEFAULT_SCOPES: () => DEFAULT_SCOPES,
280
- DEFAULT_SERVER_URL: () => DEFAULT_SERVER_URL,
281
- DEVICE_CODE_GRANT_TYPE: () => DEVICE_CODE_GRANT_TYPE,
282
- DefaultKeychainRunner: () => DefaultKeychainRunner,
283
- HttpError: () => HttpError,
284
- KeychainService: () => KeychainService,
285
- ReauthenticationRequiredError: () => ReauthenticationRequiredError,
286
- TokenRefreshManager: () => TokenRefreshManager,
287
- VaultLock: () => VaultLock,
288
- VaultLockError: () => VaultLockError,
289
- VaultLockTimeoutError: () => VaultLockTimeoutError,
290
- decodeJwtPayload: () => decodeJwtPayload,
291
- defaultIsProcessAlive: () => defaultIsProcessAlive,
292
- executeLogin: () => executeLogin,
293
- executeLogout: () => executeLogout,
294
- executeStatus: () => executeStatus,
295
- isCredentialExpired: () => isCredentialExpired,
296
- isLocalhost: () => isLocalhost,
297
- isValidStoredCredentials: () => isValidStoredCredentials,
298
- normalizeProfileKey: () => normalizeProfileKey,
299
- openBrowser: () => openBrowser,
300
- pollDeviceToken: () => pollDeviceToken,
301
- redactCredentials: () => redactCredentials,
302
- requestDeviceCode: () => requestDeviceCode,
303
- resolveAuthServerUrl: () => resolveAuthServerUrl,
304
- resolveMcpResourceUrl: () => resolveMcpResourceUrl,
305
- sanitizeSecretString: () => sanitizeSecretString,
306
- sendRequest: () => sendRequest,
307
- validateScopes: () => validateScopes,
308
- withVaultLock: () => withVaultLock
309
- });
310
- module.exports = __toCommonJS(auth_exports);
311
-
312
- // src/auth/types.ts
313
- function isValidStoredCredentials(obj) {
314
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
315
- return false;
316
- }
317
- const record = obj;
318
- if (typeof record.serverUrl !== "string" || typeof record.clientId !== "string" || typeof record.accessToken !== "string" || typeof record.tokenType !== "string" || typeof record.expiresAt !== "number" || !Number.isFinite(record.expiresAt)) {
319
- return false;
320
- }
321
- if (!("organizationId" in record) || record.organizationId !== null && typeof record.organizationId !== "string") {
322
- return false;
323
- }
324
- if (record.refreshToken !== void 0 && typeof record.refreshToken !== "string") {
325
- return false;
326
- }
327
- if (record.scope !== void 0 && typeof record.scope !== "string") {
328
- return false;
329
- }
330
- if (record.resource !== void 0 && typeof record.resource !== "string") {
331
- return false;
332
- }
333
- if (record.updatedAt !== void 0 && (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt))) {
334
- return false;
335
- }
336
- return true;
337
- }
338
- function normalizeProfileKey(serverUrl, clientId) {
339
- const cleanClient = (clientId || "").trim();
340
- let urlToParse = (serverUrl || "").trim();
341
- if (!/^https?:\/\//i.test(urlToParse)) {
342
- urlToParse = `https://${urlToParse}`;
343
- }
344
- try {
345
- const parsed = new URL(urlToParse);
346
- const origin = parsed.origin.toLowerCase();
347
- return `${origin}::${cleanClient}`;
348
- } catch {
349
- const cleanUrl = urlToParse.replace(/\/+$/, "").toLowerCase();
350
- return `${cleanUrl}::${cleanClient}`;
351
- }
352
- }
353
- function redactCredentials(creds) {
354
- const result = { ...creds };
355
- if ("accessToken" in result && typeof result.accessToken === "string") {
356
- result.accessToken = "[REDACTED]";
357
- }
358
- if ("refreshToken" in result && typeof result.refreshToken === "string") {
359
- result.refreshToken = "[REDACTED]";
360
- }
361
- return result;
362
- }
363
- function sanitizeSecretString(input, secrets) {
364
- let sanitized = input;
365
- for (const secret of secrets) {
366
- if (secret && typeof secret === "string" && secret.length >= 4) {
367
- sanitized = sanitized.split(secret).join("[REDACTED]");
368
- }
369
- }
370
- return sanitized;
371
- }
372
- function isCredentialExpired(creds, skewSeconds = 60) {
373
- if (!creds.expiresAt) {
374
- return false;
375
- }
376
- const expiresAtMs = creds.expiresAt > 1e11 ? creds.expiresAt : creds.expiresAt * 1e3;
377
- return Date.now() + skewSeconds * 1e3 >= expiresAtMs;
378
- }
379
-
380
- // src/auth/keychain.ts
381
- var import_node_child_process = require("child_process");
382
- var DefaultKeychainRunner = class {
383
- defaultTimeoutMs;
384
- constructor(defaultTimeoutMs = 5e3) {
385
- this.defaultTimeoutMs = defaultTimeoutMs;
386
- }
387
- spawn(command, args, options) {
388
- return new Promise((resolve) => {
389
- let child;
390
- try {
391
- child = (0, import_node_child_process.spawn)(command, args, {
392
- shell: false,
393
- stdio: ["pipe", "pipe", "pipe"]
394
- });
395
- } catch (err) {
396
- const message = err instanceof Error ? err.message : String(err);
397
- return resolve({ stdout: "", stderr: message, exitCode: -1 });
398
- }
399
- let stdout = "";
400
- let stderr = "";
401
- let isSettled = false;
402
- const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
403
- const timer = setTimeout(() => {
404
- if (!isSettled) {
405
- isSettled = true;
406
- try {
407
- child.kill("SIGKILL");
408
- } catch {
409
- }
410
- resolve({
411
- stdout,
412
- stderr: `Keychain operation timed out after ${timeoutMs}ms`,
413
- exitCode: -1
414
- });
415
- }
416
- }, timeoutMs);
417
- child.stdout?.on("data", (chunk) => {
418
- stdout += chunk.toString();
419
- });
420
- child.stderr?.on("data", (chunk) => {
421
- stderr += chunk.toString();
422
- });
423
- child.on("error", (err) => {
424
- stderr += err.message;
425
- });
426
- child.on("close", (code) => {
427
- if (!isSettled) {
428
- isSettled = true;
429
- clearTimeout(timer);
430
- resolve({
431
- stdout,
432
- stderr,
433
- exitCode: code ?? -1
434
- });
435
- }
436
- });
437
- if (options?.input !== void 0 && child.stdin) {
438
- try {
439
- child.stdin.write(options.input);
440
- child.stdin.end();
441
- } catch {
442
- }
443
- }
444
- });
445
- }
446
- };
447
- var DEFAULT_KEYCHAIN_SERVICE = "iwas-cli";
448
- var KeychainService = class {
449
- service;
450
- runner;
451
- platform;
452
- constructor(options) {
453
- this.service = options?.serviceName || DEFAULT_KEYCHAIN_SERVICE;
454
- this.runner = options?.runner || new DefaultKeychainRunner();
455
- this.platform = options?.platform || process.platform;
456
- }
457
- availableCache = null;
458
- /**
459
- * Returns the configured OS platform.
460
- */
461
- getPlatform() {
462
- return this.platform;
463
- }
464
- /**
465
- * Probes whether the host environment has an operable keychain utility.
466
- * Results are cached when available to avoid unnecessary subprocess spawns (n7 fix).
467
- */
468
- async isAvailable(forceProbe = false) {
469
- if (!forceProbe && this.availableCache === true) {
470
- return true;
471
- }
472
- if (this.platform === "darwin") {
473
- const res = await this.runner.spawn("/usr/bin/security", ["help"]);
474
- const isAvail = res.exitCode === 0 || res.stdout.includes("security") || res.stderr.includes("security");
475
- if (isAvail) {
476
- this.availableCache = true;
477
- }
478
- return isAvail;
479
- }
480
- if (this.platform === "linux") {
481
- const res = await this.runner.spawn("which", ["secret-tool"]);
482
- const isAvail = res.exitCode === 0;
483
- if (isAvail) {
484
- this.availableCache = true;
485
- }
486
- return isAvail;
487
- }
488
- return false;
489
- }
490
- /**
491
- * Clears the cached isAvailable probe result. Useful for testing and environment shifts.
492
- */
493
- clearAvailabilityCache() {
494
- this.availableCache = null;
495
- }
496
- /**
497
- * Retrieves a secret password string by account identifier.
498
- */
499
- async getSecret(account) {
500
- if (this.platform === "darwin") {
501
- const res = await this.runner.spawn("/usr/bin/security", [
502
- "find-generic-password",
503
- "-s",
504
- this.service,
505
- "-a",
506
- account,
507
- "-w"
508
- ]);
509
- if (res.exitCode === 0) {
510
- const raw = res.stdout.trim();
511
- if (raw.length > 0 && raw.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(raw)) {
512
- if (raw.startsWith("7b") || raw.startsWith("7B") || raw.startsWith("5b") || raw.startsWith("5B")) {
513
- try {
514
- const decoded = Buffer.from(raw, "hex").toString("utf8");
515
- if (!decoded.includes("\uFFFD")) {
516
- return decoded;
517
- }
518
- } catch {
519
- }
520
- }
521
- }
522
- return raw;
523
- }
524
- if (res.exitCode === 44 || res.stderr.includes("could not be found") || res.stderr.includes("The specified item could not be found")) {
525
- return null;
526
- }
527
- throw new Error(`Keychain read failed: ${res.stderr.trim() || `exit code ${res.exitCode}`}`);
528
- }
529
- if (this.platform === "linux") {
530
- const res = await this.runner.spawn("secret-tool", [
531
- "lookup",
532
- "service",
533
- this.service,
534
- "account",
535
- account
536
- ]);
537
- if (res.exitCode === 0) {
538
- return res.stdout.length > 0 ? res.stdout.trim() : null;
539
- }
540
- const err = res.stderr.trim();
541
- if (res.exitCode === 1 && (err === "" || err.toLowerCase().includes("not found") || err.toLowerCase().includes("no such secret"))) {
542
- return null;
543
- }
544
- throw new Error(`Secret-tool read failed: ${err || `exit code ${res.exitCode}`}`);
545
- }
546
- throw new Error(`OS keychain not supported on platform: ${this.platform}`);
547
- }
548
- /**
549
- * Stores a secret password under the given account identifier.
550
- */
551
- async setSecret(account, secret, label) {
552
- const itemLabel = label || `${this.service} (${account})`;
553
- const secretsToRedact = [secret];
554
- try {
555
- const parsed = JSON.parse(secret);
556
- if (typeof parsed === "object" && parsed !== null) {
557
- for (const val of Object.values(parsed)) {
558
- if (typeof val === "string" && val.length >= 4) {
559
- secretsToRedact.push(val);
560
- }
561
- }
562
- }
563
- } catch {
564
- }
565
- if (this.platform === "darwin") {
566
- const res = await this.runner.spawn("/usr/bin/security", [
567
- "add-generic-password",
568
- "-U",
569
- "-s",
570
- this.service,
571
- "-a",
572
- account,
573
- "-l",
574
- itemLabel,
575
- "-w",
576
- secret
577
- ]);
578
- if (res.exitCode !== 0) {
579
- const errorMsg = sanitizeSecretString(res.stderr.trim(), secretsToRedact);
580
- throw new Error(`Keychain write failed: ${errorMsg || `exit code ${res.exitCode}`}`);
581
- }
582
- return;
583
- }
584
- if (this.platform === "linux") {
585
- const res = await this.runner.spawn(
586
- "secret-tool",
587
- ["store", `--label=${itemLabel}`, "service", this.service, "account", account],
588
- { input: secret }
589
- );
590
- if (res.exitCode !== 0) {
591
- const errorMsg = sanitizeSecretString(res.stderr.trim(), secretsToRedact);
592
- throw new Error(`Secret-tool write failed: ${errorMsg || `exit code ${res.exitCode}`}`);
593
- }
594
- return;
595
- }
596
- throw new Error(`OS keychain not supported on platform: ${this.platform}`);
597
- }
598
- /**
599
- * Deletes a secret from the keychain. Operation is strictly idempotent.
600
- */
601
- async deleteSecret(account) {
602
- if (this.platform === "darwin") {
603
- const res = await this.runner.spawn("/usr/bin/security", [
604
- "delete-generic-password",
605
- "-s",
606
- this.service,
607
- "-a",
608
- account
609
- ]);
610
- if (res.exitCode === 0 || res.exitCode === 44 || res.stderr.includes("could not be found")) {
611
- return;
612
- }
613
- throw new Error(`Keychain delete failed: ${res.stderr.trim() || `exit code ${res.exitCode}`}`);
614
- }
615
- if (this.platform === "linux") {
616
- const res = await this.runner.spawn("secret-tool", [
617
- "clear",
618
- "service",
619
- this.service,
620
- "account",
621
- account
622
- ]);
623
- if (res.exitCode === 0 || res.exitCode === 1) {
624
- return;
625
- }
626
- throw new Error(`Secret-tool clear failed: ${res.stderr.trim() || `exit code ${res.exitCode}`}`);
627
- }
628
- throw new Error(`OS keychain not supported on platform: ${this.platform}`);
629
- }
630
- };
631
-
632
- // src/auth/credentials-vault.ts
633
- var import_node_fs = __toESM(require("fs"));
634
- var import_node_path = __toESM(require("path"));
635
- var import_os = __toESM(require("os"));
636
- var KEYCHAIN_PROFILES_INDEX_ACCOUNT = "__profiles__";
637
- var CredentialVault = class {
638
- vaultType;
639
- configDir;
640
- credentialsFilePath;
641
- keychain;
642
- constructor(options) {
643
- const rawEnvVault = process.env.IWAS_MCP_VAULT;
644
- if (rawEnvVault && !["auto", "keychain", "file"].includes(rawEnvVault)) {
645
- throw new Error(
646
- `Invalid IWAS_MCP_VAULT value: "${rawEnvVault}". Supported values are "auto", "keychain", or "file".`
647
- );
648
- }
649
- const envVault = rawEnvVault;
650
- this.vaultType = options?.vaultType || envVault || "auto";
651
- const baseConfigDir = options?.configDir || process.env.XDG_CONFIG_HOME || import_node_path.default.join(import_os.default.homedir(), ".config");
652
- this.configDir = options?.configDir ? options.configDir : import_node_path.default.join(baseConfigDir, "iwas");
653
- this.credentialsFilePath = import_node_path.default.join(this.configDir, "credentials.json");
654
- this.keychain = options?.keychainService || new KeychainService({
655
- serviceName: options?.serviceName || DEFAULT_KEYCHAIN_SERVICE,
656
- runner: options?.keychainRunner
657
- });
658
- }
659
- /**
660
- * Returns the configuration directory for this vault (~/.config/iwas).
661
- */
662
- getConfigDir() {
663
- return this.configDir;
664
- }
665
- /**
666
- * Returns the credentials file path (~/.config/iwas/credentials.json).
667
- */
668
- getCredentialsFilePath() {
669
- return this.credentialsFilePath;
670
- }
671
- /**
672
- * Returns the resolved vault mode: 'keychain' or 'file'.
673
- * In strict 'keychain' mode, throws if OS keychain is unavailable.
674
- */
675
- async getEffectiveBackend() {
676
- if (this.vaultType === "file") {
677
- return "file";
678
- }
679
- const available = await this.keychain.isAvailable().catch(() => false);
680
- const platform = this.keychain.getPlatform();
681
- if (this.vaultType === "keychain") {
682
- if (!available) {
683
- throw new Error(
684
- `Keychain vault requested (IWAS_MCP_VAULT="keychain") but OS keychain is unavailable on platform: ${platform}`
685
- );
686
- }
687
- return "keychain";
688
- }
689
- return available ? "keychain" : "file";
690
- }
691
- /**
692
- * Retrieves stored credentials by profile key.
693
- * In auto mode, checks keychain first; falls back to file if missing or failed.
694
- * Compares timestamps if both exist and migrates newer file credentials to keychain (Medium fix).
695
- */
696
- async load(profileKey) {
697
- const backend = await this.getEffectiveBackend();
698
- if (backend === "keychain") {
699
- let kcCreds = null;
700
- try {
701
- const raw = await this.keychain.getSecret(profileKey);
702
- if (raw) {
703
- try {
704
- const parsed = JSON.parse(raw);
705
- if (isValidStoredCredentials(parsed)) {
706
- kcCreds = parsed;
707
- } else if (this.vaultType === "keychain") {
708
- throw new Error(`Corrupt credentials stored in keychain for profile: ${profileKey}`);
709
- }
710
- } catch (parseErr) {
711
- if (this.vaultType === "keychain") {
712
- const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
713
- throw new Error(`Corrupt credentials JSON in keychain for profile ${profileKey}: ${msg}`);
714
- }
715
- }
716
- }
717
- } catch (err) {
718
- if (this.vaultType === "keychain") {
719
- throw err;
720
- }
721
- }
722
- if (this.vaultType === "keychain") {
723
- return kcCreds;
724
- }
725
- const fileCreds = this.loadFromFile(profileKey);
726
- if (!kcCreds) {
727
- if (fileCreds) {
728
- try {
729
- await this.keychain.setSecret(
730
- profileKey,
731
- JSON.stringify(fileCreds),
732
- `IWAS MCP Profile (${profileKey})`
733
- );
734
- await this.addProfileToKeychainIndex(profileKey);
735
- this.deleteFromFile(profileKey);
736
- } catch {
737
- }
738
- }
739
- return fileCreds;
740
- }
741
- if (!fileCreds) {
742
- return kcCreds;
743
- }
744
- const kcTime = kcCreds.updatedAt ?? kcCreds.expiresAt ?? 0;
745
- const fileTime = fileCreds.updatedAt ?? fileCreds.expiresAt ?? 0;
746
- if (fileTime > kcTime) {
747
- try {
748
- await this.keychain.setSecret(
749
- profileKey,
750
- JSON.stringify(fileCreds),
751
- `IWAS MCP Profile (${profileKey})`
752
- );
753
- await this.addProfileToKeychainIndex(profileKey);
754
- this.deleteFromFile(profileKey);
755
- } catch {
756
- }
757
- return fileCreds;
758
- } else {
759
- this.deleteFromFile(profileKey);
760
- return kcCreds;
761
- }
762
- }
763
- return this.loadFromFile(profileKey);
764
- }
765
- /**
766
- * Persists credentials for the given profile key.
767
- * When saving to keychain, purges any stale copy from the file fallback (Major 1).
768
- */
769
- async save(profileKey, credentials) {
770
- const backend = await this.getEffectiveBackend();
771
- const toSave = {
772
- ...credentials,
773
- updatedAt: credentials.updatedAt ?? Date.now()
774
- };
775
- if (backend === "keychain") {
776
- let secretSaved = false;
777
- try {
778
- const secret = JSON.stringify(toSave);
779
- await this.keychain.setSecret(
780
- profileKey,
781
- secret,
782
- `IWAS MCP Profile (${profileKey})`
783
- );
784
- secretSaved = true;
785
- try {
786
- await this.addProfileToKeychainIndex(profileKey);
787
- } catch (indexErr) {
788
- const message = indexErr instanceof Error ? indexErr.message : String(indexErr);
789
- const sanitized = sanitizeSecretString(message, [
790
- toSave.accessToken,
791
- toSave.refreshToken
792
- ]);
793
- throw new Error(`Keychain index update failed: ${sanitized}`);
794
- }
795
- this.deleteFromFile(profileKey);
796
- return;
797
- } catch (err) {
798
- if (this.vaultType === "keychain" || secretSaved) {
799
- const message = err instanceof Error ? err.message : String(err);
800
- const sanitized = sanitizeSecretString(message, [
801
- toSave.accessToken,
802
- toSave.refreshToken
803
- ]);
804
- throw err instanceof Error && err.message.startsWith("Keychain") ? err : new Error(`Keychain save failed: ${sanitized}`);
805
- }
806
- }
807
- }
808
- this.saveToFile(profileKey, toSave);
809
- }
810
- /**
811
- * Removes credentials for the given profile key.
812
- * Cleanses BOTH Keychain and File fallback to prevent plaintext leakage (Major 1).
813
- * In auto mode, throws if keychain is available but secret deletion fails (Major fix).
814
- */
815
- async delete(profileKey) {
816
- let keychainError = null;
817
- let isAvailable = false;
818
- if (this.vaultType === "file") {
819
- this.deleteFromFile(profileKey);
820
- return;
821
- }
822
- try {
823
- isAvailable = await this.keychain.isAvailable().catch(() => false);
824
- if (this.vaultType === "keychain" && !isAvailable) {
825
- throw new Error(
826
- `Keychain vault requested (IWAS_MCP_VAULT="keychain") but OS keychain is unavailable on platform: ${this.keychain.getPlatform()}`
827
- );
828
- }
829
- if (isAvailable) {
830
- await this.keychain.deleteSecret(profileKey);
831
- await this.removeProfileFromKeychainIndex(profileKey);
832
- }
833
- } catch (err) {
834
- keychainError = err;
835
- }
836
- this.deleteFromFile(profileKey);
837
- if (keychainError) {
838
- if (this.vaultType === "keychain" || this.vaultType === "auto" && isAvailable) {
839
- throw keychainError;
840
- }
841
- }
842
- }
843
- /**
844
- * Lists all stored profile keys.
845
- * In auto mode, merges profiles from both Keychain and File.
846
- */
847
- async listProfiles() {
848
- if (this.vaultType === "file") {
849
- return this.listProfilesFromFile();
850
- }
851
- if (this.vaultType === "keychain") {
852
- return this.readKeychainIndex();
853
- }
854
- const profiles = /* @__PURE__ */ new Set();
855
- try {
856
- const isAvailable = await this.keychain.isAvailable();
857
- if (isAvailable) {
858
- const kcProfiles = await this.readKeychainIndex();
859
- for (const p of kcProfiles) {
860
- profiles.add(p);
861
- }
862
- }
863
- } catch {
864
- }
865
- for (const p of this.listProfilesFromFile()) {
866
- profiles.add(p);
867
- }
868
- return Array.from(profiles);
869
- }
870
- // --- Internal Keychain Index (Major 2: Safe read & abort on error) ---
871
- async readKeychainIndex() {
872
- const rawIndex = await this.keychain.getSecret(KEYCHAIN_PROFILES_INDEX_ACCOUNT);
873
- if (!rawIndex) {
874
- return [];
875
- }
876
- try {
877
- const parsed = JSON.parse(rawIndex);
878
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
879
- } catch (err) {
880
- const message = err instanceof Error ? err.message : String(err);
881
- throw new Error(`Corrupt keychain profile index at ${KEYCHAIN_PROFILES_INDEX_ACCOUNT}: ${message}`);
882
- }
883
- }
884
- async addProfileToKeychainIndex(profileKey) {
885
- const current = await this.readKeychainIndex();
886
- if (!current.includes(profileKey)) {
887
- current.push(profileKey);
888
- await this.keychain.setSecret(
889
- KEYCHAIN_PROFILES_INDEX_ACCOUNT,
890
- JSON.stringify(current),
891
- "IWAS MCP Profiles Index"
892
- );
893
- }
894
- }
895
- async removeProfileFromKeychainIndex(profileKey) {
896
- const current = await this.readKeychainIndex();
897
- if (current.includes(profileKey)) {
898
- const updated = current.filter((k) => k !== profileKey);
899
- await this.keychain.setSecret(
900
- KEYCHAIN_PROFILES_INDEX_ACCOUNT,
901
- JSON.stringify(updated),
902
- "IWAS MCP Profiles Index"
903
- );
904
- }
905
- }
906
- // --- Internal File Storage Backend (dir 0700, file 0600, atomic replace) ---
907
- ensureConfigDirectory() {
908
- if (!import_node_fs.default.existsSync(this.configDir)) {
909
- import_node_fs.default.mkdirSync(this.configDir, { recursive: true, mode: 448 });
910
- }
911
- try {
912
- import_node_fs.default.chmodSync(this.configDir, 448);
913
- } catch {
914
- }
915
- }
916
- readAllFromFile() {
917
- if (!import_node_fs.default.existsSync(this.credentialsFilePath)) {
918
- return {};
919
- }
920
- if (process.platform !== "win32") {
921
- try {
922
- import_node_fs.default.chmodSync(this.credentialsFilePath, 384);
923
- } catch {
924
- }
925
- }
926
- const content = import_node_fs.default.readFileSync(this.credentialsFilePath, "utf8").trim();
927
- if (!content) {
928
- return {};
929
- }
930
- try {
931
- const parsed = JSON.parse(content);
932
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
933
- throw new Error("Root credentials object must be a JSON record");
934
- }
935
- for (const [key, item] of Object.entries(parsed)) {
936
- if (!isValidStoredCredentials(item)) {
937
- throw new Error(`Profile "${key}" contains invalid or missing credential fields`);
938
- }
939
- }
940
- return parsed;
941
- } catch (err) {
942
- const message = err instanceof Error ? err.message : String(err);
943
- throw new Error(
944
- `Corrupt credentials file at ${this.credentialsFilePath}: ${message}. Will not overwrite.`
945
- );
946
- }
947
- }
948
- writeAllToFile(data) {
949
- this.ensureConfigDirectory();
950
- const content = JSON.stringify(data, null, 2) + "\n";
951
- const tmpFilePath = import_node_path.default.join(
952
- this.configDir,
953
- `.credentials.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`
954
- );
955
- let fd = null;
956
- try {
957
- fd = import_node_fs.default.openSync(tmpFilePath, "w", 384);
958
- import_node_fs.default.writeSync(fd, content, void 0, "utf8");
959
- import_node_fs.default.fsyncSync(fd);
960
- import_node_fs.default.closeSync(fd);
961
- fd = null;
962
- try {
963
- import_node_fs.default.chmodSync(tmpFilePath, 384);
964
- } catch {
965
- }
966
- import_node_fs.default.renameSync(tmpFilePath, this.credentialsFilePath);
967
- try {
968
- import_node_fs.default.chmodSync(this.credentialsFilePath, 384);
969
- } catch {
970
- }
971
- if (process.platform !== "win32") {
972
- try {
973
- const dirFd = import_node_fs.default.openSync(this.configDir, "r");
974
- import_node_fs.default.fsyncSync(dirFd);
975
- import_node_fs.default.closeSync(dirFd);
976
- } catch {
977
- }
978
- }
979
- } catch (err) {
980
- if (fd !== null) {
981
- try {
982
- import_node_fs.default.closeSync(fd);
983
- } catch {
984
- }
985
- }
986
- if (import_node_fs.default.existsSync(tmpFilePath)) {
987
- try {
988
- import_node_fs.default.unlinkSync(tmpFilePath);
989
- } catch {
990
- }
991
- }
992
- throw err;
993
- }
994
- }
995
- loadFromFile(profileKey) {
996
- const all = this.readAllFromFile();
997
- return all[profileKey] ?? null;
998
- }
999
- saveToFile(profileKey, credentials) {
1000
- const all = this.readAllFromFile();
1001
- all[profileKey] = credentials;
1002
- this.writeAllToFile(all);
1003
- }
1004
- deleteFromFile(profileKey) {
1005
- if (!import_node_fs.default.existsSync(this.credentialsFilePath)) {
1006
- return;
1007
- }
1008
- const all = this.readAllFromFile();
1009
- if (profileKey in all) {
1010
- delete all[profileKey];
1011
- this.writeAllToFile(all);
1012
- }
1013
- }
1014
- listProfilesFromFile() {
1015
- if (!import_node_fs.default.existsSync(this.credentialsFilePath)) {
1016
- return [];
1017
- }
1018
- const all = this.readAllFromFile();
1019
- return Object.keys(all);
1020
- }
1021
- };
1022
-
1023
- // src/auth/http.ts
1024
- var HttpError = class extends Error {
1025
- status;
1026
- data;
1027
- constructor(message, status, data) {
1028
- super(message);
1029
- this.name = "HttpError";
1030
- this.status = status;
1031
- this.data = data;
1032
- }
1033
- };
1034
- var DEFAULT_TIMEOUT_MS = 15e3;
1035
- var DEFAULT_RETRIES = 2;
1036
- async function sendRequest(url, options) {
1037
- const method = options?.method || "GET";
1038
- const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1039
- const maxRetries = options?.retries ?? DEFAULT_RETRIES;
1040
- const headers = {
1041
- Accept: "application/json",
1042
- ...options?.headers || {}
1043
- };
1044
- let serializedBody;
1045
- if (options?.body) {
1046
- if (typeof options.body === "string") {
1047
- serializedBody = options.body;
1048
- } else if (options.body instanceof URLSearchParams) {
1049
- serializedBody = options.body.toString();
1050
- if (!headers["Content-Type"]) {
1051
- headers["Content-Type"] = "application/x-www-form-urlencoded";
1052
- }
1053
- } else {
1054
- serializedBody = JSON.stringify(options.body);
1055
- if (!headers["Content-Type"]) {
1056
- headers["Content-Type"] = "application/json";
1057
- }
1058
- }
1059
- }
1060
- let attempt = 0;
1061
- let lastError = null;
1062
- while (attempt <= maxRetries) {
1063
- attempt++;
1064
- const controller = new AbortController();
1065
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1066
- try {
1067
- const fetchImpl = options?.fetchFn || fetch;
1068
- const res = await fetchImpl(url, {
1069
- method,
1070
- headers,
1071
- body: serializedBody,
1072
- signal: controller.signal
1073
- });
1074
- clearTimeout(timer);
1075
- const contentType = res.headers.get("content-type") || "";
1076
- let parsedData;
1077
- if (contentType.includes("application/json")) {
1078
- try {
1079
- parsedData = await res.json();
1080
- } catch {
1081
- parsedData = null;
1082
- }
1083
- } else {
1084
- parsedData = await res.text();
1085
- }
1086
- if (res.status >= 500 && res.status <= 504 && attempt <= maxRetries) {
1087
- const backoffMs = Math.min(1e3 * Math.pow(2, attempt - 1) + Math.random() * 200, 5e3);
1088
- await new Promise((resolve) => setTimeout(resolve, backoffMs));
1089
- continue;
1090
- }
1091
- return {
1092
- status: res.status,
1093
- ok: res.ok,
1094
- headers: res.headers,
1095
- data: parsedData
1096
- };
1097
- } catch (err) {
1098
- clearTimeout(timer);
1099
- const isAbort = err instanceof Error && err.name === "AbortError";
1100
- const errMsg = isAbort ? `Request timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : String(err);
1101
- lastError = new Error(`Network request failed for ${method} ${url}: ${errMsg}`);
1102
- if (attempt <= maxRetries) {
1103
- const backoffMs = Math.min(500 * Math.pow(2, attempt - 1) + Math.random() * 200, 3e3);
1104
- await new Promise((resolve) => setTimeout(resolve, backoffMs));
1105
- continue;
1106
- }
1107
- }
1108
- }
1109
- throw lastError || new Error(`Network request failed after ${attempt} attempts`);
1110
- }
1111
-
1112
- // src/auth/device-flow.ts
1113
- var import_node_child_process2 = require("child_process");
1114
- var import_permissions = __toESM(require_permissions());
1115
-
1116
- // src/auth/logout.ts
1117
- async function executeLogout(options) {
1118
- const log = options?.logger || ((msg) => process.stdout.write(`${msg}
1119
- `));
1120
- const errLog = options?.errorLogger || ((msg) => process.stderr.write(`${msg}
1121
- `));
1122
- const serverUrl = resolveAuthServerUrl(options?.server);
1123
- const clientId = (options?.clientId || DEFAULT_CLI_CLIENT_ID).trim();
1124
- const profileKey = normalizeProfileKey(serverUrl, clientId);
1125
- const vault = options?.vault || new CredentialVault();
1126
- const force = options?.force === true;
1127
- const credentials = await vault.load(profileKey);
1128
- if (!credentials) {
1129
- const result2 = {
1130
- success: true,
1131
- profileKey,
1132
- serverRevoked: false,
1133
- refreshTokenRevoked: false,
1134
- localCleared: false,
1135
- message: `No stored credentials found for profile: ${profileKey}`
1136
- };
1137
- if (options?.json) {
1138
- log(
1139
- JSON.stringify({
1140
- event: "logout",
1141
- success: true,
1142
- loggedOut: true,
1143
- profileKey,
1144
- serverRevoked: false,
1145
- refreshTokenRevoked: false,
1146
- localCleared: false,
1147
- message: result2.message
1148
- })
1149
- );
1150
- } else {
1151
- log(`\u2139\uFE0F No stored credentials found for profile: ${profileKey}`);
1152
- }
1153
- return result2;
1154
- }
1155
- let serverRevoked = false;
1156
- let serverError = null;
1157
- const sessionLogoutUrl = `${serverUrl.replace(/\/+$/, "")}/api/v1/mcp-session/logout`;
1158
- try {
1159
- const authScheme = credentials.tokenType || "Bearer";
1160
- const res = await sendRequest(sessionLogoutUrl, {
1161
- method: "POST",
1162
- headers: {
1163
- Authorization: `${authScheme} ${credentials.accessToken}`
1164
- },
1165
- fetchFn: options?.fetchFn
1166
- });
1167
- if (res.status === 204 || res.status === 200) {
1168
- serverRevoked = true;
1169
- } else if (res.status === 401 || res.status === 403) {
1170
- serverRevoked = true;
1171
- } else if (res.status === 400) {
1172
- const data = res.data;
1173
- throw new Error(data?.message || "Server rejected logout request (HTTP 400).");
1174
- } else {
1175
- throw new HttpError(
1176
- `Server returned status ${res.status} during logout`,
1177
- res.status,
1178
- res.data
1179
- );
1180
- }
1181
- } catch (err) {
1182
- serverError = err instanceof Error ? err : new Error(String(err));
1183
- }
1184
- if (serverError && !force) {
1185
- const errorMsg = `Server revocation failed: ${serverError.message}. Local credentials preserved. Use --force to clear local credentials anyway if the server is permanently offline.`;
1186
- throw new Error(errorMsg);
1187
- }
1188
- let refreshTokenRevoked = false;
1189
- if (credentials.refreshToken) {
1190
- try {
1191
- const revokeUrl = `${serverUrl.replace(/\/+$/, "")}/api/auth/oauth2/revoke`;
1192
- const body = new URLSearchParams({
1193
- token: credentials.refreshToken,
1194
- token_type_hint: "refresh_token",
1195
- client_id: clientId
1196
- });
1197
- const res = await sendRequest(revokeUrl, {
1198
- method: "POST",
1199
- body,
1200
- fetchFn: options?.fetchFn
1201
- });
1202
- if (res.ok || res.status === 200 || res.status === 204 || res.status === 401 || res.status === 400) {
1203
- refreshTokenRevoked = true;
1204
- }
1205
- } catch {
1206
- }
1207
- }
1208
- await vault.delete(profileKey);
1209
- const result = {
1210
- success: true,
1211
- profileKey,
1212
- serverRevoked,
1213
- refreshTokenRevoked,
1214
- localCleared: true,
1215
- message: serverError ? `Local credentials cleared with --force (server unreachable: ${serverError.message})` : "Successfully logged out and revoked all grants server-side"
1216
- };
1217
- if (options?.json) {
1218
- log(
1219
- JSON.stringify({
1220
- event: "logout",
1221
- success: true,
1222
- loggedOut: true,
1223
- profileKey,
1224
- serverRevoked,
1225
- refreshTokenRevoked,
1226
- localCleared: true,
1227
- forced: force && !!serverError
1228
- })
1229
- );
1230
- } else {
1231
- if (serverError) {
1232
- errLog(`\u26A0\uFE0F Warning: Server revocation failed (${serverError.message})`);
1233
- log(`\u2705 Local credentials cleared for profile: ${profileKey} (--force)
1234
- `);
1235
- } else {
1236
- log(`
1237
- \u2705 Successfully logged out from profile: ${profileKey}`);
1238
- log(" \u2022 Server session & OAuth grants: Revoked");
1239
- log(" \u2022 Local credentials: Removed\n");
1240
- }
1241
- }
1242
- return result;
1243
- }
1244
-
1245
- // src/auth/device-flow.ts
1246
- var DEFAULT_CLI_CLIENT_ID = "iwas-cli";
1247
- var DEFAULT_SERVER_URL = "https://getiwas.com";
1248
- var DEFAULT_READ_ONLY_SCOPES = import_permissions.MCP_ELIGIBLE_PERMISSIONS.filter(
1249
- (perm) => perm.endsWith(":read")
1250
- );
1251
- var DEFAULT_SCOPES = [
1252
- ...DEFAULT_READ_ONLY_SCOPES,
1253
- import_permissions.OFFLINE_ACCESS_SCOPE
1254
- ].join(" ");
1255
- var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
1256
- function validateScopes(scopesString) {
1257
- const requested = scopesString.split(/\s+/).filter(Boolean);
1258
- if (requested.length === 0) {
1259
- return {
1260
- valid: false,
1261
- invalidScopes: [],
1262
- error: "At least one OAuth scope must be requested."
1263
- };
1264
- }
1265
- const allowedSet = new Set(import_permissions.MCP_OAUTH_SCOPES);
1266
- const invalidScopes = requested.filter((s) => !allowedSet.has(s));
1267
- if (invalidScopes.length > 0) {
1268
- return {
1269
- valid: false,
1270
- invalidScopes,
1271
- error: `Invalid OAuth scope(s) requested: [${invalidScopes.join(", ")}]. Supported scopes are: [${import_permissions.MCP_OAUTH_SCOPES.join(", ")}]`
1272
- };
1273
- }
1274
- const capabilityScopes = requested.filter((s) => s !== import_permissions.OFFLINE_ACCESS_SCOPE);
1275
- if (capabilityScopes.length === 0) {
1276
- return {
1277
- valid: false,
1278
- invalidScopes: [],
1279
- error: "At least one MCP capability scope besides offline_access must be requested."
1280
- };
1281
- }
1282
- return { valid: true, invalidScopes: [] };
1283
- }
1284
- function isLocalhost(hostname) {
1285
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1286
- }
1287
- function resolveAuthServerUrl(input) {
1288
- let urlStr = (input || process.env.IWAS_SERVER_URL || DEFAULT_SERVER_URL).trim();
1289
- urlStr = urlStr.replace(/\/+$/, "");
1290
- if (urlStr.toLowerCase().endsWith("/mcp")) {
1291
- throw new Error(
1292
- `Invalid --server value: "${urlStr}". The server argument must be the auth/web origin (e.g. "https://getiwas.com"), not the MCP resource endpoint. Use --resource for "${urlStr}".`
1293
- );
1294
- }
1295
- if (!/^https?:\/\//i.test(urlStr)) {
1296
- urlStr = `https://${urlStr}`;
1297
- }
1298
- let parsed;
1299
- try {
1300
- parsed = new URL(urlStr);
1301
- } catch {
1302
- throw new Error(`Invalid server URL: "${urlStr}"`);
1303
- }
1304
- if (parsed.pathname && parsed.pathname !== "/") {
1305
- const originStr = parsed.origin;
1306
- const label = input ? `--server value: "${input}"` : `IWAS_SERVER_URL: "${urlStr}"`;
1307
- throw new Error(
1308
- `Invalid ${label}. The server argument must be an origin without a path component (e.g. "${originStr}").`
1309
- );
1310
- }
1311
- if (!isLocalhost(parsed.hostname) && parsed.protocol !== "https:") {
1312
- throw new Error(
1313
- `Insecure server protocol: "${urlStr}". Non-localhost servers require HTTPS per OAuth 2.1 specifications.`
1314
- );
1315
- }
1316
- return parsed.origin.toLowerCase();
1317
- }
1318
- function resolveMcpResourceUrl(serverUrl, input) {
1319
- if (input && input.trim()) {
1320
- let clean = input.trim();
1321
- if (!/^https?:\/\//i.test(clean)) {
1322
- const isLoopbackCandidate = clean.startsWith("localhost") || clean.startsWith("127.0.0.1") || clean.startsWith("[::1]") || clean.startsWith("::1");
1323
- clean = isLoopbackCandidate ? `http://${clean}` : `https://${clean}`;
1324
- }
1325
- let parsed;
1326
- try {
1327
- parsed = new URL(clean);
1328
- } catch {
1329
- throw new Error(`Invalid resource URL: "${clean}"`);
1330
- }
1331
- if (!isLocalhost(parsed.hostname) && parsed.protocol !== "https:") {
1332
- throw new Error(
1333
- `Insecure resource protocol: "${clean}". Non-localhost resource endpoints require HTTPS.`
1334
- );
1335
- }
1336
- return parsed.href.replace(/\/+$/, "");
1337
- }
1338
- return `${serverUrl}/mcp`;
1339
- }
1340
- function decodeJwtPayload(token) {
1341
- const parts = token.split(".");
1342
- if (parts.length !== 3) {
1343
- return {};
1344
- }
1345
- try {
1346
- const jsonStr = Buffer.from(parts[1], "base64url").toString("utf8");
1347
- const parsed = JSON.parse(jsonStr);
1348
- return typeof parsed === "object" && parsed !== null ? parsed : {};
1349
- } catch {
1350
- return {};
1351
- }
1352
- }
1353
- function openBrowser(url, spawnFn = import_node_child_process2.spawn, onWarning) {
1354
- try {
1355
- let parsed;
1356
- try {
1357
- parsed = new URL(url);
1358
- } catch {
1359
- onWarning?.(`Invalid verification URL: "${url}". Please open manually.`);
1360
- return false;
1361
- }
1362
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1363
- onWarning?.(`Insecure verification URL protocol: "${parsed.protocol}". Expected http: or https:.`);
1364
- return false;
1365
- }
1366
- const platform = process.platform;
1367
- let child;
1368
- if (platform === "darwin") {
1369
- child = spawnFn("open", [parsed.href], { detached: true, stdio: "ignore" });
1370
- } else if (platform === "win32") {
1371
- child = spawnFn("explorer.exe", [parsed.href], { detached: true, stdio: "ignore" });
1372
- } else {
1373
- child = spawnFn("xdg-open", [parsed.href], { detached: true, stdio: "ignore" });
1374
- }
1375
- child.on("error", (err) => {
1376
- const msg = err instanceof Error ? err.message : String(err);
1377
- onWarning?.(`Could not launch browser automatically (${msg}). Please open the verification URL manually.`);
1378
- });
1379
- child.unref();
1380
- return true;
1381
- } catch (err) {
1382
- const msg = err instanceof Error ? err.message : String(err);
1383
- onWarning?.(`Could not launch browser (${msg}). Please open the verification URL manually.`);
1384
- return false;
1385
- }
1386
- }
1387
- async function requestDeviceCode(serverUrl, params) {
1388
- const endpoint = `${serverUrl}/api/auth/device/code`;
1389
- const res = await sendRequest(endpoint, {
1390
- method: "POST",
1391
- body: {
1392
- client_id: params.clientId,
1393
- scope: params.scope,
1394
- resource: params.resource
1395
- },
1396
- retries: 0
1397
- // Zero retries to guarantee single-use device_code creation
1398
- });
1399
- if (!res.ok || !res.data || typeof res.data !== "object") {
1400
- throw new HttpError(
1401
- `Failed to initiate device authorization with ${endpoint}: HTTP ${res.status}`,
1402
- res.status,
1403
- res.data
1404
- );
1405
- }
1406
- const data = res.data;
1407
- if (!data.device_code || !data.user_code || !data.verification_uri) {
1408
- throw new Error(
1409
- `Malformed device code response from ${endpoint}: missing required RFC 8628 fields`
1410
- );
1411
- }
1412
- return {
1413
- device_code: data.device_code,
1414
- user_code: data.user_code,
1415
- verification_uri: data.verification_uri,
1416
- verification_uri_complete: data.verification_uri_complete || `${data.verification_uri}?user_code=${encodeURIComponent(data.user_code)}`,
1417
- expires_in: data.expires_in || 600,
1418
- interval: data.interval || 5
1419
- };
1420
- }
1421
- async function pollDeviceToken(serverUrl, params, onPollTick) {
1422
- const tokenEndpoint = `${serverUrl}/api/auth/oauth2/token`;
1423
- let intervalSec = Math.max(params.initialInterval || 5, 1);
1424
- const timeoutMs = params.timeoutMs || 3e5;
1425
- const startTime = Date.now();
1426
- while (Date.now() - startTime < timeoutMs) {
1427
- await new Promise((resolve) => setTimeout(resolve, intervalSec * 1e3));
1428
- if (Date.now() - startTime >= timeoutMs) {
1429
- break;
1430
- }
1431
- onPollTick?.("polling");
1432
- const formBody = new URLSearchParams({
1433
- grant_type: DEVICE_CODE_GRANT_TYPE,
1434
- client_id: params.clientId,
1435
- device_code: params.deviceCode,
1436
- resource: params.resource
1437
- });
1438
- const res = await sendRequest(
1439
- tokenEndpoint,
1440
- {
1441
- method: "POST",
1442
- body: formBody,
1443
- retries: 0
1444
- // Zero retries to guarantee single-use device_code is never double-consumed
1445
- }
1446
- );
1447
- if (res.status === 200) {
1448
- const tokenData = res.data;
1449
- if (!tokenData.access_token) {
1450
- throw new Error("Token endpoint returned HTTP 200 without access_token");
1451
- }
1452
- return tokenData;
1453
- }
1454
- if (res.status === 429) {
1455
- const retryAfterHeader = res.headers?.get?.("retry-after");
1456
- let retrySeconds = intervalSec + 5;
1457
- if (retryAfterHeader) {
1458
- const parsedSec = parseInt(retryAfterHeader, 10);
1459
- if (!isNaN(parsedSec) && parsedSec > 0) {
1460
- retrySeconds = parsedSec;
1461
- } else {
1462
- const parsedDate = Date.parse(retryAfterHeader);
1463
- if (!isNaN(parsedDate)) {
1464
- const diffSec = Math.ceil((parsedDate - Date.now()) / 1e3);
1465
- if (diffSec > 0) {
1466
- retrySeconds = diffSec;
1467
- }
1468
- }
1469
- }
1470
- }
1471
- const remainingMs = Math.max(0, timeoutMs - (Date.now() - startTime));
1472
- const remainingSec = Math.ceil(remainingMs / 1e3);
1473
- intervalSec = remainingSec > 0 ? Math.min(retrySeconds, 60, remainingSec) : 0;
1474
- onPollTick?.(`slow_down (rate_limited, retry-after: ${intervalSec}s)`);
1475
- continue;
1476
- }
1477
- const errorData = res.data || {};
1478
- const errorCode = errorData.error || "unknown_error";
1479
- const errorDesc = errorData.error_description || errorCode;
1480
- switch (errorCode) {
1481
- case "authorization_pending":
1482
- continue;
1483
- case "slow_down":
1484
- intervalSec += 5;
1485
- onPollTick?.(`slow_down (interval: ${intervalSec}s)`);
1486
- continue;
1487
- case "expired_token":
1488
- throw new Error(
1489
- "Device code has expired. Please run `iwas login` again to obtain a new code."
1490
- );
1491
- case "access_denied":
1492
- throw new Error("Authorization was denied by the user.");
1493
- case "invalid_grant":
1494
- throw new Error(
1495
- `Invalid grant: ${errorDesc}. The device code may have already been used or revoked.`
1496
- );
1497
- case "invalid_client":
1498
- throw new Error(`Invalid OAuth client: ${errorDesc}. Check your --client-id.`);
1499
- default:
1500
- if (res.status >= 500) {
1501
- intervalSec = Math.min(intervalSec + 2, 30);
1502
- continue;
1503
- }
1504
- throw new Error(`Device authorization failed: ${errorDesc} (HTTP ${res.status})`);
1505
- }
1506
- }
1507
- throw new Error("Device login timed out waiting for user authorization.");
1508
- }
1509
- async function executeLogin(options) {
1510
- const log = options?.logger || ((msg) => process.stdout.write(`${msg}
1511
- `));
1512
- const errLog = options?.errorLogger || ((msg) => process.stderr.write(`${msg}
1513
- `));
1514
- const serverUrl = resolveAuthServerUrl(options?.server);
1515
- const resourceUrl = resolveMcpResourceUrl(serverUrl, options?.resource);
1516
- const clientId = (options?.clientId || DEFAULT_CLI_CLIENT_ID).trim();
1517
- const scopes = (options?.scopes || DEFAULT_SCOPES).trim();
1518
- const vault = options?.vault || new CredentialVault();
1519
- const scopeValidation = validateScopes(scopes);
1520
- if (!scopeValidation.valid) {
1521
- throw new Error(scopeValidation.error || "Invalid OAuth scopes requested.");
1522
- }
1523
- const authResponse = await requestDeviceCode(serverUrl, {
1524
- clientId,
1525
- scope: scopes,
1526
- resource: resourceUrl
1527
- });
1528
- if (options?.json) {
1529
- const initEvent = {
1530
- event: "device_authorization",
1531
- user_code: authResponse.user_code,
1532
- verification_uri: authResponse.verification_uri,
1533
- verification_uri_complete: authResponse.verification_uri_complete,
1534
- expires_in: authResponse.expires_in,
1535
- interval: authResponse.interval
1536
- };
1537
- log(JSON.stringify(initEvent));
1538
- errLog(`
1539
- \u{1F449} USER CODE: ${authResponse.user_code}`);
1540
- errLog(`\u{1F517} To verify, open: ${authResponse.verification_uri_complete}
1541
- `);
1542
- } else {
1543
- log("\n============================================================");
1544
- log(" IWAS MCP Device Authorization ");
1545
- log("============================================================\n");
1546
- log(`First, enter this code on the verification page:`);
1547
- log(`
1548
- \u{1F449} USER CODE: \x1B[1m\x1B[32m${authResponse.user_code}\x1B[0m
1549
- `);
1550
- log(`To verify, open:`);
1551
- log(` \u{1F517} ${authResponse.verification_uri_complete}
1552
- `);
1553
- log("Waiting for approval in browser...");
1554
- }
1555
- const spawnFn = options?.spawnFn || import_node_child_process2.spawn;
1556
- const opener = options?.browserOpener || ((url) => openBrowser(url, spawnFn, (warning) => {
1557
- if (!options?.json) {
1558
- log(`\u26A0\uFE0F ${warning}`);
1559
- } else {
1560
- errLog(`\u26A0\uFE0F ${warning}`);
1561
- }
1562
- }));
1563
- if (!options?.noBrowser) {
1564
- opener(authResponse.verification_uri_complete);
1565
- }
1566
- const pollTimeoutMs = options?.pollTimeoutMs ?? authResponse.expires_in * 1e3;
1567
- const tokenResponse = await pollDeviceToken(
1568
- serverUrl,
1569
- {
1570
- clientId,
1571
- deviceCode: authResponse.device_code,
1572
- resource: resourceUrl,
1573
- initialInterval: authResponse.interval,
1574
- timeoutMs: pollTimeoutMs
1575
- },
1576
- (status) => {
1577
- if (!options?.json && status.startsWith("slow_down")) {
1578
- log(`Rate limit reached: slowing poll interval.`);
1579
- }
1580
- }
1581
- );
1582
- const payload = decodeJwtPayload(tokenResponse.access_token);
1583
- if (!payload || Object.keys(payload).length === 0) {
1584
- throw new Error(
1585
- "Server returned an opaque or non-JWT access token. Expected a JWT access token targeted for MCP resource."
1586
- );
1587
- }
1588
- const tokenAud = Array.isArray(payload.aud) ? payload.aud : typeof payload.aud === "string" ? [payload.aud] : [];
1589
- const normalizedResourceUrl = resourceUrl.replace(/\/+$/, "").toLowerCase();
1590
- const hasResourceAud = tokenAud.some((aud) => {
1591
- try {
1592
- return new URL(String(aud)).href.replace(/\/+$/, "").toLowerCase() === normalizedResourceUrl;
1593
- } catch {
1594
- return String(aud).toLowerCase() === normalizedResourceUrl;
1595
- }
1596
- });
1597
- if (!hasResourceAud) {
1598
- throw new Error(
1599
- `Access token audience [${tokenAud.join(", ")}] does not match MCP resource URL "${resourceUrl}".`
1600
- );
1601
- }
1602
- const grantedScopesStr = typeof tokenResponse.scope === "string" && tokenResponse.scope || typeof payload.scope === "string" && payload.scope || "";
1603
- const grantedList = grantedScopesStr.split(/\s+/).filter(Boolean);
1604
- const requestedList = scopes.split(/\s+/).filter(Boolean);
1605
- const matchedScopes = requestedList.filter((s) => grantedList.includes(s));
1606
- const matchedCapabilities = matchedScopes.filter((s) => s !== import_permissions.OFFLINE_ACCESS_SCOPE);
1607
- if (matchedCapabilities.length === 0) {
1608
- throw new Error(
1609
- `Access token lacks granted capability scopes (requested: [${requestedList.join(", ")}], granted: [${grantedList.join(", ")}]). At least one permission scope besides offline_access must be granted.`
1610
- );
1611
- }
1612
- const organizationId = typeof payload.organizationId === "string" ? payload.organizationId : null;
1613
- const expiresAtMs = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : typeof payload.exp === "number" ? payload.exp * 1e3 : Date.now() + 36e5;
1614
- const credentials = {
1615
- serverUrl,
1616
- clientId,
1617
- accessToken: tokenResponse.access_token,
1618
- refreshToken: tokenResponse.refresh_token,
1619
- tokenType: tokenResponse.token_type || "Bearer",
1620
- scope: tokenResponse.scope || scopes,
1621
- resource: resourceUrl,
1622
- organizationId,
1623
- expiresAt: expiresAtMs,
1624
- updatedAt: Date.now()
1625
- };
1626
- const profileKey = normalizeProfileKey(serverUrl, clientId);
1627
- await vault.save(profileKey, credentials);
1628
- const result = {
1629
- profileKey,
1630
- serverUrl,
1631
- clientId,
1632
- organizationId,
1633
- scope: credentials.scope,
1634
- resource: credentials.resource,
1635
- expiresAt: expiresAtMs
1636
- };
1637
- if (options?.json) {
1638
- const successEvent = {
1639
- event: "login_success",
1640
- success: true,
1641
- ...result
1642
- };
1643
- log(JSON.stringify(successEvent));
1644
- } else {
1645
- log("\n\u2705 Login successful!");
1646
- log(`- Profile: ${profileKey}`);
1647
- log(`- Server: ${serverUrl}`);
1648
- log(`- Client ID: ${clientId}`);
1649
- log(`- Organization: ${organizationId || "(none / platform scope)"}`);
1650
- log(`- Scopes: ${credentials.scope || "none"}`);
1651
- log(`- Resource: ${resourceUrl}`);
1652
- log(`- Expires: ${new Date(expiresAtMs).toISOString()}
1653
- `);
1654
- }
1655
- return result;
1656
- }
1657
- async function executeStatus(options) {
1658
- const log = options?.logger || ((msg) => process.stdout.write(`${msg}
1659
- `));
1660
- const serverUrl = resolveAuthServerUrl(options?.server);
1661
- const clientId = (options?.clientId || DEFAULT_CLI_CLIENT_ID).trim();
1662
- const profileKey = normalizeProfileKey(serverUrl, clientId);
1663
- const vault = options?.vault || new CredentialVault();
1664
- const creds = await vault.load(profileKey);
1665
- if (!creds) {
1666
- if (options?.json) {
1667
- log(JSON.stringify({ authenticated: false, profileKey, serverUrl, clientId }));
1668
- } else {
1669
- log(`No active session found for profile: ${profileKey}`);
1670
- log(`Run \`iwas login --server ${serverUrl}\` to authenticate.
1671
- `);
1672
- }
1673
- return;
1674
- }
1675
- const isExpired = isCredentialExpired(creds, 60);
1676
- const statusData = {
1677
- authenticated: !isExpired,
1678
- expired: isExpired,
1679
- profileKey,
1680
- serverUrl: creds.serverUrl,
1681
- clientId: creds.clientId,
1682
- organizationId: creds.organizationId,
1683
- scope: creds.scope,
1684
- resource: creds.resource,
1685
- expiresAt: new Date(creds.expiresAt).toISOString(),
1686
- expiresInSeconds: Math.max(0, Math.round((creds.expiresAt - Date.now()) / 1e3))
1687
- };
1688
- if (options?.json) {
1689
- log(JSON.stringify(statusData));
1690
- } else {
1691
- log("\n============================================================");
1692
- log(" IWAS MCP Auth Status ");
1693
- log("============================================================\n");
1694
- log(`- Profile: ${profileKey}`);
1695
- log(`- Server: ${creds.serverUrl}`);
1696
- log(`- Client ID: ${creds.clientId}`);
1697
- log(`- Status: ${isExpired ? "\u{1F534} Expired" : "\u{1F7E2} Active"}`);
1698
- log(`- Organization: ${creds.organizationId || "(none / platform scope)"}`);
1699
- log(`- Scopes: ${creds.scope || "none"}`);
1700
- if (creds.resource) {
1701
- log(`- Resource: ${creds.resource}`);
1702
- }
1703
- log(`- Expires: ${statusData.expiresAt} (${statusData.expiresInSeconds}s remaining)
1704
- `);
1705
- }
1706
- }
1707
-
1708
- // src/auth/vault-lock.ts
1709
- var import_node_fs2 = __toESM(require("fs"));
1710
- var import_node_path2 = __toESM(require("path"));
1711
- var import_node_os = __toESM(require("os"));
1712
- var import_node_crypto = require("crypto");
1713
- var VaultLockError = class extends Error {
1714
- constructor(message) {
1715
- super(message);
1716
- this.name = "VaultLockError";
1717
- }
1718
- };
1719
- var VaultLockTimeoutError = class extends VaultLockError {
1720
- lockPath;
1721
- timeoutMs;
1722
- constructor(lockPath, timeoutMs) {
1723
- super(`Timed out waiting to acquire credentials lock at "${lockPath}" after ${timeoutMs}ms.`);
1724
- this.name = "VaultLockTimeoutError";
1725
- this.lockPath = lockPath;
1726
- this.timeoutMs = timeoutMs;
1727
- }
1728
- };
1729
- var DEFAULT_TIMEOUT_MS2 = 6e4;
1730
- var DEFAULT_STALE_TIMEOUT_MS = 6e4;
1731
- var DEFAULT_RETRY_INTERVAL_MS = 50;
1732
- function defaultIsProcessAlive(pid) {
1733
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
1734
- return false;
1735
- }
1736
- try {
1737
- process.kill(pid, 0);
1738
- return true;
1739
- } catch (err) {
1740
- const code = err?.code;
1741
- if (code === "ESRCH") {
1742
- return false;
1743
- }
1744
- if (code === "EPERM") {
1745
- return true;
1746
- }
1747
- return false;
1748
- }
1749
- }
1750
- var VaultLock = class {
1751
- lockPath;
1752
- isCustomLockPath;
1753
- timeoutMs;
1754
- staleTimeoutMs;
1755
- corruptLockGraceMs;
1756
- retryIntervalMs;
1757
- isProcessAlive;
1758
- currentLockId = null;
1759
- currentLockIno;
1760
- constructor(options) {
1761
- if (options?.lockPath) {
1762
- this.lockPath = options.lockPath;
1763
- this.isCustomLockPath = true;
1764
- } else {
1765
- const baseConfigDir = options?.configDir || process.env.XDG_CONFIG_HOME || import_node_path2.default.join(import_node_os.default.homedir(), ".config");
1766
- const dir = options?.configDir ? options.configDir : import_node_path2.default.join(baseConfigDir, "iwas");
1767
- this.lockPath = import_node_path2.default.join(dir, "credentials.lock");
1768
- this.isCustomLockPath = Boolean(options?.configDir);
1769
- }
1770
- this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1771
- this.staleTimeoutMs = options?.staleTimeoutMs ?? DEFAULT_STALE_TIMEOUT_MS;
1772
- this.corruptLockGraceMs = options?.corruptLockGraceMs ?? Math.min(this.staleTimeoutMs, 1e4);
1773
- this.retryIntervalMs = options?.retryIntervalMs ?? DEFAULT_RETRY_INTERVAL_MS;
1774
- this.isProcessAlive = options?.isProcessAliveFn ?? defaultIsProcessAlive;
1775
- }
1776
- /**
1777
- * Returns whether this instance currently holds the active lock.
1778
- */
1779
- isHeld() {
1780
- return this.currentLockId !== null;
1781
- }
1782
- /**
1783
- * Returns the current lock ID if held.
1784
- */
1785
- getCurrentLockId() {
1786
- return this.currentLockId;
1787
- }
1788
- /**
1789
- * Acquires the lock exclusively.
1790
- * Retries with bounded exponential backoff and jitter if held by another process.
1791
- * Recovers automatically if the lock is determined to be stale.
1792
- */
1793
- async acquire() {
1794
- if (this.currentLockId) {
1795
- return;
1796
- }
1797
- const parentDir = import_node_path2.default.dirname(this.lockPath);
1798
- let createdParent = false;
1799
- if (!import_node_fs2.default.existsSync(parentDir)) {
1800
- try {
1801
- import_node_fs2.default.mkdirSync(parentDir, { recursive: true, mode: 448 });
1802
- createdParent = true;
1803
- } catch {
1804
- }
1805
- }
1806
- if (!this.isCustomLockPath || createdParent) {
1807
- try {
1808
- import_node_fs2.default.chmodSync(parentDir, 448);
1809
- } catch {
1810
- }
1811
- }
1812
- const startTime = Date.now();
1813
- const lockId = (0, import_node_crypto.randomUUID)();
1814
- let currentDelay = this.retryIntervalMs;
1815
- while (true) {
1816
- let fd = null;
1817
- let acquiredIno;
1818
- try {
1819
- fd = import_node_fs2.default.openSync(this.lockPath, "wx", 384);
1820
- const stat = import_node_fs2.default.fstatSync(fd);
1821
- acquiredIno = stat.ino;
1822
- const lockData = {
1823
- pid: process.pid,
1824
- createdAt: Date.now(),
1825
- lockId,
1826
- hostname: import_node_os.default.hostname()
1827
- };
1828
- import_node_fs2.default.writeFileSync(fd, JSON.stringify(lockData), "utf8");
1829
- import_node_fs2.default.closeSync(fd);
1830
- fd = null;
1831
- this.currentLockId = lockId;
1832
- this.currentLockIno = acquiredIno;
1833
- return;
1834
- } catch (err) {
1835
- if (fd !== null) {
1836
- try {
1837
- import_node_fs2.default.closeSync(fd);
1838
- } catch {
1839
- }
1840
- try {
1841
- const curStat = import_node_fs2.default.statSync(this.lockPath);
1842
- if ((acquiredIno === void 0 || curStat.ino === acquiredIno) && curStat.size === 0) {
1843
- import_node_fs2.default.unlinkSync(this.lockPath);
1844
- }
1845
- } catch {
1846
- }
1847
- }
1848
- const code = err?.code;
1849
- if (code !== "EEXIST") {
1850
- throw err;
1851
- }
1852
- }
1853
- this.tryCleanStaleLock();
1854
- const elapsed = Date.now() - startTime;
1855
- if (elapsed >= this.timeoutMs) {
1856
- throw new VaultLockTimeoutError(this.lockPath, this.timeoutMs);
1857
- }
1858
- const jitter = Math.floor(Math.random() * 25);
1859
- const sleepMs = Math.min(currentDelay + jitter, 250);
1860
- currentDelay = Math.min(currentDelay * 1.5, 200);
1861
- await new Promise((resolve) => setTimeout(resolve, sleepMs));
1862
- }
1863
- }
1864
- /**
1865
- * Releases the lock file safely.
1866
- * Strictly enforces ownership and inode integrity: Never deletes a lock file owned by a different process.
1867
- */
1868
- async release() {
1869
- this.releaseSync();
1870
- }
1871
- /**
1872
- * Synchronous release helper for process exit handlers.
1873
- *
1874
- * Uses a hardlink guard so the inode we verified cannot be swapped for another
1875
- * process's lock between the check and the unlink. If hardlinks are unavailable,
1876
- * falls back to the stat-verified path (safe for the owner in practice).
1877
- */
1878
- releaseSync() {
1879
- if (!this.currentLockId) {
1880
- return;
1881
- }
1882
- const lockIdToRelease = this.currentLockId;
1883
- const lockInoToRelease = this.currentLockIno;
1884
- this.currentLockId = null;
1885
- this.currentLockIno = void 0;
1886
- const parentDir = import_node_path2.default.dirname(this.lockPath);
1887
- const guardPath = import_node_path2.default.join(parentDir, `.credentials.lock.release.${Date.now()}.${(0, import_node_crypto.randomUUID)()}`);
1888
- let guardCreated = false;
1889
- try {
1890
- if (!import_node_fs2.default.existsSync(this.lockPath)) {
1891
- return;
1892
- }
1893
- try {
1894
- import_node_fs2.default.linkSync(this.lockPath, guardPath);
1895
- guardCreated = true;
1896
- } catch {
1897
- this.releaseSyncLoose(lockIdToRelease, lockInoToRelease);
1898
- return;
1899
- }
1900
- const guardStat = import_node_fs2.default.statSync(guardPath);
1901
- if (lockInoToRelease !== void 0 && guardStat.ino !== lockInoToRelease) {
1902
- return;
1903
- }
1904
- const content = import_node_fs2.default.readFileSync(guardPath, "utf8");
1905
- let data = null;
1906
- try {
1907
- data = JSON.parse(content);
1908
- } catch {
1909
- return;
1910
- }
1911
- if (data && data.lockId === lockIdToRelease && data.pid === process.pid) {
1912
- const curStat = import_node_fs2.default.statSync(this.lockPath);
1913
- if (curStat.ino === guardStat.ino && curStat.nlink >= 2) {
1914
- try {
1915
- import_node_fs2.default.unlinkSync(this.lockPath);
1916
- } catch {
1917
- }
1918
- }
1919
- }
1920
- } catch {
1921
- } finally {
1922
- if (guardCreated) {
1923
- try {
1924
- import_node_fs2.default.unlinkSync(guardPath);
1925
- } catch {
1926
- }
1927
- }
1928
- }
1929
- }
1930
- /**
1931
- * Fallback release for filesystems without hardlink support.
1932
- *
1933
- * Hardlink creation can fail on filesystems lacking link support (e.g. FAT32, certain network mounts).
1934
- * In this fallback, the owner verifies that:
1935
- * 1. The lock file exists and matches the inode recorded at acquisition time.
1936
- * 2. The JSON payload matches lockIdToRelease and process.pid.
1937
- * 3. A post-read stat confirms the path still points to the same inode before unlinking.
1938
- *
1939
- * Note on TOCTOU: While a theoretical micro-window exists between post-read stat and unlinkSync,
1940
- * in practice only the legitimate owner releases an active, unexpired lock. If the lock was already
1941
- * unlinked or replaced by another inode, unlink is safely skipped.
1942
- */
1943
- releaseSyncLoose(lockIdToRelease, lockInoToRelease) {
1944
- try {
1945
- if (!import_node_fs2.default.existsSync(this.lockPath)) {
1946
- return;
1947
- }
1948
- const stat = import_node_fs2.default.statSync(this.lockPath);
1949
- if (lockInoToRelease !== void 0 && stat.ino !== lockInoToRelease) {
1950
- return;
1951
- }
1952
- const content = import_node_fs2.default.readFileSync(this.lockPath, "utf8");
1953
- let data = null;
1954
- try {
1955
- data = JSON.parse(content);
1956
- } catch {
1957
- return;
1958
- }
1959
- if (data && data.lockId === lockIdToRelease && data.pid === process.pid) {
1960
- const postStat = import_node_fs2.default.statSync(this.lockPath);
1961
- if (postStat.ino === stat.ino && postStat.mtimeMs === stat.mtimeMs) {
1962
- try {
1963
- import_node_fs2.default.unlinkSync(this.lockPath);
1964
- } catch {
1965
- }
1966
- }
1967
- }
1968
- } catch {
1969
- }
1970
- }
1971
- /**
1972
- * Sweeps orphan temporary guard files (.credentials.lock.release.*, .stale.*, .corrupt.*)
1973
- * left behind if a previous process crashed between linkSync and unlinkSync.
1974
- *
1975
- * Hardlink safety:
1976
- * 1. Hardlinks share `mtime` with the primary lock file. If a lock was held for > 60s,
1977
- * a newly created release/quarantine hardlink will have an old `mtime`.
1978
- * Therefore, age is strictly calculated from the timestamp embedded in the guard filename,
1979
- * NOT from `stat.mtimeMs`.
1980
- * 2. A guard still hardlinked to the current `lockPath` inode (nlink > 1 and
1981
- * the same inode) is active and is NEVER deleted.
1982
- * 3. Only timestamped-UUID guard names are considered. Guards from a replaced or
1983
- * removed lock (different inode / missing lockPath) are swept once their embedded
1984
- * timestamp exceeds `staleTimeoutMs`.
1985
- */
1986
- cleanOrphanGuards() {
1987
- try {
1988
- const parentDir = import_node_path2.default.dirname(this.lockPath);
1989
- if (!import_node_fs2.default.existsSync(parentDir)) {
1990
- return;
1991
- }
1992
- const lockFileName = import_node_path2.default.basename(this.lockPath);
1993
- const prefix = `.${lockFileName}.`;
1994
- const entries = import_node_fs2.default.readdirSync(parentDir);
1995
- const guardNamePattern = /^(release|stale|corrupt)\.(\d+)\.([0-9a-fA-F-]{36})$/;
1996
- let lockStat = null;
1997
- try {
1998
- lockStat = import_node_fs2.default.statSync(this.lockPath);
1999
- } catch {
2000
- }
2001
- const now = Date.now();
2002
- for (const entry of entries) {
2003
- if (!entry.startsWith(prefix)) {
2004
- continue;
2005
- }
2006
- const match = guardNamePattern.exec(entry.slice(prefix.length));
2007
- if (!match) {
2008
- continue;
2009
- }
2010
- const createdAt = Number(match[2]);
2011
- const fullPath = import_node_path2.default.join(parentDir, entry);
2012
- try {
2013
- const stat = import_node_fs2.default.statSync(fullPath);
2014
- if (!stat.isFile()) {
2015
- continue;
2016
- }
2017
- if (stat.nlink > 1 && lockStat && stat.ino === lockStat.ino) {
2018
- continue;
2019
- }
2020
- if (now - createdAt > this.staleTimeoutMs) {
2021
- import_node_fs2.default.unlinkSync(fullPath);
2022
- }
2023
- } catch {
2024
- }
2025
- }
2026
- } catch {
2027
- }
2028
- }
2029
- /**
2030
- * Executes an async operation within an exclusive vault lock.
2031
- * Guarantees that release() is called in finally.
2032
- */
2033
- async withLock(fn) {
2034
- await this.acquire();
2035
- try {
2036
- return await fn();
2037
- } finally {
2038
- await this.release();
2039
- }
2040
- }
2041
- /**
2042
- * Inspects existing lock file and safely reclaims it if stale.
2043
- *
2044
- * Hardened rules (M1 & TOCTOU fix):
2045
- * 1. Same-host lock: Only reclaimed if the holding process is confirmed DEAD (!isProcessAlive).
2046
- * If the process is ALIVE, the lock is NEVER deleted regardless of age.
2047
- * 2. Remote-host lock: Reclaimed only if age exceeds staleTimeoutMs (since cross-host PID is untestable).
2048
- * 3. TOCTOU prevention: Uses hardlink + nlink verification and non-overwriting semantics.
2049
- * Never replaces or destroys an active lock file acquired by a racing process.
2050
- */
2051
- tryCleanStaleLock() {
2052
- try {
2053
- this.cleanOrphanGuards();
2054
- if (!import_node_fs2.default.existsSync(this.lockPath)) {
2055
- return;
2056
- }
2057
- const initialStat = import_node_fs2.default.statSync(this.lockPath);
2058
- let data = null;
2059
- try {
2060
- const content = import_node_fs2.default.readFileSync(this.lockPath, "utf8");
2061
- data = JSON.parse(content);
2062
- } catch {
2063
- const ageMs = Date.now() - initialStat.mtimeMs;
2064
- if (ageMs > this.corruptLockGraceMs) {
2065
- this.reclaimCorruptLock(initialStat);
2066
- }
2067
- return;
2068
- }
2069
- if (!data || typeof data.createdAt !== "number") {
2070
- return;
2071
- }
2072
- const currentHostname = import_node_os.default.hostname();
2073
- const isSameHost = !data.hostname || data.hostname === currentHostname;
2074
- let isStale = false;
2075
- if (isSameHost) {
2076
- const isAlive = typeof data.pid === "number" && this.isProcessAlive(data.pid);
2077
- isStale = !isAlive;
2078
- } else {
2079
- const ageMs = Date.now() - data.createdAt;
2080
- isStale = ageMs > this.staleTimeoutMs;
2081
- }
2082
- if (isStale) {
2083
- this.reclaimStaleLock(data.lockId, initialStat);
2084
- }
2085
- } catch {
2086
- }
2087
- }
2088
- /**
2089
- * TOCTOU-safe stale lock removal using hardlink + nlink verification.
2090
- * Ensures that if another process acquires this.lockPath before or during cleanup,
2091
- * its active lock is never deleted or overwritten.
2092
- */
2093
- reclaimStaleLock(expectedLockId, initialStat) {
2094
- const parentDir = import_node_path2.default.dirname(this.lockPath);
2095
- const quarantinePath = import_node_path2.default.join(parentDir, `.credentials.lock.stale.${Date.now()}.${(0, import_node_crypto.randomUUID)()}`);
2096
- try {
2097
- import_node_fs2.default.linkSync(this.lockPath, quarantinePath);
2098
- try {
2099
- const qContent = import_node_fs2.default.readFileSync(quarantinePath, "utf8");
2100
- const qData = JSON.parse(qContent);
2101
- if (qData.lockId !== expectedLockId) {
2102
- return;
2103
- }
2104
- const curStat = import_node_fs2.default.statSync(this.lockPath);
2105
- if (curStat.ino === initialStat.ino && curStat.nlink >= 2) {
2106
- import_node_fs2.default.unlinkSync(this.lockPath);
2107
- }
2108
- } finally {
2109
- try {
2110
- import_node_fs2.default.unlinkSync(quarantinePath);
2111
- } catch {
2112
- }
2113
- }
2114
- } catch {
2115
- }
2116
- }
2117
- /**
2118
- * TOCTOU-safe corrupt lock removal with inode, size, and mtime verification.
2119
- */
2120
- reclaimCorruptLock(initialStat) {
2121
- const parentDir = import_node_path2.default.dirname(this.lockPath);
2122
- const quarantinePath = import_node_path2.default.join(parentDir, `.credentials.lock.corrupt.${Date.now()}.${(0, import_node_crypto.randomUUID)()}`);
2123
- try {
2124
- import_node_fs2.default.linkSync(this.lockPath, quarantinePath);
2125
- try {
2126
- const qStat = import_node_fs2.default.statSync(quarantinePath);
2127
- if (qStat.ino !== initialStat.ino || qStat.size !== initialStat.size || qStat.mtimeMs !== initialStat.mtimeMs) {
2128
- return;
2129
- }
2130
- try {
2131
- const qContent = import_node_fs2.default.readFileSync(quarantinePath, "utf8");
2132
- JSON.parse(qContent);
2133
- return;
2134
- } catch {
2135
- }
2136
- const curStat = import_node_fs2.default.statSync(this.lockPath);
2137
- if (curStat.ino === initialStat.ino && curStat.nlink >= 2) {
2138
- import_node_fs2.default.unlinkSync(this.lockPath);
2139
- }
2140
- } finally {
2141
- try {
2142
- import_node_fs2.default.unlinkSync(quarantinePath);
2143
- } catch {
2144
- }
2145
- }
2146
- } catch {
2147
- }
2148
- }
2149
- };
2150
- async function withVaultLock(lockPath, fn, options) {
2151
- const lock = new VaultLock({ lockPath, ...options });
2152
- return await lock.withLock(fn);
2153
- }
2154
-
2155
- // src/auth/token-refresh.ts
2156
- var import_node_path3 = __toESM(require("path"));
2157
- var ReauthenticationRequiredError = class extends Error {
2158
- profileKey;
2159
- reason;
2160
- terminalError;
2161
- constructor(profileKey, reason, terminalError) {
2162
- const errorSuffix = terminalError ? ` (${terminalError})` : "";
2163
- super(`Re-authentication required for profile "${profileKey}": ${reason}${errorSuffix}`);
2164
- this.name = "ReauthenticationRequiredError";
2165
- this.profileKey = profileKey;
2166
- this.reason = reason;
2167
- this.terminalError = terminalError;
2168
- }
2169
- };
2170
- var DEFAULT_SAFETY_MARGIN_SECONDS = 60;
2171
- var DEFAULT_LOCK_TIMEOUT_MS = 6e4;
2172
- var DEFAULT_STALE_LOCK_TIMEOUT_MS = 6e4;
2173
- var TokenRefreshManager = class {
2174
- vault;
2175
- safetyMarginSeconds;
2176
- lockTimeoutMs;
2177
- staleLockTimeoutMs;
2178
- lockPath;
2179
- fetchFn;
2180
- logger;
2181
- errorLogger;
2182
- inFlightRefreshes = /* @__PURE__ */ new Map();
2183
- constructor(options) {
2184
- this.vault = options?.vault || new CredentialVault();
2185
- this.safetyMarginSeconds = options?.safetyMarginSeconds ?? DEFAULT_SAFETY_MARGIN_SECONDS;
2186
- this.lockTimeoutMs = options?.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
2187
- this.staleLockTimeoutMs = options?.staleLockTimeoutMs ?? options?.staleTimeoutMs ?? DEFAULT_STALE_LOCK_TIMEOUT_MS;
2188
- this.lockPath = options?.lockPath;
2189
- this.fetchFn = options?.fetchFn;
2190
- this.logger = options?.logger;
2191
- this.errorLogger = options?.errorLogger;
2192
- }
2193
- /**
2194
- * Returns a valid access token for the given profile key.
2195
- * If the current access token is within the safety margin or expired, refreshes
2196
- * transparently using the stored refresh token behind a cross-process lock.
2197
- */
2198
- async getValidAccessToken(profileKey) {
2199
- const creds = await this.getValidCredentials(profileKey);
2200
- return creds.accessToken;
2201
- }
2202
- /**
2203
- * Returns valid stored credentials for the profile key, refreshing if necessary.
2204
- */
2205
- async getValidCredentials(profileKey) {
2206
- const creds = await this.vault.load(profileKey);
2207
- if (!creds) {
2208
- throw new ReauthenticationRequiredError(profileKey, "No credentials found in vault.");
2209
- }
2210
- if (!isCredentialExpired(creds, this.safetyMarginSeconds)) {
2211
- return creds;
2212
- }
2213
- return await this.refreshWithSingleFlight(
2214
- profileKey,
2215
- false
2216
- /* not forced */
2217
- );
2218
- }
2219
- /**
2220
- * Explicitly forces a token refresh regardless of current expiry time.
2221
- * Reuses an in-flight refresh promise if one is already active (m7 fix).
2222
- */
2223
- async refresh(profileKey) {
2224
- const existing = this.inFlightRefreshes.get(profileKey);
2225
- if (existing) {
2226
- return await existing;
2227
- }
2228
- return await this.refreshWithSingleFlight(
2229
- profileKey,
2230
- true
2231
- /* forced */
2232
- );
2233
- }
2234
- /**
2235
- * Collapses concurrent in-process calls for the same profileKey to a single execution.
2236
- */
2237
- async refreshWithSingleFlight(profileKey, force) {
2238
- const existing = this.inFlightRefreshes.get(profileKey);
2239
- if (existing && !force) {
2240
- return await existing;
2241
- }
2242
- const refreshPromise = this.executeRefreshWithLock(profileKey, force);
2243
- this.inFlightRefreshes.set(profileKey, refreshPromise);
2244
- try {
2245
- return await refreshPromise;
2246
- } finally {
2247
- this.inFlightRefreshes.delete(profileKey);
2248
- }
2249
- }
2250
- /**
2251
- * Acquires the cross-process lock, double-checks if another process already refreshed,
2252
- * and invokes the server refresh endpoint if still needed.
2253
- */
2254
- async executeRefreshWithLock(profileKey, force) {
2255
- const resolvedLockPath = this.lockPath || import_node_path3.default.join(this.vault.getConfigDir(), "credentials.lock");
2256
- const lock = new VaultLock({
2257
- lockPath: resolvedLockPath,
2258
- timeoutMs: this.lockTimeoutMs,
2259
- staleTimeoutMs: this.staleLockTimeoutMs
2260
- });
2261
- return await lock.withLock(async () => {
2262
- const current = await this.vault.load(profileKey);
2263
- if (!current) {
2264
- throw new ReauthenticationRequiredError(profileKey, "Credentials missing from vault.");
2265
- }
2266
- if (!force && !isCredentialExpired(current, this.safetyMarginSeconds)) {
2267
- this.logger?.(`Token for profile "${profileKey}" was already refreshed by another process.`);
2268
- return current;
2269
- }
2270
- return await this.performTokenExchange(profileKey, current);
2271
- });
2272
- }
2273
- /**
2274
- * Performs the HTTP refresh exchange with the OAuth2 token endpoint.
2275
- */
2276
- async performTokenExchange(profileKey, current) {
2277
- if (!current.refreshToken) {
2278
- throw new ReauthenticationRequiredError(
2279
- profileKey,
2280
- "Cannot refresh access token: No refresh token stored in credentials."
2281
- );
2282
- }
2283
- const tokenUrl = `${current.serverUrl}/api/auth/oauth2/token`;
2284
- const bodyParams = new URLSearchParams();
2285
- bodyParams.set("grant_type", "refresh_token");
2286
- bodyParams.set("refresh_token", current.refreshToken);
2287
- bodyParams.set("client_id", current.clientId);
2288
- if (current.resource) {
2289
- bodyParams.set("resource", current.resource);
2290
- }
2291
- let tokenResponse;
2292
- try {
2293
- const res = await sendRequest(tokenUrl, {
2294
- method: "POST",
2295
- headers: {
2296
- "Content-Type": "application/x-www-form-urlencoded",
2297
- Accept: "application/json"
2298
- },
2299
- body: bodyParams,
2300
- retries: 1,
2301
- fetchFn: this.fetchFn
2302
- });
2303
- if (!res.ok) {
2304
- throw new HttpError(
2305
- `Token refresh request failed with HTTP ${res.status}`,
2306
- res.status,
2307
- res.data
2308
- );
2309
- }
2310
- tokenResponse = res.data;
2311
- } catch (err) {
2312
- if (err instanceof HttpError) {
2313
- const errorData = err.data && typeof err.data === "object" ? err.data : {};
2314
- const errorCode = typeof errorData.error === "string" ? errorData.error : "";
2315
- const errorDesc = typeof errorData.error_description === "string" ? errorData.error_description : err.message;
2316
- const isTerminal = errorCode === "invalid_grant" || errorCode === "invalid_client";
2317
- const sanitizedMsg = sanitizeSecretString(errorDesc, [
2318
- current.refreshToken,
2319
- current.accessToken
2320
- ]);
2321
- this.errorLogger?.(
2322
- `Token refresh rejected (${err.status}, ${errorCode || "unknown"}): ${sanitizedMsg}`
2323
- );
2324
- if (isTerminal) {
2325
- try {
2326
- await this.vault.delete(profileKey);
2327
- } catch {
2328
- }
2329
- throw new ReauthenticationRequiredError(
2330
- profileKey,
2331
- `Refresh token rejected by authorization server: ${sanitizedMsg}`,
2332
- errorCode
2333
- );
2334
- }
2335
- throw new HttpError(
2336
- `Token refresh rejected (${err.status}): ${sanitizedMsg}`,
2337
- err.status,
2338
- err.data
2339
- );
2340
- }
2341
- this.errorLogger?.(
2342
- `Token refresh network/transient error for profile "${profileKey}":
2343
- ${err instanceof Error ? err.message : String(err)}`
2344
- );
2345
- throw err;
2346
- }
2347
- if (!tokenResponse || !tokenResponse.access_token) {
2348
- const errCode = tokenResponse?.error || "unknown_error";
2349
- const rawDesc = tokenResponse?.error_description || "Missing access_token in token response.";
2350
- const sanitizedDesc = sanitizeSecretString(rawDesc, [
2351
- current.refreshToken,
2352
- current.accessToken
2353
- ]);
2354
- this.errorLogger?.(`Token response invalid for profile "${profileKey}": ${sanitizedDesc}`);
2355
- if (errCode === "invalid_grant" || errCode === "invalid_client") {
2356
- try {
2357
- await this.vault.delete(profileKey);
2358
- } catch {
2359
- }
2360
- throw new ReauthenticationRequiredError(profileKey, sanitizedDesc, errCode);
2361
- }
2362
- throw new Error(`Token refresh failed: ${sanitizedDesc}`);
2363
- }
2364
- const payload = decodeJwtPayload(tokenResponse.access_token);
2365
- let organizationId;
2366
- if ("organizationId" in payload) {
2367
- if (typeof payload.organizationId === "string") {
2368
- organizationId = payload.organizationId;
2369
- } else if (payload.organizationId === null) {
2370
- organizationId = null;
2371
- } else {
2372
- organizationId = current.organizationId ?? null;
2373
- }
2374
- } else {
2375
- organizationId = current.organizationId ?? null;
2376
- }
2377
- const expiresAtMs = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : typeof payload.exp === "number" ? payload.exp * 1e3 : Date.now() + 3600 * 1e3;
2378
- const newRefreshToken = tokenResponse.refresh_token || current.refreshToken;
2379
- const updatedCreds = {
2380
- serverUrl: current.serverUrl,
2381
- clientId: current.clientId,
2382
- accessToken: tokenResponse.access_token,
2383
- refreshToken: newRefreshToken,
2384
- tokenType: tokenResponse.token_type || current.tokenType || "Bearer",
2385
- scope: tokenResponse.scope || current.scope,
2386
- resource: current.resource,
2387
- organizationId,
2388
- expiresAt: expiresAtMs,
2389
- updatedAt: Date.now()
2390
- };
2391
- try {
2392
- await this.vault.save(profileKey, updatedCreds);
2393
- } catch (saveErr) {
2394
- try {
2395
- await this.vault.delete(profileKey);
2396
- } catch {
2397
- }
2398
- const msg = saveErr instanceof Error ? saveErr.message : String(saveErr);
2399
- this.errorLogger?.(
2400
- `Failed to persist refreshed credentials for profile "${profileKey}": ${msg}`
2401
- );
2402
- throw new ReauthenticationRequiredError(
2403
- profileKey,
2404
- `Failed to persist refreshed credentials to vault: ${msg}`,
2405
- "persistence_failure"
2406
- );
2407
- }
2408
- this.logger?.(
2409
- `Successfully refreshed credentials for profile "${profileKey}"
2410
- (expires: ${new Date(expiresAtMs).toISOString()}).`
2411
- );
2412
- return updatedCreds;
2413
- }
2414
- };
2415
- // Annotate the CommonJS export names for ESM import in node:
2416
- 0 && (module.exports = {
2417
- CredentialVault,
2418
- DEFAULT_CLI_CLIENT_ID,
2419
- DEFAULT_KEYCHAIN_SERVICE,
2420
- DEFAULT_READ_ONLY_SCOPES,
2421
- DEFAULT_SCOPES,
2422
- DEFAULT_SERVER_URL,
2423
- DEVICE_CODE_GRANT_TYPE,
2424
- DefaultKeychainRunner,
2425
- HttpError,
2426
- KeychainService,
2427
- ReauthenticationRequiredError,
2428
- TokenRefreshManager,
2429
- VaultLock,
2430
- VaultLockError,
2431
- VaultLockTimeoutError,
2432
- decodeJwtPayload,
2433
- defaultIsProcessAlive,
2434
- executeLogin,
2435
- executeLogout,
2436
- executeStatus,
2437
- isCredentialExpired,
2438
- isLocalhost,
2439
- isValidStoredCredentials,
2440
- normalizeProfileKey,
2441
- openBrowser,
2442
- pollDeviceToken,
2443
- redactCredentials,
2444
- requestDeviceCode,
2445
- resolveAuthServerUrl,
2446
- resolveMcpResourceUrl,
2447
- sanitizeSecretString,
2448
- sendRequest,
2449
- validateScopes,
2450
- withVaultLock
2451
- });
1
+ "use strict";var be=Object.create;var N=Object.defineProperty;var _e=Object.getOwnPropertyDescriptor;var Ie=Object.getOwnPropertyNames;var xe=Object.getPrototypeOf,Me=Object.prototype.hasOwnProperty;var Le=(i,e)=>()=>{try{return e||i((e={exports:{}}).exports,e),e.exports}catch(t){throw e=0,t}},Ae=(i,e)=>{for(var t in e)N(i,t,{get:e[t],enumerable:!0})},se=(i,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of Ie(e))!Me.call(i,n)&&n!==t&&N(i,n,{get:()=>e[n],enumerable:!(r=_e(e,n))||r.enumerable});return i};var I=(i,e,t)=>(t=i!=null?be(xe(i)):{},se(e||!i||!i.__esModule?N(t,"default",{value:i,enumerable:!0}):t,i)),$e=i=>se(N({},"__esModule",{value:!0}),i);var he=Le(g=>{"use strict";Object.defineProperty(g,"__esModule",{value:!0});g.MCP_OAUTH_SCOPES=g.OFFLINE_ACCESS_SCOPE=g.MCP_ELIGIBLE_PERMISSION_SET=g.MCP_ELIGIBLE_PERMISSIONS=g.SENSITIVE_MCP_RESOURCES=g.PERMISSION_SET=g.ALL_PERMISSIONS=g.CATALOG_DEFINITIONS=void 0;g.isPermissionString=ce;g.parsePermissionString=le;g.isSensitiveMcpResource=ue;g.isMcpEligiblePermission=de;g.partitionMcpScopes=De;g.CATALOG_DEFINITIONS={organization:{tier:"T",actions:["update","delete"],description:"Organization tenant profile and settings",models:["Organization","OrganizationSetting","OrganizationSubscription"],mcpEligible:!0},member:{tier:"T",actions:["create","update","delete"],description:"Organization member management",models:["Member"],mcpEligible:!0},invitation:{tier:"T",actions:["create","cancel"],description:"Organization membership invitations",models:["Invitation"],mcpEligible:!0},location:{tier:"T",actions:["read","write","delete"],description:"Physical hotspot location management",models:["Location"],mcpEligible:!0},router:{tier:"T",actions:["read","write","delete","control"],description:"Gateway router inventory, provisioning and control",models:["Router","ZtpToken","CoaQueue","IpamAllocationCursor"],mcpEligible:!0},package:{tier:"S",actions:["read","write","delete","publish"],description:"WiFi access package catalog (tenant + shared)",models:["Package","VoucherPackage"],mcpEligible:!0},voucher:{tier:"S",actions:["read","write","publish","issue","revoke"],description:"Prepaid voucher codes and issuance (tenant + shared)",models:["Voucher","VoucherIssuance","VoucherUser"],mcpEligible:!0},session:{tier:"T",actions:["read","control"],description:"Active and historical WiFi guest sessions",models:["WifiSession","RadiusAccounting","DeviceToken"],mcpEligible:!0},wallet:{tier:"T",actions:["read","topup","charge","refund","freeze"],description:"Prepaid wallet balances, topups and ledger transactions (SENSITIVE - MCP EXCLUDED)",models:["Wallet","WalletTransaction","TopupRequest"],hasOwnQualifier:!0,mcpEligible:!1},payment:{tier:"T",actions:["read","approve","refund"],description:"Inbound payment transactions and gateway integrations (SENSITIVE - MCP EXCLUDED)",models:["PaymentTransaction","PaymentGatewayIntegration"],hasOwnQualifier:!0,mcpEligible:!1},subscription:{tier:"T",actions:["read","manage"],description:"SaaS organization plan subscription and entitlements",models:["OrganizationSubscription"],mcpEligible:!0},partner:{tier:"T",actions:["read","manage","payout:approve"],description:"Affiliate/partner programs and payout requests",models:["Partner","PartnerPayoutRequest"],mcpEligible:!0},campaign:{tier:"T",actions:["read","write"],description:"Captive portal ad campaigns and lead capture",models:["AdCampaign","LeadCapture"],mcpEligible:!0},notification:{tier:"T",actions:["read","send","template:write"],description:"Multi-channel notifications, templates and logs",models:["NotificationItem","NotificationTemplate","DeliveryLog"],hasOwnQualifier:!0,mcpEligible:!0},store:{tier:"P",actions:["read","write"],description:"Public hardware/affiliate store catalog",models:["StoreProduct","AffiliateClick"],mcpEligible:!0},blog:{tier:"P",actions:["read","write","publish"],description:"Company-wide editorial articles and CDN media",models:["BlogPost","Media"],hasOwnQualifier:!0,mcpEligible:!0},analytics:{tier:"D",actions:["read"],description:"Aggregated usage, session and revenue metrics",models:["WifiSession","Package","PaymentTransaction"],mcpEligible:!0},diagnostics:{tier:"P",actions:["read","write"],description:"System error events, MCP tool execution spans, flow events, issue triage and audit logs",models:["ErrorEvent","McpToolSpan","AuditLog","SystemSetting","FlowEvent","IssueState"],mcpEligible:!0}};g.ALL_PERMISSIONS=["organization:update","organization:delete","member:create","member:update","member:delete","invitation:create","invitation:cancel","location:read","location:write","location:delete","router:read","router:write","router:delete","router:control","package:read","package:write","package:delete","package:publish","voucher:read","voucher:write","voucher:publish","voucher:issue","voucher:revoke","session:read","session:control","wallet:read","wallet:topup","wallet:charge","wallet:refund","wallet:freeze","payment:read","payment:approve","payment:refund","subscription:read","subscription:manage","partner:read","partner:manage","partner:payout:approve","campaign:read","campaign:write","notification:read","notification:send","notification:template:write","store:read","store:write","blog:read","blog:write","blog:publish","analytics:read","diagnostics:read","diagnostics:write"];g.PERMISSION_SET=new Set(g.ALL_PERMISSIONS);function ce(i){return typeof i=="string"&&g.PERMISSION_SET.has(i)}function le(i){if(!ce(i))return null;let e=i.indexOf(":"),t=i.slice(0,e),r=i.slice(e+1);return{resource:t,action:r}}g.SENSITIVE_MCP_RESOURCES=["payment","wallet"];function ue(i){return i==="payment"||i==="wallet"}function de(i){let e=le(i);return e?!ue(e.resource):!1}g.MCP_ELIGIBLE_PERMISSIONS=g.ALL_PERMISSIONS.filter(de);g.MCP_ELIGIBLE_PERMISSION_SET=new Set(g.MCP_ELIGIBLE_PERMISSIONS);g.OFFLINE_ACCESS_SCOPE="offline_access";g.MCP_OAUTH_SCOPES=[...g.MCP_ELIGIBLE_PERMISSIONS,g.OFFLINE_ACCESS_SCOPE];function De(i){return{permissionScopes:i.filter(e=>e!==g.OFFLINE_ACCESS_SCOPE),offlineAccessRequested:i.includes(g.OFFLINE_ACCESS_SCOPE)}}});var Ge={};Ae(Ge,{CredentialVault:()=>P,DEFAULT_CLI_CLIENT_ID:()=>R,DEFAULT_KEYCHAIN_SERVICE:()=>j,DEFAULT_READ_ONLY_SCOPES:()=>ge,DEFAULT_SCOPES:()=>me,DEFAULT_SERVER_URL:()=>pe,DEVICE_CODE_GRANT_TYPE:()=>ye,DefaultKeychainRunner:()=>z,HttpError:()=>T,KeychainService:()=>A,ReauthenticationRequiredError:()=>_,TokenRefreshManager:()=>ee,VaultLock:()=>L,VaultLockError:()=>J,VaultLockTimeoutError:()=>B,decodeJwtPayload:()=>q,defaultIsProcessAlive:()=>Pe,executeLogin:()=>Ne,executeLogout:()=>fe,executeStatus:()=>Ue,formatRemainingDuration:()=>Ee,isCredentialExpired:()=>M,isLocalhost:()=>Z,isValidStoredCredentials:()=>U,normalizeProfileKey:()=>x,openBrowser:()=>ve,pollDeviceToken:()=>Te,redactCredentials:()=>Re,requestDeviceCode:()=>ke,resolveAuthServerUrl:()=>O,resolveMcpResourceUrl:()=>Se,sanitizeSecretString:()=>E,sendRequest:()=>C,validateScopes:()=>we,withVaultLock:()=>qe});module.exports=$e(Ge);function U(i){if(!i||typeof i!="object"||Array.isArray(i))return!1;let e=i;return!(typeof e.serverUrl!="string"||typeof e.clientId!="string"||typeof e.accessToken!="string"||typeof e.tokenType!="string"||typeof e.expiresAt!="number"||!Number.isFinite(e.expiresAt)||!("organizationId"in e)||e.organizationId!==null&&typeof e.organizationId!="string"||e.refreshToken!==void 0&&typeof e.refreshToken!="string"||e.scope!==void 0&&typeof e.scope!="string"||e.resource!==void 0&&typeof e.resource!="string"||e.updatedAt!==void 0&&(typeof e.updatedAt!="number"||!Number.isFinite(e.updatedAt)))}function x(i,e){let t=(e||"").trim(),r=(i||"").trim();/^https?:\/\//i.test(r)||(r=`https://${r}`);try{return`${new URL(r).origin.toLowerCase()}::${t}`}catch{return`${r.replace(/\/+$/,"").toLowerCase()}::${t}`}}function Re(i){let e={...i};return"accessToken"in e&&typeof e.accessToken=="string"&&(e.accessToken="[REDACTED]"),"refreshToken"in e&&typeof e.refreshToken=="string"&&(e.refreshToken="[REDACTED]"),e}function E(i,e){let t=i;for(let r of e)r&&typeof r=="string"&&r.length>=4&&(t=t.split(r).join("[REDACTED]"));return t}function M(i,e=60){if(!i.expiresAt)return!1;let t=i.expiresAt>1e11?i.expiresAt:i.expiresAt*1e3;return Date.now()+e*1e3>=t}var oe=require("child_process");var z=class{defaultTimeoutMs;constructor(e=5e3){this.defaultTimeoutMs=e}spawn(e,t,r){return new Promise(n=>{let s;try{s=(0,oe.spawn)(e,t,{shell:!1,stdio:["pipe","pipe","pipe"]})}catch(a){let d=a instanceof Error?a.message:String(a);return n({stdout:"",stderr:d,exitCode:-1})}let o="",c="",l=!1,u=r?.timeoutMs??this.defaultTimeoutMs,m=setTimeout(()=>{if(!l){l=!0;try{s.kill("SIGKILL")}catch{}n({stdout:o,stderr:`Keychain operation timed out after ${u}ms`,exitCode:-1})}},u);if(s.stdout?.on("data",a=>{o+=a.toString()}),s.stderr?.on("data",a=>{c+=a.toString()}),s.on("error",a=>{c+=a.message}),s.on("close",a=>{l||(l=!0,clearTimeout(m),n({stdout:o,stderr:c,exitCode:a??-1}))}),r?.input!==void 0&&s.stdin)try{s.stdin.write(r.input),s.stdin.end()}catch{}})}},j="iwas-cli",A=class{service;runner;platform;constructor(e){this.service=e?.serviceName||j,this.runner=e?.runner||new z,this.platform=e?.platform||process.platform}availableCache=null;getPlatform(){return this.platform}async isAvailable(e=!1){if(!e&&this.availableCache===!0)return!0;if(this.platform==="darwin"){let t=await this.runner.spawn("/usr/bin/security",["help"]),r=t.exitCode===0||t.stdout.includes("security")||t.stderr.includes("security");return r&&(this.availableCache=!0),r}if(this.platform==="linux"){let r=(await this.runner.spawn("which",["secret-tool"])).exitCode===0;return r&&(this.availableCache=!0),r}return!1}clearAvailabilityCache(){this.availableCache=null}async getSecret(e){if(this.platform==="darwin"){let t=await this.runner.spawn("/usr/bin/security",["find-generic-password","-s",this.service,"-a",e,"-w"]);if(t.exitCode===0){let r=t.stdout.trim();if(r.length>0&&r.length%2===0&&/^[0-9a-fA-F]+$/.test(r)&&(r.startsWith("7b")||r.startsWith("7B")||r.startsWith("5b")||r.startsWith("5B")))try{let n=Buffer.from(r,"hex").toString("utf8");if(!n.includes("\uFFFD"))return n}catch{}return r}if(t.exitCode===44||t.stderr.includes("could not be found")||t.stderr.includes("The specified item could not be found"))return null;throw new Error(`Keychain read failed: ${t.stderr.trim()||`exit code ${t.exitCode}`}`)}if(this.platform==="linux"){let t=await this.runner.spawn("secret-tool",["lookup","service",this.service,"account",e]);if(t.exitCode===0)return t.stdout.length>0?t.stdout.trim():null;let r=t.stderr.trim();if(t.exitCode===1&&(r===""||r.toLowerCase().includes("not found")||r.toLowerCase().includes("no such secret")))return null;throw new Error(`Secret-tool read failed: ${r||`exit code ${t.exitCode}`}`)}throw new Error(`OS keychain not supported on platform: ${this.platform}`)}async setSecret(e,t,r){let n=r||`${this.service} (${e})`,s=[t];try{let o=JSON.parse(t);if(typeof o=="object"&&o!==null)for(let c of Object.values(o))typeof c=="string"&&c.length>=4&&s.push(c)}catch{}if(this.platform==="darwin"){let o=await this.runner.spawn("/usr/bin/security",["add-generic-password","-U","-s",this.service,"-a",e,"-l",n,"-w",t]);if(o.exitCode!==0){let c=E(o.stderr.trim(),s);throw new Error(`Keychain write failed: ${c||`exit code ${o.exitCode}`}`)}return}if(this.platform==="linux"){let o=await this.runner.spawn("secret-tool",["store",`--label=${n}`,"service",this.service,"account",e],{input:t});if(o.exitCode!==0){let c=E(o.stderr.trim(),s);throw new Error(`Secret-tool write failed: ${c||`exit code ${o.exitCode}`}`)}return}throw new Error(`OS keychain not supported on platform: ${this.platform}`)}async deleteSecret(e){if(this.platform==="darwin"){let t=await this.runner.spawn("/usr/bin/security",["delete-generic-password","-s",this.service,"-a",e]);if(t.exitCode===0||t.exitCode===44||t.stderr.includes("could not be found"))return;throw new Error(`Keychain delete failed: ${t.stderr.trim()||`exit code ${t.exitCode}`}`)}if(this.platform==="linux"){let t=await this.runner.spawn("secret-tool",["clear","service",this.service,"account",e]);if(t.exitCode===0||t.exitCode===1)return;throw new Error(`Secret-tool clear failed: ${t.stderr.trim()||`exit code ${t.exitCode}`}`)}throw new Error(`OS keychain not supported on platform: ${this.platform}`)}};var w=I(require("fs")),$=I(require("path")),ae=I(require("os"));var V="__profiles__",P=class{vaultType;configDir;credentialsFilePath;keychain;constructor(e){let t=process.env.IWAS_MCP_VAULT;if(t&&!["auto","keychain","file"].includes(t))throw new Error(`Invalid IWAS_MCP_VAULT value: "${t}". Supported values are "auto", "keychain", or "file".`);let r=t;this.vaultType=e?.vaultType||r||"auto";let n=e?.configDir||process.env.XDG_CONFIG_HOME||$.default.join(ae.default.homedir(),".config");this.configDir=e?.configDir?e.configDir:$.default.join(n,"iwas"),this.credentialsFilePath=$.default.join(this.configDir,"credentials.json"),this.keychain=e?.keychainService||new A({serviceName:e?.serviceName||j,runner:e?.keychainRunner})}getConfigDir(){return this.configDir}getCredentialsFilePath(){return this.credentialsFilePath}async getEffectiveBackend(){if(this.vaultType==="file")return"file";let e=await this.keychain.isAvailable().catch(()=>!1),t=this.keychain.getPlatform();if(this.vaultType==="keychain"){if(!e)throw new Error(`Keychain vault requested (IWAS_MCP_VAULT="keychain") but OS keychain is unavailable on platform: ${t}`);return"keychain"}return e?"keychain":"file"}async load(e){if(await this.getEffectiveBackend()==="keychain"){let r=null;try{let c=await this.keychain.getSecret(e);if(c)try{let l=JSON.parse(c);if(U(l))r=l;else if(this.vaultType==="keychain")throw new Error(`Corrupt credentials stored in keychain for profile: ${e}`)}catch(l){if(this.vaultType==="keychain"){let u=l instanceof Error?l.message:String(l);throw new Error(`Corrupt credentials JSON in keychain for profile ${e}: ${u}`)}}}catch(c){if(this.vaultType==="keychain")throw c}if(this.vaultType==="keychain")return r;let n=this.loadFromFile(e);if(!r){if(n)try{await this.keychain.setSecret(e,JSON.stringify(n),`IWAS MCP Profile (${e})`),await this.addProfileToKeychainIndex(e),this.deleteFromFile(e)}catch{}return n}if(!n)return r;let s=r.updatedAt??r.expiresAt??0;if((n.updatedAt??n.expiresAt??0)>s){try{await this.keychain.setSecret(e,JSON.stringify(n),`IWAS MCP Profile (${e})`),await this.addProfileToKeychainIndex(e),this.deleteFromFile(e)}catch{}return n}else return this.deleteFromFile(e),r}return this.loadFromFile(e)}async save(e,t){let r=await this.getEffectiveBackend(),n={...t,updatedAt:t.updatedAt??Date.now()};if(r==="keychain"){let s=!1;try{let o=JSON.stringify(n);await this.keychain.setSecret(e,o,`IWAS MCP Profile (${e})`),s=!0;try{await this.addProfileToKeychainIndex(e)}catch(c){let l=c instanceof Error?c.message:String(c),u=E(l,[n.accessToken,n.refreshToken]);throw new Error(`Keychain index update failed: ${u}`)}this.deleteFromFile(e);return}catch(o){if(this.vaultType==="keychain"||s){let c=o instanceof Error?o.message:String(o),l=E(c,[n.accessToken,n.refreshToken]);throw o instanceof Error&&o.message.startsWith("Keychain")?o:new Error(`Keychain save failed: ${l}`)}}}this.saveToFile(e,n)}async delete(e){let t=null,r=!1;if(this.vaultType==="file"){this.deleteFromFile(e);return}try{if(r=await this.keychain.isAvailable().catch(()=>!1),this.vaultType==="keychain"&&!r)throw new Error(`Keychain vault requested (IWAS_MCP_VAULT="keychain") but OS keychain is unavailable on platform: ${this.keychain.getPlatform()}`);r&&(await this.keychain.deleteSecret(e),await this.removeProfileFromKeychainIndex(e))}catch(n){t=n}if(this.deleteFromFile(e),t&&(this.vaultType==="keychain"||this.vaultType==="auto"&&r))throw t}async listProfiles(){if(this.vaultType==="file")return this.listProfilesFromFile();if(this.vaultType==="keychain")return this.readKeychainIndex();let e=new Set;try{if(await this.keychain.isAvailable()){let r=await this.readKeychainIndex();for(let n of r)e.add(n)}}catch{}for(let t of this.listProfilesFromFile())e.add(t);return Array.from(e)}async readKeychainIndex(){let e=await this.keychain.getSecret(V);if(!e)return[];try{let t=JSON.parse(e);return Array.isArray(t)?t.filter(r=>typeof r=="string"):[]}catch(t){let r=t instanceof Error?t.message:String(t);throw new Error(`Corrupt keychain profile index at ${V}: ${r}`)}}async addProfileToKeychainIndex(e){let t=await this.readKeychainIndex();t.includes(e)||(t.push(e),await this.keychain.setSecret(V,JSON.stringify(t),"IWAS MCP Profiles Index"))}async removeProfileFromKeychainIndex(e){let t=await this.readKeychainIndex();if(t.includes(e)){let r=t.filter(n=>n!==e);await this.keychain.setSecret(V,JSON.stringify(r),"IWAS MCP Profiles Index")}}ensureConfigDirectory(){w.default.existsSync(this.configDir)||w.default.mkdirSync(this.configDir,{recursive:!0,mode:448});try{w.default.chmodSync(this.configDir,448)}catch{}}readAllFromFile(){if(!w.default.existsSync(this.credentialsFilePath))return{};if(process.platform!=="win32")try{w.default.chmodSync(this.credentialsFilePath,384)}catch{}let e=w.default.readFileSync(this.credentialsFilePath,"utf8").trim();if(!e)return{};try{let t=JSON.parse(e);if(typeof t!="object"||t===null||Array.isArray(t))throw new Error("Root credentials object must be a JSON record");for(let[r,n]of Object.entries(t))if(!U(n))throw new Error(`Profile "${r}" contains invalid or missing credential fields`);return t}catch(t){let r=t instanceof Error?t.message:String(t);throw new Error(`Corrupt credentials file at ${this.credentialsFilePath}: ${r}. Will not overwrite.`)}}writeAllToFile(e){this.ensureConfigDirectory();let t=JSON.stringify(e,null,2)+`
2
+ `,r=$.default.join(this.configDir,`.credentials.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`),n=null;try{n=w.default.openSync(r,"w",384),w.default.writeSync(n,t,void 0,"utf8"),w.default.fsyncSync(n),w.default.closeSync(n),n=null;try{w.default.chmodSync(r,384)}catch{}w.default.renameSync(r,this.credentialsFilePath);try{w.default.chmodSync(this.credentialsFilePath,384)}catch{}if(process.platform!=="win32")try{let s=w.default.openSync(this.configDir,"r");w.default.fsyncSync(s),w.default.closeSync(s)}catch{}}catch(s){if(n!==null)try{w.default.closeSync(n)}catch{}if(w.default.existsSync(r))try{w.default.unlinkSync(r)}catch{}throw s}}loadFromFile(e){return this.readAllFromFile()[e]??null}saveToFile(e,t){let r=this.readAllFromFile();r[e]=t,this.writeAllToFile(r)}deleteFromFile(e){if(!w.default.existsSync(this.credentialsFilePath))return;let t=this.readAllFromFile();e in t&&(delete t[e],this.writeAllToFile(t))}listProfilesFromFile(){if(!w.default.existsSync(this.credentialsFilePath))return[];let e=this.readAllFromFile();return Object.keys(e)}};var T=class extends Error{status;data;constructor(e,t,r){super(e),this.name="HttpError",this.status=t,this.data=r}},Oe=15e3,Fe=2;async function C(i,e){let t=e?.method||"GET",r=e?.timeoutMs??Oe,n=e?.retries??Fe,s={Accept:"application/json",...e?.headers||{}},o;e?.body&&(typeof e.body=="string"?o=e.body:e.body instanceof URLSearchParams?(o=e.body.toString(),s["Content-Type"]||(s["Content-Type"]="application/x-www-form-urlencoded")):(o=JSON.stringify(e.body),s["Content-Type"]||(s["Content-Type"]="application/json")));let c=0,l=null;for(;c<=n;){c++;let u=new AbortController,m=setTimeout(()=>u.abort(),r);try{let d=await(e?.fetchFn||fetch)(i,{method:t,headers:s,body:o,signal:u.signal});clearTimeout(m);let p=d.headers.get("content-type")||"",f;if(p.includes("application/json"))try{f=await d.json()}catch{f=null}else f=await d.text();if(d.status>=500&&d.status<=504&&c<=n){let y=Math.min(1e3*Math.pow(2,c-1)+Math.random()*200,5e3);await new Promise(S=>setTimeout(S,y));continue}return{status:d.status,ok:d.ok,headers:d.headers,data:f}}catch(a){clearTimeout(m);let p=a instanceof Error&&a.name==="AbortError"?`Request timed out after ${r}ms`:a instanceof Error?a.message:String(a);if(l=new Error(`Network request failed for ${t} ${i}: ${p}`),c<=n){let f=Math.min(500*Math.pow(2,c-1)+Math.random()*200,3e3);await new Promise(y=>setTimeout(y,f));continue}}}throw l||new Error(`Network request failed after ${c} attempts`)}var K=require("child_process"),b=I(he());async function fe(i){let e=i?.logger||(f=>process.stdout.write(`${f}
3
+ `)),t=i?.errorLogger||(f=>process.stderr.write(`${f}
4
+ `)),r=O(i?.server),n=(i?.clientId||R).trim(),s=x(r,n),o=i?.vault||new P,c=i?.force===!0,l=await o.load(s);if(!l){let f={success:!0,profileKey:s,serverRevoked:!1,refreshTokenRevoked:!1,localCleared:!1,message:`No stored credentials found for profile: ${s}`};return i?.json?e(JSON.stringify({event:"logout",success:!0,loggedOut:!0,profileKey:s,serverRevoked:!1,refreshTokenRevoked:!1,localCleared:!1,message:f.message})):e(`\u2139\uFE0F No stored credentials found for profile: ${s}`),f}let u=!1,m=null,a=`${r.replace(/\/+$/,"")}/api/v1/mcp-session/logout`;try{let f=l.tokenType||"Bearer",y=await C(a,{method:"POST",headers:{Authorization:`${f} ${l.accessToken}`},fetchFn:i?.fetchFn});if(y.status===204||y.status===200)u=!0;else if(y.status===401||y.status===403)u=!0;else if(y.status===400){let S=y.data;throw new Error(S?.message||"Server rejected logout request (HTTP 400).")}else throw new T(`Server returned status ${y.status} during logout`,y.status,y.data)}catch(f){m=f instanceof Error?f:new Error(String(f))}if(m&&!c){let f=`Server revocation failed: ${m.message}. Local credentials preserved. Use --force to clear local credentials anyway if the server is permanently offline.`;throw new Error(f)}let d=!1;if(l.refreshToken)try{let f=`${r.replace(/\/+$/,"")}/api/auth/oauth2/revoke`,y=new URLSearchParams({token:l.refreshToken,token_type_hint:"refresh_token",client_id:n}),S=await C(f,{method:"POST",body:y,fetchFn:i?.fetchFn});(S.ok||S.status===200||S.status===204||S.status===401||S.status===400)&&(d=!0)}catch{}await o.delete(s);let p={success:!0,profileKey:s,serverRevoked:u,refreshTokenRevoked:d,localCleared:!0,message:m?`Local credentials cleared with --force (server unreachable: ${m.message})`:"Successfully logged out and revoked all grants server-side"};return i?.json?e(JSON.stringify({event:"logout",success:!0,loggedOut:!0,profileKey:s,serverRevoked:u,refreshTokenRevoked:d,localCleared:!0,forced:c&&!!m})):m?(t(`\u26A0\uFE0F Warning: Server revocation failed (${m.message})`),e(`\u2705 Local credentials cleared for profile: ${s} (--force)
5
+ `)):(e(`
6
+ \u2705 Successfully logged out from profile: ${s}`),e(" \u2022 Server session & OAuth grants: Revoked"),e(` \u2022 Local credentials: Removed
7
+ `)),p}var R="iwas-cli",pe="https://getiwas.com",ge=b.MCP_ELIGIBLE_PERMISSIONS.filter(i=>i.endsWith(":read")),me=[...ge,b.OFFLINE_ACCESS_SCOPE].join(" "),ye="urn:ietf:params:oauth:grant-type:device_code";function we(i){let e=i.split(/\s+/).filter(Boolean);if(e.length===0)return{valid:!1,invalidScopes:[],error:"At least one OAuth scope must be requested."};let t=new Set(b.MCP_OAUTH_SCOPES),r=e.filter(s=>!t.has(s));return r.length>0?{valid:!1,invalidScopes:r,error:`Invalid OAuth scope(s) requested: [${r.join(", ")}]. Supported scopes are: [${b.MCP_OAUTH_SCOPES.join(", ")}]`}:e.filter(s=>s!==b.OFFLINE_ACCESS_SCOPE).length===0?{valid:!1,invalidScopes:[],error:"At least one MCP capability scope besides offline_access must be requested."}:{valid:!0,invalidScopes:[]}}function Z(i){return i==="localhost"||i==="127.0.0.1"||i==="::1"||i==="[::1]"}function O(i){let e=(i||process.env.IWAS_SERVER_URL||pe).trim();if(e=e.replace(/\/+$/,""),e.toLowerCase().endsWith("/mcp"))throw new Error(`Invalid --server value: "${e}". The server argument must be the auth/web origin (e.g. "https://getiwas.com"), not the MCP resource endpoint. Use --resource for "${e}".`);/^https?:\/\//i.test(e)||(e=`https://${e}`);let t;try{t=new URL(e)}catch{throw new Error(`Invalid server URL: "${e}"`)}if(t.pathname&&t.pathname!=="/"){let r=t.origin,n=i?`--server value: "${i}"`:`IWAS_SERVER_URL: "${e}"`;throw new Error(`Invalid ${n}. The server argument must be an origin without a path component (e.g. "${r}").`)}if(!Z(t.hostname)&&t.protocol!=="https:")throw new Error(`Insecure server protocol: "${e}". Non-localhost servers require HTTPS per OAuth 2.1 specifications.`);return t.origin.toLowerCase()}function Se(i,e){if(e&&e.trim()){let t=e.trim();/^https?:\/\//i.test(t)||(t=t.startsWith("localhost")||t.startsWith("127.0.0.1")||t.startsWith("[::1]")||t.startsWith("::1")?`http://${t}`:`https://${t}`);let r;try{r=new URL(t)}catch{throw new Error(`Invalid resource URL: "${t}"`)}if(!Z(r.hostname)&&r.protocol!=="https:")throw new Error(`Insecure resource protocol: "${t}". Non-localhost resource endpoints require HTTPS.`);return r.href.replace(/\/+$/,"")}return`${i}/mcp`}function q(i){let e=i.split(".");if(e.length!==3)return{};try{let t=Buffer.from(e[1],"base64url").toString("utf8"),r=JSON.parse(t);return typeof r=="object"&&r!==null?r:{}}catch{return{}}}function ve(i,e=K.spawn,t){try{let r;try{r=new URL(i)}catch{return t?.(`Invalid verification URL: "${i}". Please open manually.`),!1}if(r.protocol!=="http:"&&r.protocol!=="https:")return t?.(`Insecure verification URL protocol: "${r.protocol}". Expected http: or https:.`),!1;let n=process.platform,s;return n==="darwin"?s=e("open",[r.href],{detached:!0,stdio:"ignore"}):n==="win32"?s=e("explorer.exe",[r.href],{detached:!0,stdio:"ignore"}):s=e("xdg-open",[r.href],{detached:!0,stdio:"ignore"}),s.on("error",o=>{let c=o instanceof Error?o.message:String(o);t?.(`Could not launch browser automatically (${c}). Please open the verification URL manually.`)}),s.unref(),!0}catch(r){let n=r instanceof Error?r.message:String(r);return t?.(`Could not launch browser (${n}). Please open the verification URL manually.`),!1}}async function ke(i,e){let t=`${i}/api/auth/device/code`,r=await C(t,{method:"POST",body:{client_id:e.clientId,scope:e.scope,resource:e.resource},retries:0});if(!r.ok||!r.data||typeof r.data!="object")throw new T(`Failed to initiate device authorization with ${t}: HTTP ${r.status}`,r.status,r.data);let n=r.data;if(!n.device_code||!n.user_code||!n.verification_uri)throw new Error(`Malformed device code response from ${t}: missing required RFC 8628 fields`);return{device_code:n.device_code,user_code:n.user_code,verification_uri:n.verification_uri,verification_uri_complete:n.verification_uri_complete||`${n.verification_uri}?user_code=${encodeURIComponent(n.user_code)}`,expires_in:n.expires_in||600,interval:n.interval||5}}async function Te(i,e,t){let r=`${i}/api/auth/oauth2/token`,n=Math.max(e.initialInterval||5,1),s=e.timeoutMs||3e5,o=Date.now();for(;Date.now()-o<s&&(await new Promise(d=>setTimeout(d,n*1e3)),!(Date.now()-o>=s));){t?.("polling");let c=new URLSearchParams({grant_type:ye,client_id:e.clientId,device_code:e.deviceCode,resource:e.resource}),l=await C(r,{method:"POST",body:c,retries:0});if(l.status===200){let d=l.data;if(!d.access_token)throw new Error("Token endpoint returned HTTP 200 without access_token");return d}if(l.status===429){let d=l.headers?.get?.("retry-after"),p=n+5;if(d){let S=parseInt(d,10);if(!isNaN(S)&&S>0)p=S;else{let G=Date.parse(d);if(!isNaN(G)){let H=Math.ceil((G-Date.now())/1e3);H>0&&(p=H)}}}let f=Math.max(0,s-(Date.now()-o)),y=Math.ceil(f/1e3);n=y>0?Math.min(p,60,y):0,t?.(`slow_down (rate_limited, retry-after: ${n}s)`);continue}let u=l.data||{},m=u.error||"unknown_error",a=u.error_description||m;switch(m){case"authorization_pending":continue;case"slow_down":n+=5,t?.(`slow_down (interval: ${n}s)`);continue;case"expired_token":throw new Error("Device code has expired. Please run `iwas login` again to obtain a new code.");case"access_denied":throw new Error("Authorization was denied by the user.");case"invalid_grant":throw new Error(`Invalid grant: ${a}. The device code may have already been used or revoked.`);case"invalid_client":throw new Error(`Invalid OAuth client: ${a}. Check your --client-id.`);default:if(l.status>=500){n=Math.min(n+2,30);continue}throw new Error(`Device authorization failed: ${a} (HTTP ${l.status})`)}}throw new Error("Device login timed out waiting for user authorization.")}async function Ne(i){let e=i?.logger||(v=>process.stdout.write(`${v}
8
+ `)),t=i?.errorLogger||(v=>process.stderr.write(`${v}
9
+ `)),r=O(i?.server),n=Se(r,i?.resource),s=(i?.clientId||R).trim(),o=(i?.scopes||me).trim(),c=i?.vault||new P,l=we(o);if(!l.valid)throw new Error(l.error||"Invalid OAuth scopes requested.");let u=await ke(r,{clientId:s,scope:o,resource:n});if(i?.json){let v={event:"device_authorization",user_code:u.user_code,verification_uri:u.verification_uri,verification_uri_complete:u.verification_uri_complete,expires_in:u.expires_in,interval:u.interval};e(JSON.stringify(v)),t(`
10
+ \u{1F449} USER CODE: ${u.user_code}`),t(`\u{1F517} To verify, open: ${u.verification_uri_complete}
11
+ `)}else e(`
12
+ ============================================================`),e(" IWAS MCP Device Authorization "),e(`============================================================
13
+ `),e("First, enter this code on the verification page:"),e(`
14
+ \u{1F449} USER CODE: \x1B[1m\x1B[32m${u.user_code}\x1B[0m
15
+ `),e("To verify, open:"),e(` \u{1F517} ${u.verification_uri_complete}
16
+ `),e("Waiting for approval in browser...");let m=i?.spawnFn||K.spawn,a=i?.browserOpener||(v=>ve(v,m,ie=>{i?.json?t(`\u26A0\uFE0F ${ie}`):e(`\u26A0\uFE0F ${ie}`)}));i?.noBrowser||a(u.verification_uri_complete);let d=i?.pollTimeoutMs??u.expires_in*1e3,p=await Te(r,{clientId:s,deviceCode:u.device_code,resource:n,initialInterval:u.interval,timeoutMs:d},v=>{!i?.json&&v.startsWith("slow_down")&&e("Rate limit reached: slowing poll interval.")}),f=q(p.access_token);if(!f||Object.keys(f).length===0)throw new Error("Server returned an opaque or non-JWT access token. Expected a JWT access token targeted for MCP resource.");let y=Array.isArray(f.aud)?f.aud:typeof f.aud=="string"?[f.aud]:[],S=n.replace(/\/+$/,"").toLowerCase();if(!y.some(v=>{try{return new URL(String(v)).href.replace(/\/+$/,"").toLowerCase()===S}catch{return String(v).toLowerCase()===S}}))throw new Error(`Access token audience [${y.join(", ")}] does not match MCP resource URL "${n}".`);let te=(typeof p.scope=="string"&&p.scope||typeof f.scope=="string"&&f.scope||"").split(/\s+/).filter(Boolean),re=o.split(/\s+/).filter(Boolean);if(re.filter(v=>te.includes(v)).filter(v=>v!==b.OFFLINE_ACCESS_SCOPE).length===0)throw new Error(`Access token lacks granted capability scopes (requested: [${re.join(", ")}], granted: [${te.join(", ")}]). At least one permission scope besides offline_access must be granted.`);let Y=typeof f.organizationId=="string"?f.organizationId:null,X=p.expires_in?Date.now()+p.expires_in*1e3:typeof f.exp=="number"?f.exp*1e3:Date.now()+36e5,D={serverUrl:r,clientId:s,accessToken:p.access_token,refreshToken:p.refresh_token,tokenType:p.token_type||"Bearer",scope:p.scope||o,resource:n,organizationId:Y,expiresAt:X,updatedAt:Date.now()},Q=x(r,s);await c.save(Q,D);let ne={profileKey:Q,serverUrl:r,clientId:s,organizationId:Y,scope:D.scope,resource:D.resource,expiresAt:X};if(i?.json){let v={event:"login_success",success:!0,...ne};e(JSON.stringify(v))}else e(`
17
+ \u2705 Login successful!`),e(`- Profile: ${Q}`),e(`- Server: ${r}`),e(`- Client ID: ${s}`),e(`- Organization: ${Y||"(none / platform scope)"}`),e(`- Scopes: ${D.scope||"none"}`),e(`- Resource: ${n}`),e(`- Expires: ${new Date(X).toISOString()}
18
+ `);return ne}function Ee(i){if(i<=0)return"0s";let e=Math.floor(i/86400),t=Math.floor(i%86400/3600),r=Math.floor(i%3600/60),n=i%60,s=[];return e>0&&s.push(`${e}d`),t>0&&s.push(`${t}h`),r>0&&s.push(`${r}m`),(n>0||s.length===0)&&s.push(`${n}s`),s.join(" ")}async function Ue(i){let e=i?.logger||(a=>process.stdout.write(`${a}
19
+ `)),t=O(i?.server),r=(i?.clientId||R).trim(),n=x(t,r),o=await(i?.vault||new P).load(n);if(!o){i?.json?e(JSON.stringify({authenticated:!1,profileKey:n,serverUrl:t,clientId:r})):(e(`No active session found for profile: ${n}`),e(`Run \`iwas login --server ${t}\` to authenticate.
20
+ `));return}let c=M(o,60),l=Math.max(0,Math.round((o.expiresAt-Date.now())/1e3)),u=Ee(l),m={authenticated:!c,expired:c,profileKey:n,serverUrl:o.serverUrl,clientId:o.clientId,organizationId:o.organizationId,scope:o.scope,resource:o.resource,expiresAt:new Date(o.expiresAt).toISOString(),expiresInSeconds:l,expiresInFormatted:u};i?.json?e(JSON.stringify(m)):(e(`
21
+ ============================================================`),e(" IWAS MCP Auth Status "),e(`============================================================
22
+ `),e(`- Profile: ${n}`),e(`- Server: ${o.serverUrl}`),e(`- Client ID: ${o.clientId}`),e(`- Status: ${c?"\u{1F534} Expired":"\u{1F7E2} Active"}`),e(`- Organization: ${o.organizationId||"(none / platform scope)"}`),e(`- Scopes: ${o.scope||"none"}`),o.resource&&e(`- Resource: ${o.resource}`),e(`- Expires: ${m.expiresAt} (${u} remaining)
23
+ `))}var h=I(require("fs")),k=I(require("path")),W=I(require("os")),F=require("crypto"),J=class extends Error{constructor(e){super(e),this.name="VaultLockError"}},B=class extends J{lockPath;timeoutMs;constructor(e,t){super(`Timed out waiting to acquire credentials lock at "${e}" after ${t}ms.`),this.name="VaultLockTimeoutError",this.lockPath=e,this.timeoutMs=t}},ze=6e4,je=6e4,Ve=50;function Pe(i){if(typeof i!="number"||!Number.isInteger(i)||i<=0)return!1;try{return process.kill(i,0),!0}catch(e){let t=e?.code;return t==="ESRCH"?!1:t==="EPERM"}}var L=class{lockPath;isCustomLockPath;timeoutMs;staleTimeoutMs;corruptLockGraceMs;retryIntervalMs;isProcessAlive;currentLockId=null;currentLockIno;constructor(e){if(e?.lockPath)this.lockPath=e.lockPath,this.isCustomLockPath=!0;else{let t=e?.configDir||process.env.XDG_CONFIG_HOME||k.default.join(W.default.homedir(),".config"),r=e?.configDir?e.configDir:k.default.join(t,"iwas");this.lockPath=k.default.join(r,"credentials.lock"),this.isCustomLockPath=!!e?.configDir}this.timeoutMs=e?.timeoutMs??ze,this.staleTimeoutMs=e?.staleTimeoutMs??je,this.corruptLockGraceMs=e?.corruptLockGraceMs??Math.min(this.staleTimeoutMs,1e4),this.retryIntervalMs=e?.retryIntervalMs??Ve,this.isProcessAlive=e?.isProcessAliveFn??Pe}isHeld(){return this.currentLockId!==null}getCurrentLockId(){return this.currentLockId}async acquire(){if(this.currentLockId)return;let e=k.default.dirname(this.lockPath),t=!1;if(!h.default.existsSync(e))try{h.default.mkdirSync(e,{recursive:!0,mode:448}),t=!0}catch{}if(!this.isCustomLockPath||t)try{h.default.chmodSync(e,448)}catch{}let r=Date.now(),n=(0,F.randomUUID)(),s=this.retryIntervalMs;for(;;){let o=null,c;try{o=h.default.openSync(this.lockPath,"wx",384),c=h.default.fstatSync(o).ino;let d={pid:process.pid,createdAt:Date.now(),lockId:n,hostname:W.default.hostname()};h.default.writeFileSync(o,JSON.stringify(d),"utf8"),h.default.closeSync(o),o=null,this.currentLockId=n,this.currentLockIno=c;return}catch(a){if(o!==null){try{h.default.closeSync(o)}catch{}try{let p=h.default.statSync(this.lockPath);(c===void 0||p.ino===c)&&p.size===0&&h.default.unlinkSync(this.lockPath)}catch{}}if(a?.code!=="EEXIST")throw a}if(this.tryCleanStaleLock(),Date.now()-r>=this.timeoutMs)throw new B(this.lockPath,this.timeoutMs);let u=Math.floor(Math.random()*25),m=Math.min(s+u,250);s=Math.min(s*1.5,200),await new Promise(a=>setTimeout(a,m))}}async release(){this.releaseSync()}releaseSync(){if(!this.currentLockId)return;let e=this.currentLockId,t=this.currentLockIno;this.currentLockId=null,this.currentLockIno=void 0;let r=k.default.dirname(this.lockPath),n=k.default.join(r,`.credentials.lock.release.${Date.now()}.${(0,F.randomUUID)()}`),s=!1;try{if(!h.default.existsSync(this.lockPath))return;try{h.default.linkSync(this.lockPath,n),s=!0}catch{this.releaseSyncLoose(e,t);return}let o=h.default.statSync(n);if(t!==void 0&&o.ino!==t)return;let c=h.default.readFileSync(n,"utf8"),l=null;try{l=JSON.parse(c)}catch{return}if(l&&l.lockId===e&&l.pid===process.pid){let u=h.default.statSync(this.lockPath);if(u.ino===o.ino&&u.nlink>=2)try{h.default.unlinkSync(this.lockPath)}catch{}}}catch{}finally{if(s)try{h.default.unlinkSync(n)}catch{}}}releaseSyncLoose(e,t){try{if(!h.default.existsSync(this.lockPath))return;let r=h.default.statSync(this.lockPath);if(t!==void 0&&r.ino!==t)return;let n=h.default.readFileSync(this.lockPath,"utf8"),s=null;try{s=JSON.parse(n)}catch{return}if(s&&s.lockId===e&&s.pid===process.pid){let o=h.default.statSync(this.lockPath);if(o.ino===r.ino&&o.mtimeMs===r.mtimeMs)try{h.default.unlinkSync(this.lockPath)}catch{}}}catch{}}cleanOrphanGuards(){try{let e=k.default.dirname(this.lockPath);if(!h.default.existsSync(e))return;let r=`.${k.default.basename(this.lockPath)}.`,n=h.default.readdirSync(e),s=/^(release|stale|corrupt)\.(\d+)\.([0-9a-fA-F-]{36})$/,o=null;try{o=h.default.statSync(this.lockPath)}catch{}let c=Date.now();for(let l of n){if(!l.startsWith(r))continue;let u=s.exec(l.slice(r.length));if(!u)continue;let m=Number(u[2]),a=k.default.join(e,l);try{let d=h.default.statSync(a);if(!d.isFile()||d.nlink>1&&o&&d.ino===o.ino)continue;c-m>this.staleTimeoutMs&&h.default.unlinkSync(a)}catch{}}}catch{}}async withLock(e){await this.acquire();try{return await e()}finally{await this.release()}}tryCleanStaleLock(){try{if(this.cleanOrphanGuards(),!h.default.existsSync(this.lockPath))return;let e=h.default.statSync(this.lockPath),t=null;try{let o=h.default.readFileSync(this.lockPath,"utf8");t=JSON.parse(o)}catch{Date.now()-e.mtimeMs>this.corruptLockGraceMs&&this.reclaimCorruptLock(e);return}if(!t||typeof t.createdAt!="number")return;let r=W.default.hostname(),n=!t.hostname||t.hostname===r,s=!1;n?s=!(typeof t.pid=="number"&&this.isProcessAlive(t.pid)):s=Date.now()-t.createdAt>this.staleTimeoutMs,s&&this.reclaimStaleLock(t.lockId,e)}catch{}}reclaimStaleLock(e,t){let r=k.default.dirname(this.lockPath),n=k.default.join(r,`.credentials.lock.stale.${Date.now()}.${(0,F.randomUUID)()}`);try{h.default.linkSync(this.lockPath,n);try{let s=h.default.readFileSync(n,"utf8");if(JSON.parse(s).lockId!==e)return;let c=h.default.statSync(this.lockPath);c.ino===t.ino&&c.nlink>=2&&h.default.unlinkSync(this.lockPath)}finally{try{h.default.unlinkSync(n)}catch{}}}catch{}}reclaimCorruptLock(e){let t=k.default.dirname(this.lockPath),r=k.default.join(t,`.credentials.lock.corrupt.${Date.now()}.${(0,F.randomUUID)()}`);try{h.default.linkSync(this.lockPath,r);try{let n=h.default.statSync(r);if(n.ino!==e.ino||n.size!==e.size||n.mtimeMs!==e.mtimeMs)return;try{let o=h.default.readFileSync(r,"utf8");JSON.parse(o);return}catch{}let s=h.default.statSync(this.lockPath);s.ino===e.ino&&s.nlink>=2&&h.default.unlinkSync(this.lockPath)}finally{try{h.default.unlinkSync(r)}catch{}}}catch{}}};async function qe(i,e,t){return await new L({lockPath:i,...t}).withLock(e)}var Ce=I(require("path"));var _=class extends Error{profileKey;reason;terminalError;constructor(e,t,r){let n=r?` (${r})`:"";super(`Re-authentication required for profile "${e}": ${t}${n}`),this.name="ReauthenticationRequiredError",this.profileKey=e,this.reason=t,this.terminalError=r}},We=60,Je=6e4,Be=6e4,ee=class{vault;safetyMarginSeconds;lockTimeoutMs;staleLockTimeoutMs;lockPath;fetchFn;logger;errorLogger;inFlightRefreshes=new Map;constructor(e){this.vault=e?.vault||new P,this.safetyMarginSeconds=e?.safetyMarginSeconds??We,this.lockTimeoutMs=e?.lockTimeoutMs??Je,this.staleLockTimeoutMs=e?.staleLockTimeoutMs??e?.staleTimeoutMs??Be,this.lockPath=e?.lockPath,this.fetchFn=e?.fetchFn,this.logger=e?.logger,this.errorLogger=e?.errorLogger}async getValidAccessToken(e){return(await this.getValidCredentials(e)).accessToken}async getValidCredentials(e){let t=await this.vault.load(e);if(!t)throw new _(e,"No credentials found in vault.");return M(t,this.safetyMarginSeconds)?await this.refreshWithSingleFlight(e,!1):t}async refresh(e){let t=this.inFlightRefreshes.get(e);return t?await t:await this.refreshWithSingleFlight(e,!0)}async refreshWithSingleFlight(e,t){let r=this.inFlightRefreshes.get(e);if(r&&!t)return await r;let n=this.executeRefreshWithLock(e,t);this.inFlightRefreshes.set(e,n);try{return await n}finally{this.inFlightRefreshes.delete(e)}}async executeRefreshWithLock(e,t){let r=this.lockPath||Ce.default.join(this.vault.getConfigDir(),"credentials.lock");return await new L({lockPath:r,timeoutMs:this.lockTimeoutMs,staleTimeoutMs:this.staleLockTimeoutMs}).withLock(async()=>{let s=await this.vault.load(e);if(!s)throw new _(e,"Credentials missing from vault.");return!t&&!M(s,this.safetyMarginSeconds)?(this.logger?.(`Token for profile "${e}" was already refreshed by another process.`),s):await this.performTokenExchange(e,s)})}async performTokenExchange(e,t){if(!t.refreshToken)throw new _(e,"Cannot refresh access token: No refresh token stored in credentials.");let r=`${t.serverUrl}/api/auth/oauth2/token`,n=new URLSearchParams;n.set("grant_type","refresh_token"),n.set("refresh_token",t.refreshToken),n.set("client_id",t.clientId),t.resource&&n.set("resource",t.resource);let s;try{let a=await C(r,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:n,retries:1,fetchFn:this.fetchFn});if(!a.ok)throw new T(`Token refresh request failed with HTTP ${a.status}`,a.status,a.data);s=a.data}catch(a){if(a instanceof T){let d=a.data&&typeof a.data=="object"?a.data:{},p=typeof d.error=="string"?d.error:"",f=typeof d.error_description=="string"?d.error_description:a.message,y=p==="invalid_grant"||p==="invalid_client",S=E(f,[t.refreshToken,t.accessToken]);if(this.errorLogger?.(`Token refresh rejected (${a.status}, ${p||"unknown"}): ${S}`),y){try{await this.vault.delete(e)}catch{}throw new _(e,`Refresh token rejected by authorization server: ${S}`,p)}throw new T(`Token refresh rejected (${a.status}): ${S}`,a.status,a.data)}throw this.errorLogger?.(`Token refresh network/transient error for profile "${e}":
24
+ ${a instanceof Error?a.message:String(a)}`),a}if(!s||!s.access_token){let a=s?.error||"unknown_error",d=s?.error_description||"Missing access_token in token response.",p=E(d,[t.refreshToken,t.accessToken]);if(this.errorLogger?.(`Token response invalid for profile "${e}": ${p}`),a==="invalid_grant"||a==="invalid_client"){try{await this.vault.delete(e)}catch{}throw new _(e,p,a)}throw new Error(`Token refresh failed: ${p}`)}let o=q(s.access_token),c;"organizationId"in o?typeof o.organizationId=="string"?c=o.organizationId:o.organizationId===null?c=null:c=t.organizationId??null:c=t.organizationId??null;let l=s.expires_in?Date.now()+s.expires_in*1e3:typeof o.exp=="number"?o.exp*1e3:Date.now()+3600*1e3,u=s.refresh_token||t.refreshToken,m={serverUrl:t.serverUrl,clientId:t.clientId,accessToken:s.access_token,refreshToken:u,tokenType:s.token_type||t.tokenType||"Bearer",scope:s.scope||t.scope,resource:t.resource,organizationId:c,expiresAt:l,updatedAt:Date.now()};try{await this.vault.save(e,m)}catch(a){try{await this.vault.delete(e)}catch{}let d=a instanceof Error?a.message:String(a);throw this.errorLogger?.(`Failed to persist refreshed credentials for profile "${e}": ${d}`),new _(e,`Failed to persist refreshed credentials to vault: ${d}`,"persistence_failure")}return this.logger?.(`Successfully refreshed credentials for profile "${e}"
25
+ (expires: ${new Date(l).toISOString()}).`),m}};0&&(module.exports={CredentialVault,DEFAULT_CLI_CLIENT_ID,DEFAULT_KEYCHAIN_SERVICE,DEFAULT_READ_ONLY_SCOPES,DEFAULT_SCOPES,DEFAULT_SERVER_URL,DEVICE_CODE_GRANT_TYPE,DefaultKeychainRunner,HttpError,KeychainService,ReauthenticationRequiredError,TokenRefreshManager,VaultLock,VaultLockError,VaultLockTimeoutError,decodeJwtPayload,defaultIsProcessAlive,executeLogin,executeLogout,executeStatus,formatRemainingDuration,isCredentialExpired,isLocalhost,isValidStoredCredentials,normalizeProfileKey,openBrowser,pollDeviceToken,redactCredentials,requestDeviceCode,resolveAuthServerUrl,resolveMcpResourceUrl,sanitizeSecretString,sendRequest,validateScopes,withVaultLock});