@iwas/mcp 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js ADDED
@@ -0,0 +1,1833 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __commonJS = (cb, mod) => function __require() {
10
+ try {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ } catch (e) {
13
+ throw mod = 0, e;
14
+ }
15
+ };
16
+ var __export = (target, all) => {
17
+ for (var name in all)
18
+ __defProp(target, name, { get: all[name], enumerable: true });
19
+ };
20
+ var __copyProps = (to, from, except, desc) => {
21
+ if (from && typeof from === "object" || typeof from === "function") {
22
+ for (let key of __getOwnPropNames(from))
23
+ if (!__hasOwnProp.call(to, key) && key !== except)
24
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
25
+ }
26
+ return to;
27
+ };
28
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
29
+ // If the importer is in node compatibility mode or this is not an ESM
30
+ // file that has been converted to a CommonJS file using a Babel-
31
+ // compatible transform (i.e. "__esModule" has not been set), then set
32
+ // "default" to the CommonJS "module.exports" for node compatibility.
33
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
34
+ mod
35
+ ));
36
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
+
38
+ // ../auth/dist/rbac/permissions.js
39
+ var require_permissions = __commonJS({
40
+ "../auth/dist/rbac/permissions.js"(exports2) {
41
+ "use strict";
42
+ Object.defineProperty(exports2, "__esModule", { value: true });
43
+ 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;
44
+ exports2.isPermissionString = isPermissionString;
45
+ exports2.parsePermissionString = parsePermissionString;
46
+ exports2.isSensitiveMcpResource = isSensitiveMcpResource;
47
+ exports2.isMcpEligiblePermission = isMcpEligiblePermission;
48
+ exports2.partitionMcpScopes = partitionMcpScopes;
49
+ exports2.CATALOG_DEFINITIONS = {
50
+ organization: {
51
+ tier: "T",
52
+ actions: ["update", "delete"],
53
+ description: "Organization tenant profile and settings",
54
+ models: ["Organization", "OrganizationSetting", "OrganizationSubscription"],
55
+ mcpEligible: true
56
+ },
57
+ member: {
58
+ tier: "T",
59
+ actions: ["create", "update", "delete"],
60
+ description: "Organization member management",
61
+ models: ["Member"],
62
+ mcpEligible: true
63
+ },
64
+ invitation: {
65
+ tier: "T",
66
+ actions: ["create", "cancel"],
67
+ description: "Organization membership invitations",
68
+ models: ["Invitation"],
69
+ mcpEligible: true
70
+ },
71
+ location: {
72
+ tier: "T",
73
+ actions: ["read", "write", "delete"],
74
+ description: "Physical hotspot location management",
75
+ models: ["Location"],
76
+ mcpEligible: true
77
+ },
78
+ router: {
79
+ tier: "T",
80
+ actions: ["read", "write", "delete", "control"],
81
+ description: "Gateway router inventory, provisioning and control",
82
+ models: ["Router", "ZtpToken", "CoaQueue", "IpamAllocationCursor"],
83
+ mcpEligible: true
84
+ },
85
+ package: {
86
+ tier: "S",
87
+ actions: ["read", "write", "delete", "publish"],
88
+ description: "WiFi access package catalog (tenant + shared)",
89
+ models: ["Package", "VoucherPackage"],
90
+ mcpEligible: true
91
+ },
92
+ voucher: {
93
+ tier: "S",
94
+ actions: ["read", "write", "publish", "issue", "revoke"],
95
+ description: "Prepaid voucher codes and issuance (tenant + shared)",
96
+ models: ["Voucher", "VoucherIssuance", "VoucherUser"],
97
+ mcpEligible: true
98
+ },
99
+ session: {
100
+ tier: "T",
101
+ actions: ["read", "control"],
102
+ description: "Active and historical WiFi guest sessions",
103
+ models: ["WifiSession", "RadiusAccounting", "DeviceToken"],
104
+ mcpEligible: true
105
+ },
106
+ wallet: {
107
+ tier: "T",
108
+ actions: ["read", "topup", "charge", "refund", "freeze"],
109
+ description: "Prepaid wallet balances, topups and ledger transactions (SENSITIVE - MCP EXCLUDED)",
110
+ models: ["Wallet", "WalletTransaction", "TopupRequest"],
111
+ hasOwnQualifier: true,
112
+ mcpEligible: false
113
+ },
114
+ payment: {
115
+ tier: "T",
116
+ actions: ["read", "approve", "refund"],
117
+ description: "Inbound payment transactions and gateway integrations (SENSITIVE - MCP EXCLUDED)",
118
+ models: ["PaymentTransaction", "PaymentGatewayIntegration"],
119
+ hasOwnQualifier: true,
120
+ mcpEligible: false
121
+ },
122
+ subscription: {
123
+ tier: "T",
124
+ actions: ["read", "manage"],
125
+ description: "SaaS organization plan subscription and entitlements",
126
+ models: ["OrganizationSubscription"],
127
+ mcpEligible: true
128
+ },
129
+ partner: {
130
+ tier: "T",
131
+ actions: ["read", "manage", "payout:approve"],
132
+ description: "Affiliate/partner programs and payout requests",
133
+ models: ["Partner", "PartnerPayoutRequest"],
134
+ mcpEligible: true
135
+ },
136
+ campaign: {
137
+ tier: "T",
138
+ actions: ["read", "write"],
139
+ description: "Captive portal ad campaigns and lead capture",
140
+ models: ["AdCampaign", "LeadCapture"],
141
+ mcpEligible: true
142
+ },
143
+ notification: {
144
+ tier: "T",
145
+ actions: ["read", "send", "template:write"],
146
+ description: "Multi-channel notifications, templates and logs",
147
+ models: ["NotificationItem", "NotificationTemplate", "DeliveryLog"],
148
+ hasOwnQualifier: true,
149
+ mcpEligible: true
150
+ },
151
+ store: {
152
+ tier: "P",
153
+ actions: ["read", "write"],
154
+ description: "Public hardware/affiliate store catalog",
155
+ models: ["StoreProduct", "AffiliateClick"],
156
+ mcpEligible: true
157
+ },
158
+ blog: {
159
+ tier: "P",
160
+ actions: ["read", "write", "publish"],
161
+ description: "Company-wide editorial articles and CDN media",
162
+ models: ["BlogPost", "Media"],
163
+ hasOwnQualifier: true,
164
+ mcpEligible: true
165
+ },
166
+ analytics: {
167
+ tier: "D",
168
+ actions: ["read"],
169
+ description: "Aggregated usage, session and revenue metrics",
170
+ models: ["WifiSession", "Package", "PaymentTransaction"],
171
+ mcpEligible: true
172
+ },
173
+ diagnostics: {
174
+ tier: "P",
175
+ actions: ["read", "write"],
176
+ description: "System error events, MCP tool execution spans, flow events, issue triage and audit logs",
177
+ models: ["ErrorEvent", "McpToolSpan", "AuditLog", "SystemSetting", "FlowEvent", "IssueState"],
178
+ mcpEligible: true
179
+ }
180
+ };
181
+ exports2.ALL_PERMISSIONS = [
182
+ "organization:update",
183
+ "organization:delete",
184
+ "member:create",
185
+ "member:update",
186
+ "member:delete",
187
+ "invitation:create",
188
+ "invitation:cancel",
189
+ "location:read",
190
+ "location:write",
191
+ "location:delete",
192
+ "router:read",
193
+ "router:write",
194
+ "router:delete",
195
+ "router:control",
196
+ "package:read",
197
+ "package:write",
198
+ "package:delete",
199
+ "package:publish",
200
+ "voucher:read",
201
+ "voucher:write",
202
+ "voucher:publish",
203
+ "voucher:issue",
204
+ "voucher:revoke",
205
+ "session:read",
206
+ "session:control",
207
+ "wallet:read",
208
+ "wallet:topup",
209
+ "wallet:charge",
210
+ "wallet:refund",
211
+ "wallet:freeze",
212
+ "payment:read",
213
+ "payment:approve",
214
+ "payment:refund",
215
+ "subscription:read",
216
+ "subscription:manage",
217
+ "partner:read",
218
+ "partner:manage",
219
+ "partner:payout:approve",
220
+ "campaign:read",
221
+ "campaign:write",
222
+ "notification:read",
223
+ "notification:send",
224
+ "notification:template:write",
225
+ "store:read",
226
+ "store:write",
227
+ "blog:read",
228
+ "blog:write",
229
+ "blog:publish",
230
+ "analytics:read",
231
+ "diagnostics:read",
232
+ "diagnostics:write"
233
+ ];
234
+ exports2.PERMISSION_SET = new Set(exports2.ALL_PERMISSIONS);
235
+ function isPermissionString(value) {
236
+ return typeof value === "string" && exports2.PERMISSION_SET.has(value);
237
+ }
238
+ function parsePermissionString(value) {
239
+ if (!isPermissionString(value)) {
240
+ return null;
241
+ }
242
+ const colonIndex = value.indexOf(":");
243
+ const resource = value.slice(0, colonIndex);
244
+ const action = value.slice(colonIndex + 1);
245
+ return { resource, action };
246
+ }
247
+ exports2.SENSITIVE_MCP_RESOURCES = ["payment", "wallet"];
248
+ function isSensitiveMcpResource(resource) {
249
+ return resource === "payment" || resource === "wallet";
250
+ }
251
+ function isMcpEligiblePermission(permission) {
252
+ const parsed = parsePermissionString(permission);
253
+ if (!parsed)
254
+ return false;
255
+ return !isSensitiveMcpResource(parsed.resource);
256
+ }
257
+ exports2.MCP_ELIGIBLE_PERMISSIONS = exports2.ALL_PERMISSIONS.filter(isMcpEligiblePermission);
258
+ exports2.MCP_ELIGIBLE_PERMISSION_SET = new Set(exports2.MCP_ELIGIBLE_PERMISSIONS);
259
+ exports2.OFFLINE_ACCESS_SCOPE = "offline_access";
260
+ exports2.MCP_OAUTH_SCOPES = [
261
+ ...exports2.MCP_ELIGIBLE_PERMISSIONS,
262
+ exports2.OFFLINE_ACCESS_SCOPE
263
+ ];
264
+ function partitionMcpScopes(scopes) {
265
+ return {
266
+ permissionScopes: scopes.filter((scope) => scope !== exports2.OFFLINE_ACCESS_SCOPE),
267
+ offlineAccessRequested: scopes.includes(exports2.OFFLINE_ACCESS_SCOPE)
268
+ };
269
+ }
270
+ }
271
+ });
272
+
273
+ // src/bin.ts
274
+ var bin_exports = {};
275
+ __export(bin_exports, {
276
+ CLI_NAME: () => CLI_NAME,
277
+ CLI_VERSION: () => CLI_VERSION,
278
+ printHelp: () => printHelp,
279
+ runCli: () => runCli
280
+ });
281
+ module.exports = __toCommonJS(bin_exports);
282
+ var import_node_util = require("util");
283
+
284
+ // src/auth/device-flow.ts
285
+ var import_node_child_process2 = require("child_process");
286
+ var import_permissions = __toESM(require_permissions());
287
+
288
+ // src/auth/http.ts
289
+ var HttpError = class extends Error {
290
+ status;
291
+ data;
292
+ constructor(message, status, data) {
293
+ super(message);
294
+ this.name = "HttpError";
295
+ this.status = status;
296
+ this.data = data;
297
+ }
298
+ };
299
+ var DEFAULT_TIMEOUT_MS = 15e3;
300
+ var DEFAULT_RETRIES = 2;
301
+ async function sendRequest(url, options) {
302
+ const method = options?.method || "GET";
303
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
304
+ const maxRetries = options?.retries ?? DEFAULT_RETRIES;
305
+ const headers = {
306
+ Accept: "application/json",
307
+ ...options?.headers || {}
308
+ };
309
+ let serializedBody;
310
+ if (options?.body) {
311
+ if (typeof options.body === "string") {
312
+ serializedBody = options.body;
313
+ } else if (options.body instanceof URLSearchParams) {
314
+ serializedBody = options.body.toString();
315
+ if (!headers["Content-Type"]) {
316
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
317
+ }
318
+ } else {
319
+ serializedBody = JSON.stringify(options.body);
320
+ if (!headers["Content-Type"]) {
321
+ headers["Content-Type"] = "application/json";
322
+ }
323
+ }
324
+ }
325
+ let attempt = 0;
326
+ let lastError = null;
327
+ while (attempt <= maxRetries) {
328
+ attempt++;
329
+ const controller = new AbortController();
330
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
331
+ try {
332
+ const fetchImpl = options?.fetchFn || fetch;
333
+ const res = await fetchImpl(url, {
334
+ method,
335
+ headers,
336
+ body: serializedBody,
337
+ signal: controller.signal
338
+ });
339
+ clearTimeout(timer);
340
+ const contentType = res.headers.get("content-type") || "";
341
+ let parsedData;
342
+ if (contentType.includes("application/json")) {
343
+ try {
344
+ parsedData = await res.json();
345
+ } catch {
346
+ parsedData = null;
347
+ }
348
+ } else {
349
+ parsedData = await res.text();
350
+ }
351
+ if (res.status >= 500 && res.status <= 504 && attempt <= maxRetries) {
352
+ const backoffMs = Math.min(1e3 * Math.pow(2, attempt - 1) + Math.random() * 200, 5e3);
353
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
354
+ continue;
355
+ }
356
+ return {
357
+ status: res.status,
358
+ ok: res.ok,
359
+ headers: res.headers,
360
+ data: parsedData
361
+ };
362
+ } catch (err) {
363
+ clearTimeout(timer);
364
+ const isAbort = err instanceof Error && err.name === "AbortError";
365
+ const errMsg = isAbort ? `Request timed out after ${timeoutMs}ms` : err instanceof Error ? err.message : String(err);
366
+ lastError = new Error(`Network request failed for ${method} ${url}: ${errMsg}`);
367
+ if (attempt <= maxRetries) {
368
+ const backoffMs = Math.min(500 * Math.pow(2, attempt - 1) + Math.random() * 200, 3e3);
369
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
370
+ continue;
371
+ }
372
+ }
373
+ }
374
+ throw lastError || new Error(`Network request failed after ${attempt} attempts`);
375
+ }
376
+
377
+ // src/auth/credentials-vault.ts
378
+ var import_node_fs = __toESM(require("fs"));
379
+ var import_node_path = __toESM(require("path"));
380
+ var import_os = __toESM(require("os"));
381
+
382
+ // src/auth/types.ts
383
+ function isValidStoredCredentials(obj) {
384
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
385
+ return false;
386
+ }
387
+ const record = obj;
388
+ 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)) {
389
+ return false;
390
+ }
391
+ if (!("organizationId" in record) || record.organizationId !== null && typeof record.organizationId !== "string") {
392
+ return false;
393
+ }
394
+ if (record.refreshToken !== void 0 && typeof record.refreshToken !== "string") {
395
+ return false;
396
+ }
397
+ if (record.scope !== void 0 && typeof record.scope !== "string") {
398
+ return false;
399
+ }
400
+ if (record.resource !== void 0 && typeof record.resource !== "string") {
401
+ return false;
402
+ }
403
+ if (record.updatedAt !== void 0 && (typeof record.updatedAt !== "number" || !Number.isFinite(record.updatedAt))) {
404
+ return false;
405
+ }
406
+ return true;
407
+ }
408
+ function normalizeProfileKey(serverUrl, clientId) {
409
+ const cleanClient = (clientId || "").trim();
410
+ let urlToParse = (serverUrl || "").trim();
411
+ if (!/^https?:\/\//i.test(urlToParse)) {
412
+ urlToParse = `https://${urlToParse}`;
413
+ }
414
+ try {
415
+ const parsed = new URL(urlToParse);
416
+ const origin = parsed.origin.toLowerCase();
417
+ return `${origin}::${cleanClient}`;
418
+ } catch {
419
+ const cleanUrl = urlToParse.replace(/\/+$/, "").toLowerCase();
420
+ return `${cleanUrl}::${cleanClient}`;
421
+ }
422
+ }
423
+ function sanitizeSecretString(input, secrets) {
424
+ let sanitized = input;
425
+ for (const secret of secrets) {
426
+ if (secret && typeof secret === "string" && secret.length >= 4) {
427
+ sanitized = sanitized.split(secret).join("[REDACTED]");
428
+ }
429
+ }
430
+ return sanitized;
431
+ }
432
+ function isCredentialExpired(creds, skewSeconds = 60) {
433
+ if (!creds.expiresAt) {
434
+ return false;
435
+ }
436
+ const expiresAtMs = creds.expiresAt > 1e11 ? creds.expiresAt : creds.expiresAt * 1e3;
437
+ return Date.now() + skewSeconds * 1e3 >= expiresAtMs;
438
+ }
439
+
440
+ // src/auth/keychain.ts
441
+ var import_node_child_process = require("child_process");
442
+ var DefaultKeychainRunner = class {
443
+ defaultTimeoutMs;
444
+ constructor(defaultTimeoutMs = 5e3) {
445
+ this.defaultTimeoutMs = defaultTimeoutMs;
446
+ }
447
+ spawn(command, args, options) {
448
+ return new Promise((resolve) => {
449
+ let child;
450
+ try {
451
+ child = (0, import_node_child_process.spawn)(command, args, {
452
+ shell: false,
453
+ stdio: ["pipe", "pipe", "pipe"]
454
+ });
455
+ } catch (err) {
456
+ const message = err instanceof Error ? err.message : String(err);
457
+ return resolve({ stdout: "", stderr: message, exitCode: -1 });
458
+ }
459
+ let stdout = "";
460
+ let stderr = "";
461
+ let isSettled = false;
462
+ const timeoutMs = options?.timeoutMs ?? this.defaultTimeoutMs;
463
+ const timer = setTimeout(() => {
464
+ if (!isSettled) {
465
+ isSettled = true;
466
+ try {
467
+ child.kill("SIGKILL");
468
+ } catch {
469
+ }
470
+ resolve({
471
+ stdout,
472
+ stderr: `Keychain operation timed out after ${timeoutMs}ms`,
473
+ exitCode: -1
474
+ });
475
+ }
476
+ }, timeoutMs);
477
+ child.stdout?.on("data", (chunk) => {
478
+ stdout += chunk.toString();
479
+ });
480
+ child.stderr?.on("data", (chunk) => {
481
+ stderr += chunk.toString();
482
+ });
483
+ child.on("error", (err) => {
484
+ stderr += err.message;
485
+ });
486
+ child.on("close", (code) => {
487
+ if (!isSettled) {
488
+ isSettled = true;
489
+ clearTimeout(timer);
490
+ resolve({
491
+ stdout,
492
+ stderr,
493
+ exitCode: code ?? -1
494
+ });
495
+ }
496
+ });
497
+ if (options?.input !== void 0 && child.stdin) {
498
+ try {
499
+ child.stdin.write(options.input);
500
+ child.stdin.end();
501
+ } catch {
502
+ }
503
+ }
504
+ });
505
+ }
506
+ };
507
+ var DEFAULT_KEYCHAIN_SERVICE = "iwas-cli";
508
+ var KeychainService = class {
509
+ service;
510
+ runner;
511
+ platform;
512
+ constructor(options) {
513
+ this.service = options?.serviceName || DEFAULT_KEYCHAIN_SERVICE;
514
+ this.runner = options?.runner || new DefaultKeychainRunner();
515
+ this.platform = options?.platform || process.platform;
516
+ }
517
+ availableCache = null;
518
+ /**
519
+ * Returns the configured OS platform.
520
+ */
521
+ getPlatform() {
522
+ return this.platform;
523
+ }
524
+ /**
525
+ * Probes whether the host environment has an operable keychain utility.
526
+ * Results are cached when available to avoid unnecessary subprocess spawns (n7 fix).
527
+ */
528
+ async isAvailable(forceProbe = false) {
529
+ if (!forceProbe && this.availableCache === true) {
530
+ return true;
531
+ }
532
+ if (this.platform === "darwin") {
533
+ const res = await this.runner.spawn("/usr/bin/security", ["help"]);
534
+ const isAvail = res.exitCode === 0 || res.stdout.includes("security") || res.stderr.includes("security");
535
+ if (isAvail) {
536
+ this.availableCache = true;
537
+ }
538
+ return isAvail;
539
+ }
540
+ if (this.platform === "linux") {
541
+ const res = await this.runner.spawn("which", ["secret-tool"]);
542
+ const isAvail = res.exitCode === 0;
543
+ if (isAvail) {
544
+ this.availableCache = true;
545
+ }
546
+ return isAvail;
547
+ }
548
+ return false;
549
+ }
550
+ /**
551
+ * Clears the cached isAvailable probe result. Useful for testing and environment shifts.
552
+ */
553
+ clearAvailabilityCache() {
554
+ this.availableCache = null;
555
+ }
556
+ /**
557
+ * Retrieves a secret password string by account identifier.
558
+ */
559
+ async getSecret(account) {
560
+ if (this.platform === "darwin") {
561
+ const res = await this.runner.spawn("/usr/bin/security", [
562
+ "find-generic-password",
563
+ "-s",
564
+ this.service,
565
+ "-a",
566
+ account,
567
+ "-w"
568
+ ]);
569
+ if (res.exitCode === 0) {
570
+ const raw = res.stdout.trim();
571
+ if (raw.length > 0 && raw.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(raw)) {
572
+ if (raw.startsWith("7b") || raw.startsWith("7B") || raw.startsWith("5b") || raw.startsWith("5B")) {
573
+ try {
574
+ const decoded = Buffer.from(raw, "hex").toString("utf8");
575
+ if (!decoded.includes("\uFFFD")) {
576
+ return decoded;
577
+ }
578
+ } catch {
579
+ }
580
+ }
581
+ }
582
+ return raw;
583
+ }
584
+ if (res.exitCode === 44 || res.stderr.includes("could not be found") || res.stderr.includes("The specified item could not be found")) {
585
+ return null;
586
+ }
587
+ throw new Error(`Keychain read failed: ${res.stderr.trim() || `exit code ${res.exitCode}`}`);
588
+ }
589
+ if (this.platform === "linux") {
590
+ const res = await this.runner.spawn("secret-tool", [
591
+ "lookup",
592
+ "service",
593
+ this.service,
594
+ "account",
595
+ account
596
+ ]);
597
+ if (res.exitCode === 0) {
598
+ return res.stdout.length > 0 ? res.stdout.trim() : null;
599
+ }
600
+ const err = res.stderr.trim();
601
+ if (res.exitCode === 1 && (err === "" || err.toLowerCase().includes("not found") || err.toLowerCase().includes("no such secret"))) {
602
+ return null;
603
+ }
604
+ throw new Error(`Secret-tool read failed: ${err || `exit code ${res.exitCode}`}`);
605
+ }
606
+ throw new Error(`OS keychain not supported on platform: ${this.platform}`);
607
+ }
608
+ /**
609
+ * Stores a secret password under the given account identifier.
610
+ */
611
+ async setSecret(account, secret, label) {
612
+ const itemLabel = label || `${this.service} (${account})`;
613
+ const secretsToRedact = [secret];
614
+ try {
615
+ const parsed = JSON.parse(secret);
616
+ if (typeof parsed === "object" && parsed !== null) {
617
+ for (const val of Object.values(parsed)) {
618
+ if (typeof val === "string" && val.length >= 4) {
619
+ secretsToRedact.push(val);
620
+ }
621
+ }
622
+ }
623
+ } catch {
624
+ }
625
+ if (this.platform === "darwin") {
626
+ const res = await this.runner.spawn("/usr/bin/security", [
627
+ "add-generic-password",
628
+ "-U",
629
+ "-s",
630
+ this.service,
631
+ "-a",
632
+ account,
633
+ "-l",
634
+ itemLabel,
635
+ "-w",
636
+ secret
637
+ ]);
638
+ if (res.exitCode !== 0) {
639
+ const errorMsg = sanitizeSecretString(res.stderr.trim(), secretsToRedact);
640
+ throw new Error(`Keychain write failed: ${errorMsg || `exit code ${res.exitCode}`}`);
641
+ }
642
+ return;
643
+ }
644
+ if (this.platform === "linux") {
645
+ const res = await this.runner.spawn(
646
+ "secret-tool",
647
+ ["store", `--label=${itemLabel}`, "service", this.service, "account", account],
648
+ { input: secret }
649
+ );
650
+ if (res.exitCode !== 0) {
651
+ const errorMsg = sanitizeSecretString(res.stderr.trim(), secretsToRedact);
652
+ throw new Error(`Secret-tool write failed: ${errorMsg || `exit code ${res.exitCode}`}`);
653
+ }
654
+ return;
655
+ }
656
+ throw new Error(`OS keychain not supported on platform: ${this.platform}`);
657
+ }
658
+ /**
659
+ * Deletes a secret from the keychain. Operation is strictly idempotent.
660
+ */
661
+ async deleteSecret(account) {
662
+ if (this.platform === "darwin") {
663
+ const res = await this.runner.spawn("/usr/bin/security", [
664
+ "delete-generic-password",
665
+ "-s",
666
+ this.service,
667
+ "-a",
668
+ account
669
+ ]);
670
+ if (res.exitCode === 0 || res.exitCode === 44 || res.stderr.includes("could not be found")) {
671
+ return;
672
+ }
673
+ throw new Error(`Keychain delete failed: ${res.stderr.trim() || `exit code ${res.exitCode}`}`);
674
+ }
675
+ if (this.platform === "linux") {
676
+ const res = await this.runner.spawn("secret-tool", [
677
+ "clear",
678
+ "service",
679
+ this.service,
680
+ "account",
681
+ account
682
+ ]);
683
+ if (res.exitCode === 0 || res.exitCode === 1) {
684
+ return;
685
+ }
686
+ throw new Error(`Secret-tool clear failed: ${res.stderr.trim() || `exit code ${res.exitCode}`}`);
687
+ }
688
+ throw new Error(`OS keychain not supported on platform: ${this.platform}`);
689
+ }
690
+ };
691
+
692
+ // src/auth/credentials-vault.ts
693
+ var KEYCHAIN_PROFILES_INDEX_ACCOUNT = "__profiles__";
694
+ var CredentialVault = class {
695
+ vaultType;
696
+ configDir;
697
+ credentialsFilePath;
698
+ keychain;
699
+ constructor(options) {
700
+ const rawEnvVault = process.env.IWAS_MCP_VAULT;
701
+ if (rawEnvVault && !["auto", "keychain", "file"].includes(rawEnvVault)) {
702
+ throw new Error(
703
+ `Invalid IWAS_MCP_VAULT value: "${rawEnvVault}". Supported values are "auto", "keychain", or "file".`
704
+ );
705
+ }
706
+ const envVault = rawEnvVault;
707
+ this.vaultType = options?.vaultType || envVault || "auto";
708
+ const baseConfigDir = options?.configDir || process.env.XDG_CONFIG_HOME || import_node_path.default.join(import_os.default.homedir(), ".config");
709
+ this.configDir = options?.configDir ? options.configDir : import_node_path.default.join(baseConfigDir, "iwas");
710
+ this.credentialsFilePath = import_node_path.default.join(this.configDir, "credentials.json");
711
+ this.keychain = options?.keychainService || new KeychainService({
712
+ serviceName: options?.serviceName || DEFAULT_KEYCHAIN_SERVICE,
713
+ runner: options?.keychainRunner
714
+ });
715
+ }
716
+ /**
717
+ * Returns the configuration directory for this vault (~/.config/iwas).
718
+ */
719
+ getConfigDir() {
720
+ return this.configDir;
721
+ }
722
+ /**
723
+ * Returns the credentials file path (~/.config/iwas/credentials.json).
724
+ */
725
+ getCredentialsFilePath() {
726
+ return this.credentialsFilePath;
727
+ }
728
+ /**
729
+ * Returns the resolved vault mode: 'keychain' or 'file'.
730
+ * In strict 'keychain' mode, throws if OS keychain is unavailable.
731
+ */
732
+ async getEffectiveBackend() {
733
+ if (this.vaultType === "file") {
734
+ return "file";
735
+ }
736
+ const available = await this.keychain.isAvailable().catch(() => false);
737
+ const platform = this.keychain.getPlatform();
738
+ if (this.vaultType === "keychain") {
739
+ if (!available) {
740
+ throw new Error(
741
+ `Keychain vault requested (IWAS_MCP_VAULT="keychain") but OS keychain is unavailable on platform: ${platform}`
742
+ );
743
+ }
744
+ return "keychain";
745
+ }
746
+ return available ? "keychain" : "file";
747
+ }
748
+ /**
749
+ * Retrieves stored credentials by profile key.
750
+ * In auto mode, checks keychain first; falls back to file if missing or failed.
751
+ * Compares timestamps if both exist and migrates newer file credentials to keychain (Medium fix).
752
+ */
753
+ async load(profileKey) {
754
+ const backend = await this.getEffectiveBackend();
755
+ if (backend === "keychain") {
756
+ let kcCreds = null;
757
+ try {
758
+ const raw = await this.keychain.getSecret(profileKey);
759
+ if (raw) {
760
+ try {
761
+ const parsed = JSON.parse(raw);
762
+ if (isValidStoredCredentials(parsed)) {
763
+ kcCreds = parsed;
764
+ } else if (this.vaultType === "keychain") {
765
+ throw new Error(`Corrupt credentials stored in keychain for profile: ${profileKey}`);
766
+ }
767
+ } catch (parseErr) {
768
+ if (this.vaultType === "keychain") {
769
+ const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
770
+ throw new Error(`Corrupt credentials JSON in keychain for profile ${profileKey}: ${msg}`);
771
+ }
772
+ }
773
+ }
774
+ } catch (err) {
775
+ if (this.vaultType === "keychain") {
776
+ throw err;
777
+ }
778
+ }
779
+ if (this.vaultType === "keychain") {
780
+ return kcCreds;
781
+ }
782
+ const fileCreds = this.loadFromFile(profileKey);
783
+ if (!kcCreds) {
784
+ if (fileCreds) {
785
+ try {
786
+ await this.keychain.setSecret(
787
+ profileKey,
788
+ JSON.stringify(fileCreds),
789
+ `IWAS MCP Profile (${profileKey})`
790
+ );
791
+ await this.addProfileToKeychainIndex(profileKey);
792
+ this.deleteFromFile(profileKey);
793
+ } catch {
794
+ }
795
+ }
796
+ return fileCreds;
797
+ }
798
+ if (!fileCreds) {
799
+ return kcCreds;
800
+ }
801
+ const kcTime = kcCreds.updatedAt ?? kcCreds.expiresAt ?? 0;
802
+ const fileTime = fileCreds.updatedAt ?? fileCreds.expiresAt ?? 0;
803
+ if (fileTime > kcTime) {
804
+ try {
805
+ await this.keychain.setSecret(
806
+ profileKey,
807
+ JSON.stringify(fileCreds),
808
+ `IWAS MCP Profile (${profileKey})`
809
+ );
810
+ await this.addProfileToKeychainIndex(profileKey);
811
+ this.deleteFromFile(profileKey);
812
+ } catch {
813
+ }
814
+ return fileCreds;
815
+ } else {
816
+ this.deleteFromFile(profileKey);
817
+ return kcCreds;
818
+ }
819
+ }
820
+ return this.loadFromFile(profileKey);
821
+ }
822
+ /**
823
+ * Persists credentials for the given profile key.
824
+ * When saving to keychain, purges any stale copy from the file fallback (Major 1).
825
+ */
826
+ async save(profileKey, credentials) {
827
+ const backend = await this.getEffectiveBackend();
828
+ const toSave = {
829
+ ...credentials,
830
+ updatedAt: credentials.updatedAt ?? Date.now()
831
+ };
832
+ if (backend === "keychain") {
833
+ let secretSaved = false;
834
+ try {
835
+ const secret = JSON.stringify(toSave);
836
+ await this.keychain.setSecret(
837
+ profileKey,
838
+ secret,
839
+ `IWAS MCP Profile (${profileKey})`
840
+ );
841
+ secretSaved = true;
842
+ try {
843
+ await this.addProfileToKeychainIndex(profileKey);
844
+ } catch (indexErr) {
845
+ const message = indexErr instanceof Error ? indexErr.message : String(indexErr);
846
+ const sanitized = sanitizeSecretString(message, [
847
+ toSave.accessToken,
848
+ toSave.refreshToken
849
+ ]);
850
+ throw new Error(`Keychain index update failed: ${sanitized}`);
851
+ }
852
+ this.deleteFromFile(profileKey);
853
+ return;
854
+ } catch (err) {
855
+ if (this.vaultType === "keychain" || secretSaved) {
856
+ const message = err instanceof Error ? err.message : String(err);
857
+ const sanitized = sanitizeSecretString(message, [
858
+ toSave.accessToken,
859
+ toSave.refreshToken
860
+ ]);
861
+ throw err instanceof Error && err.message.startsWith("Keychain") ? err : new Error(`Keychain save failed: ${sanitized}`);
862
+ }
863
+ }
864
+ }
865
+ this.saveToFile(profileKey, toSave);
866
+ }
867
+ /**
868
+ * Removes credentials for the given profile key.
869
+ * Cleanses BOTH Keychain and File fallback to prevent plaintext leakage (Major 1).
870
+ * In auto mode, throws if keychain is available but secret deletion fails (Major fix).
871
+ */
872
+ async delete(profileKey) {
873
+ let keychainError = null;
874
+ let isAvailable = false;
875
+ if (this.vaultType === "file") {
876
+ this.deleteFromFile(profileKey);
877
+ return;
878
+ }
879
+ try {
880
+ isAvailable = await this.keychain.isAvailable().catch(() => false);
881
+ if (this.vaultType === "keychain" && !isAvailable) {
882
+ throw new Error(
883
+ `Keychain vault requested (IWAS_MCP_VAULT="keychain") but OS keychain is unavailable on platform: ${this.keychain.getPlatform()}`
884
+ );
885
+ }
886
+ if (isAvailable) {
887
+ await this.keychain.deleteSecret(profileKey);
888
+ await this.removeProfileFromKeychainIndex(profileKey);
889
+ }
890
+ } catch (err) {
891
+ keychainError = err;
892
+ }
893
+ this.deleteFromFile(profileKey);
894
+ if (keychainError) {
895
+ if (this.vaultType === "keychain" || this.vaultType === "auto" && isAvailable) {
896
+ throw keychainError;
897
+ }
898
+ }
899
+ }
900
+ /**
901
+ * Lists all stored profile keys.
902
+ * In auto mode, merges profiles from both Keychain and File.
903
+ */
904
+ async listProfiles() {
905
+ if (this.vaultType === "file") {
906
+ return this.listProfilesFromFile();
907
+ }
908
+ if (this.vaultType === "keychain") {
909
+ return this.readKeychainIndex();
910
+ }
911
+ const profiles = /* @__PURE__ */ new Set();
912
+ try {
913
+ const isAvailable = await this.keychain.isAvailable();
914
+ if (isAvailable) {
915
+ const kcProfiles = await this.readKeychainIndex();
916
+ for (const p of kcProfiles) {
917
+ profiles.add(p);
918
+ }
919
+ }
920
+ } catch {
921
+ }
922
+ for (const p of this.listProfilesFromFile()) {
923
+ profiles.add(p);
924
+ }
925
+ return Array.from(profiles);
926
+ }
927
+ // --- Internal Keychain Index (Major 2: Safe read & abort on error) ---
928
+ async readKeychainIndex() {
929
+ const rawIndex = await this.keychain.getSecret(KEYCHAIN_PROFILES_INDEX_ACCOUNT);
930
+ if (!rawIndex) {
931
+ return [];
932
+ }
933
+ try {
934
+ const parsed = JSON.parse(rawIndex);
935
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
936
+ } catch (err) {
937
+ const message = err instanceof Error ? err.message : String(err);
938
+ throw new Error(`Corrupt keychain profile index at ${KEYCHAIN_PROFILES_INDEX_ACCOUNT}: ${message}`);
939
+ }
940
+ }
941
+ async addProfileToKeychainIndex(profileKey) {
942
+ const current = await this.readKeychainIndex();
943
+ if (!current.includes(profileKey)) {
944
+ current.push(profileKey);
945
+ await this.keychain.setSecret(
946
+ KEYCHAIN_PROFILES_INDEX_ACCOUNT,
947
+ JSON.stringify(current),
948
+ "IWAS MCP Profiles Index"
949
+ );
950
+ }
951
+ }
952
+ async removeProfileFromKeychainIndex(profileKey) {
953
+ const current = await this.readKeychainIndex();
954
+ if (current.includes(profileKey)) {
955
+ const updated = current.filter((k) => k !== profileKey);
956
+ await this.keychain.setSecret(
957
+ KEYCHAIN_PROFILES_INDEX_ACCOUNT,
958
+ JSON.stringify(updated),
959
+ "IWAS MCP Profiles Index"
960
+ );
961
+ }
962
+ }
963
+ // --- Internal File Storage Backend (dir 0700, file 0600, atomic replace) ---
964
+ ensureConfigDirectory() {
965
+ if (!import_node_fs.default.existsSync(this.configDir)) {
966
+ import_node_fs.default.mkdirSync(this.configDir, { recursive: true, mode: 448 });
967
+ }
968
+ try {
969
+ import_node_fs.default.chmodSync(this.configDir, 448);
970
+ } catch {
971
+ }
972
+ }
973
+ readAllFromFile() {
974
+ if (!import_node_fs.default.existsSync(this.credentialsFilePath)) {
975
+ return {};
976
+ }
977
+ if (process.platform !== "win32") {
978
+ try {
979
+ import_node_fs.default.chmodSync(this.credentialsFilePath, 384);
980
+ } catch {
981
+ }
982
+ }
983
+ const content = import_node_fs.default.readFileSync(this.credentialsFilePath, "utf8").trim();
984
+ if (!content) {
985
+ return {};
986
+ }
987
+ try {
988
+ const parsed = JSON.parse(content);
989
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
990
+ throw new Error("Root credentials object must be a JSON record");
991
+ }
992
+ for (const [key, item] of Object.entries(parsed)) {
993
+ if (!isValidStoredCredentials(item)) {
994
+ throw new Error(`Profile "${key}" contains invalid or missing credential fields`);
995
+ }
996
+ }
997
+ return parsed;
998
+ } catch (err) {
999
+ const message = err instanceof Error ? err.message : String(err);
1000
+ throw new Error(
1001
+ `Corrupt credentials file at ${this.credentialsFilePath}: ${message}. Will not overwrite.`
1002
+ );
1003
+ }
1004
+ }
1005
+ writeAllToFile(data) {
1006
+ this.ensureConfigDirectory();
1007
+ const content = JSON.stringify(data, null, 2) + "\n";
1008
+ const tmpFilePath = import_node_path.default.join(
1009
+ this.configDir,
1010
+ `.credentials.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`
1011
+ );
1012
+ let fd = null;
1013
+ try {
1014
+ fd = import_node_fs.default.openSync(tmpFilePath, "w", 384);
1015
+ import_node_fs.default.writeSync(fd, content, void 0, "utf8");
1016
+ import_node_fs.default.fsyncSync(fd);
1017
+ import_node_fs.default.closeSync(fd);
1018
+ fd = null;
1019
+ try {
1020
+ import_node_fs.default.chmodSync(tmpFilePath, 384);
1021
+ } catch {
1022
+ }
1023
+ import_node_fs.default.renameSync(tmpFilePath, this.credentialsFilePath);
1024
+ try {
1025
+ import_node_fs.default.chmodSync(this.credentialsFilePath, 384);
1026
+ } catch {
1027
+ }
1028
+ if (process.platform !== "win32") {
1029
+ try {
1030
+ const dirFd = import_node_fs.default.openSync(this.configDir, "r");
1031
+ import_node_fs.default.fsyncSync(dirFd);
1032
+ import_node_fs.default.closeSync(dirFd);
1033
+ } catch {
1034
+ }
1035
+ }
1036
+ } catch (err) {
1037
+ if (fd !== null) {
1038
+ try {
1039
+ import_node_fs.default.closeSync(fd);
1040
+ } catch {
1041
+ }
1042
+ }
1043
+ if (import_node_fs.default.existsSync(tmpFilePath)) {
1044
+ try {
1045
+ import_node_fs.default.unlinkSync(tmpFilePath);
1046
+ } catch {
1047
+ }
1048
+ }
1049
+ throw err;
1050
+ }
1051
+ }
1052
+ loadFromFile(profileKey) {
1053
+ const all = this.readAllFromFile();
1054
+ return all[profileKey] ?? null;
1055
+ }
1056
+ saveToFile(profileKey, credentials) {
1057
+ const all = this.readAllFromFile();
1058
+ all[profileKey] = credentials;
1059
+ this.writeAllToFile(all);
1060
+ }
1061
+ deleteFromFile(profileKey) {
1062
+ if (!import_node_fs.default.existsSync(this.credentialsFilePath)) {
1063
+ return;
1064
+ }
1065
+ const all = this.readAllFromFile();
1066
+ if (profileKey in all) {
1067
+ delete all[profileKey];
1068
+ this.writeAllToFile(all);
1069
+ }
1070
+ }
1071
+ listProfilesFromFile() {
1072
+ if (!import_node_fs.default.existsSync(this.credentialsFilePath)) {
1073
+ return [];
1074
+ }
1075
+ const all = this.readAllFromFile();
1076
+ return Object.keys(all);
1077
+ }
1078
+ };
1079
+
1080
+ // src/auth/logout.ts
1081
+ async function executeLogout(options) {
1082
+ const log = options?.logger || ((msg) => process.stdout.write(`${msg}
1083
+ `));
1084
+ const errLog = options?.errorLogger || ((msg) => process.stderr.write(`${msg}
1085
+ `));
1086
+ const serverUrl = resolveAuthServerUrl(options?.server);
1087
+ const clientId = (options?.clientId || DEFAULT_CLI_CLIENT_ID).trim();
1088
+ const profileKey = normalizeProfileKey(serverUrl, clientId);
1089
+ const vault = options?.vault || new CredentialVault();
1090
+ const force = options?.force === true;
1091
+ const credentials = await vault.load(profileKey);
1092
+ if (!credentials) {
1093
+ const result2 = {
1094
+ success: true,
1095
+ profileKey,
1096
+ serverRevoked: false,
1097
+ refreshTokenRevoked: false,
1098
+ localCleared: false,
1099
+ message: `No stored credentials found for profile: ${profileKey}`
1100
+ };
1101
+ if (options?.json) {
1102
+ log(
1103
+ JSON.stringify({
1104
+ event: "logout",
1105
+ success: true,
1106
+ loggedOut: true,
1107
+ profileKey,
1108
+ serverRevoked: false,
1109
+ refreshTokenRevoked: false,
1110
+ localCleared: false,
1111
+ message: result2.message
1112
+ })
1113
+ );
1114
+ } else {
1115
+ log(`\u2139\uFE0F No stored credentials found for profile: ${profileKey}`);
1116
+ }
1117
+ return result2;
1118
+ }
1119
+ let serverRevoked = false;
1120
+ let serverError = null;
1121
+ const sessionLogoutUrl = `${serverUrl.replace(/\/+$/, "")}/api/v1/mcp-session/logout`;
1122
+ try {
1123
+ const authScheme = credentials.tokenType || "Bearer";
1124
+ const res = await sendRequest(sessionLogoutUrl, {
1125
+ method: "POST",
1126
+ headers: {
1127
+ Authorization: `${authScheme} ${credentials.accessToken}`
1128
+ },
1129
+ fetchFn: options?.fetchFn
1130
+ });
1131
+ if (res.status === 204 || res.status === 200) {
1132
+ serverRevoked = true;
1133
+ } else if (res.status === 401 || res.status === 403) {
1134
+ serverRevoked = true;
1135
+ } else if (res.status === 400) {
1136
+ const data = res.data;
1137
+ throw new Error(data?.message || "Server rejected logout request (HTTP 400).");
1138
+ } else {
1139
+ throw new HttpError(
1140
+ `Server returned status ${res.status} during logout`,
1141
+ res.status,
1142
+ res.data
1143
+ );
1144
+ }
1145
+ } catch (err) {
1146
+ serverError = err instanceof Error ? err : new Error(String(err));
1147
+ }
1148
+ if (serverError && !force) {
1149
+ const errorMsg = `Server revocation failed: ${serverError.message}. Local credentials preserved. Use --force to clear local credentials anyway if the server is permanently offline.`;
1150
+ throw new Error(errorMsg);
1151
+ }
1152
+ let refreshTokenRevoked = false;
1153
+ if (credentials.refreshToken) {
1154
+ try {
1155
+ const revokeUrl = `${serverUrl.replace(/\/+$/, "")}/api/auth/oauth2/revoke`;
1156
+ const body = new URLSearchParams({
1157
+ token: credentials.refreshToken,
1158
+ token_type_hint: "refresh_token",
1159
+ client_id: clientId
1160
+ });
1161
+ const res = await sendRequest(revokeUrl, {
1162
+ method: "POST",
1163
+ body,
1164
+ fetchFn: options?.fetchFn
1165
+ });
1166
+ if (res.ok || res.status === 200 || res.status === 204 || res.status === 401 || res.status === 400) {
1167
+ refreshTokenRevoked = true;
1168
+ }
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ await vault.delete(profileKey);
1173
+ const result = {
1174
+ success: true,
1175
+ profileKey,
1176
+ serverRevoked,
1177
+ refreshTokenRevoked,
1178
+ localCleared: true,
1179
+ message: serverError ? `Local credentials cleared with --force (server unreachable: ${serverError.message})` : "Successfully logged out and revoked all grants server-side"
1180
+ };
1181
+ if (options?.json) {
1182
+ log(
1183
+ JSON.stringify({
1184
+ event: "logout",
1185
+ success: true,
1186
+ loggedOut: true,
1187
+ profileKey,
1188
+ serverRevoked,
1189
+ refreshTokenRevoked,
1190
+ localCleared: true,
1191
+ forced: force && !!serverError
1192
+ })
1193
+ );
1194
+ } else {
1195
+ if (serverError) {
1196
+ errLog(`\u26A0\uFE0F Warning: Server revocation failed (${serverError.message})`);
1197
+ log(`\u2705 Local credentials cleared for profile: ${profileKey} (--force)
1198
+ `);
1199
+ } else {
1200
+ log(`
1201
+ \u2705 Successfully logged out from profile: ${profileKey}`);
1202
+ log(" \u2022 Server session & OAuth grants: Revoked");
1203
+ log(" \u2022 Local credentials: Removed\n");
1204
+ }
1205
+ }
1206
+ return result;
1207
+ }
1208
+
1209
+ // src/auth/device-flow.ts
1210
+ var DEFAULT_CLI_CLIENT_ID = "iwas-cli";
1211
+ var DEFAULT_SERVER_URL = "https://getiwas.com";
1212
+ var DEFAULT_READ_ONLY_SCOPES = import_permissions.MCP_ELIGIBLE_PERMISSIONS.filter(
1213
+ (perm) => perm.endsWith(":read")
1214
+ );
1215
+ var DEFAULT_SCOPES = [
1216
+ ...DEFAULT_READ_ONLY_SCOPES,
1217
+ import_permissions.OFFLINE_ACCESS_SCOPE
1218
+ ].join(" ");
1219
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
1220
+ function validateScopes(scopesString) {
1221
+ const requested = scopesString.split(/\s+/).filter(Boolean);
1222
+ if (requested.length === 0) {
1223
+ return {
1224
+ valid: false,
1225
+ invalidScopes: [],
1226
+ error: "At least one OAuth scope must be requested."
1227
+ };
1228
+ }
1229
+ const allowedSet = new Set(import_permissions.MCP_OAUTH_SCOPES);
1230
+ const invalidScopes = requested.filter((s) => !allowedSet.has(s));
1231
+ if (invalidScopes.length > 0) {
1232
+ return {
1233
+ valid: false,
1234
+ invalidScopes,
1235
+ error: `Invalid OAuth scope(s) requested: [${invalidScopes.join(", ")}]. Supported scopes are: [${import_permissions.MCP_OAUTH_SCOPES.join(", ")}]`
1236
+ };
1237
+ }
1238
+ const capabilityScopes = requested.filter((s) => s !== import_permissions.OFFLINE_ACCESS_SCOPE);
1239
+ if (capabilityScopes.length === 0) {
1240
+ return {
1241
+ valid: false,
1242
+ invalidScopes: [],
1243
+ error: "At least one MCP capability scope besides offline_access must be requested."
1244
+ };
1245
+ }
1246
+ return { valid: true, invalidScopes: [] };
1247
+ }
1248
+ function isLocalhost(hostname) {
1249
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
1250
+ }
1251
+ function resolveAuthServerUrl(input) {
1252
+ let urlStr = (input || process.env.IWAS_SERVER_URL || DEFAULT_SERVER_URL).trim();
1253
+ urlStr = urlStr.replace(/\/+$/, "");
1254
+ if (urlStr.toLowerCase().endsWith("/mcp")) {
1255
+ throw new Error(
1256
+ `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}".`
1257
+ );
1258
+ }
1259
+ if (!/^https?:\/\//i.test(urlStr)) {
1260
+ urlStr = `https://${urlStr}`;
1261
+ }
1262
+ let parsed;
1263
+ try {
1264
+ parsed = new URL(urlStr);
1265
+ } catch {
1266
+ throw new Error(`Invalid server URL: "${urlStr}"`);
1267
+ }
1268
+ if (parsed.pathname && parsed.pathname !== "/") {
1269
+ const originStr = parsed.origin;
1270
+ const label = input ? `--server value: "${input}"` : `IWAS_SERVER_URL: "${urlStr}"`;
1271
+ throw new Error(
1272
+ `Invalid ${label}. The server argument must be an origin without a path component (e.g. "${originStr}").`
1273
+ );
1274
+ }
1275
+ if (!isLocalhost(parsed.hostname) && parsed.protocol !== "https:") {
1276
+ throw new Error(
1277
+ `Insecure server protocol: "${urlStr}". Non-localhost servers require HTTPS per OAuth 2.1 specifications.`
1278
+ );
1279
+ }
1280
+ return parsed.origin.toLowerCase();
1281
+ }
1282
+ function resolveMcpResourceUrl(serverUrl, input) {
1283
+ if (input && input.trim()) {
1284
+ let clean = input.trim();
1285
+ if (!/^https?:\/\//i.test(clean)) {
1286
+ const isLoopbackCandidate = clean.startsWith("localhost") || clean.startsWith("127.0.0.1") || clean.startsWith("[::1]") || clean.startsWith("::1");
1287
+ clean = isLoopbackCandidate ? `http://${clean}` : `https://${clean}`;
1288
+ }
1289
+ let parsed;
1290
+ try {
1291
+ parsed = new URL(clean);
1292
+ } catch {
1293
+ throw new Error(`Invalid resource URL: "${clean}"`);
1294
+ }
1295
+ if (!isLocalhost(parsed.hostname) && parsed.protocol !== "https:") {
1296
+ throw new Error(
1297
+ `Insecure resource protocol: "${clean}". Non-localhost resource endpoints require HTTPS.`
1298
+ );
1299
+ }
1300
+ return parsed.href.replace(/\/+$/, "");
1301
+ }
1302
+ return `${serverUrl}/mcp`;
1303
+ }
1304
+ function decodeJwtPayload(token) {
1305
+ const parts = token.split(".");
1306
+ if (parts.length !== 3) {
1307
+ return {};
1308
+ }
1309
+ try {
1310
+ const jsonStr = Buffer.from(parts[1], "base64url").toString("utf8");
1311
+ const parsed = JSON.parse(jsonStr);
1312
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
1313
+ } catch {
1314
+ return {};
1315
+ }
1316
+ }
1317
+ function openBrowser(url, spawnFn = import_node_child_process2.spawn, onWarning) {
1318
+ try {
1319
+ let parsed;
1320
+ try {
1321
+ parsed = new URL(url);
1322
+ } catch {
1323
+ onWarning?.(`Invalid verification URL: "${url}". Please open manually.`);
1324
+ return false;
1325
+ }
1326
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1327
+ onWarning?.(`Insecure verification URL protocol: "${parsed.protocol}". Expected http: or https:.`);
1328
+ return false;
1329
+ }
1330
+ const platform = process.platform;
1331
+ let child;
1332
+ if (platform === "darwin") {
1333
+ child = spawnFn("open", [parsed.href], { detached: true, stdio: "ignore" });
1334
+ } else if (platform === "win32") {
1335
+ child = spawnFn("explorer.exe", [parsed.href], { detached: true, stdio: "ignore" });
1336
+ } else {
1337
+ child = spawnFn("xdg-open", [parsed.href], { detached: true, stdio: "ignore" });
1338
+ }
1339
+ child.on("error", (err) => {
1340
+ const msg = err instanceof Error ? err.message : String(err);
1341
+ onWarning?.(`Could not launch browser automatically (${msg}). Please open the verification URL manually.`);
1342
+ });
1343
+ child.unref();
1344
+ return true;
1345
+ } catch (err) {
1346
+ const msg = err instanceof Error ? err.message : String(err);
1347
+ onWarning?.(`Could not launch browser (${msg}). Please open the verification URL manually.`);
1348
+ return false;
1349
+ }
1350
+ }
1351
+ async function requestDeviceCode(serverUrl, params) {
1352
+ const endpoint = `${serverUrl}/api/auth/device/code`;
1353
+ const res = await sendRequest(endpoint, {
1354
+ method: "POST",
1355
+ body: {
1356
+ client_id: params.clientId,
1357
+ scope: params.scope,
1358
+ resource: params.resource
1359
+ },
1360
+ retries: 0
1361
+ // Zero retries to guarantee single-use device_code creation
1362
+ });
1363
+ if (!res.ok || !res.data || typeof res.data !== "object") {
1364
+ throw new HttpError(
1365
+ `Failed to initiate device authorization with ${endpoint}: HTTP ${res.status}`,
1366
+ res.status,
1367
+ res.data
1368
+ );
1369
+ }
1370
+ const data = res.data;
1371
+ if (!data.device_code || !data.user_code || !data.verification_uri) {
1372
+ throw new Error(
1373
+ `Malformed device code response from ${endpoint}: missing required RFC 8628 fields`
1374
+ );
1375
+ }
1376
+ return {
1377
+ device_code: data.device_code,
1378
+ user_code: data.user_code,
1379
+ verification_uri: data.verification_uri,
1380
+ verification_uri_complete: data.verification_uri_complete || `${data.verification_uri}?user_code=${encodeURIComponent(data.user_code)}`,
1381
+ expires_in: data.expires_in || 600,
1382
+ interval: data.interval || 5
1383
+ };
1384
+ }
1385
+ async function pollDeviceToken(serverUrl, params, onPollTick) {
1386
+ const tokenEndpoint = `${serverUrl}/api/auth/oauth2/token`;
1387
+ let intervalSec = Math.max(params.initialInterval || 5, 1);
1388
+ const timeoutMs = params.timeoutMs || 3e5;
1389
+ const startTime = Date.now();
1390
+ while (Date.now() - startTime < timeoutMs) {
1391
+ await new Promise((resolve) => setTimeout(resolve, intervalSec * 1e3));
1392
+ if (Date.now() - startTime >= timeoutMs) {
1393
+ break;
1394
+ }
1395
+ onPollTick?.("polling");
1396
+ const formBody = new URLSearchParams({
1397
+ grant_type: DEVICE_CODE_GRANT_TYPE,
1398
+ client_id: params.clientId,
1399
+ device_code: params.deviceCode,
1400
+ resource: params.resource
1401
+ });
1402
+ const res = await sendRequest(
1403
+ tokenEndpoint,
1404
+ {
1405
+ method: "POST",
1406
+ body: formBody,
1407
+ retries: 0
1408
+ // Zero retries to guarantee single-use device_code is never double-consumed
1409
+ }
1410
+ );
1411
+ if (res.status === 200) {
1412
+ const tokenData = res.data;
1413
+ if (!tokenData.access_token) {
1414
+ throw new Error("Token endpoint returned HTTP 200 without access_token");
1415
+ }
1416
+ return tokenData;
1417
+ }
1418
+ if (res.status === 429) {
1419
+ const retryAfterHeader = res.headers?.get?.("retry-after");
1420
+ let retrySeconds = intervalSec + 5;
1421
+ if (retryAfterHeader) {
1422
+ const parsedSec = parseInt(retryAfterHeader, 10);
1423
+ if (!isNaN(parsedSec) && parsedSec > 0) {
1424
+ retrySeconds = parsedSec;
1425
+ } else {
1426
+ const parsedDate = Date.parse(retryAfterHeader);
1427
+ if (!isNaN(parsedDate)) {
1428
+ const diffSec = Math.ceil((parsedDate - Date.now()) / 1e3);
1429
+ if (diffSec > 0) {
1430
+ retrySeconds = diffSec;
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ const remainingMs = Math.max(0, timeoutMs - (Date.now() - startTime));
1436
+ const remainingSec = Math.ceil(remainingMs / 1e3);
1437
+ intervalSec = remainingSec > 0 ? Math.min(retrySeconds, 60, remainingSec) : 0;
1438
+ onPollTick?.(`slow_down (rate_limited, retry-after: ${intervalSec}s)`);
1439
+ continue;
1440
+ }
1441
+ const errorData = res.data || {};
1442
+ const errorCode = errorData.error || "unknown_error";
1443
+ const errorDesc = errorData.error_description || errorCode;
1444
+ switch (errorCode) {
1445
+ case "authorization_pending":
1446
+ continue;
1447
+ case "slow_down":
1448
+ intervalSec += 5;
1449
+ onPollTick?.(`slow_down (interval: ${intervalSec}s)`);
1450
+ continue;
1451
+ case "expired_token":
1452
+ throw new Error(
1453
+ "Device code has expired. Please run `iwas login` again to obtain a new code."
1454
+ );
1455
+ case "access_denied":
1456
+ throw new Error("Authorization was denied by the user.");
1457
+ case "invalid_grant":
1458
+ throw new Error(
1459
+ `Invalid grant: ${errorDesc}. The device code may have already been used or revoked.`
1460
+ );
1461
+ case "invalid_client":
1462
+ throw new Error(`Invalid OAuth client: ${errorDesc}. Check your --client-id.`);
1463
+ default:
1464
+ if (res.status >= 500) {
1465
+ intervalSec = Math.min(intervalSec + 2, 30);
1466
+ continue;
1467
+ }
1468
+ throw new Error(`Device authorization failed: ${errorDesc} (HTTP ${res.status})`);
1469
+ }
1470
+ }
1471
+ throw new Error("Device login timed out waiting for user authorization.");
1472
+ }
1473
+ async function executeLogin(options) {
1474
+ const log = options?.logger || ((msg) => process.stdout.write(`${msg}
1475
+ `));
1476
+ const errLog = options?.errorLogger || ((msg) => process.stderr.write(`${msg}
1477
+ `));
1478
+ const serverUrl = resolveAuthServerUrl(options?.server);
1479
+ const resourceUrl = resolveMcpResourceUrl(serverUrl, options?.resource);
1480
+ const clientId = (options?.clientId || DEFAULT_CLI_CLIENT_ID).trim();
1481
+ const scopes = (options?.scopes || DEFAULT_SCOPES).trim();
1482
+ const vault = options?.vault || new CredentialVault();
1483
+ const scopeValidation = validateScopes(scopes);
1484
+ if (!scopeValidation.valid) {
1485
+ throw new Error(scopeValidation.error || "Invalid OAuth scopes requested.");
1486
+ }
1487
+ const authResponse = await requestDeviceCode(serverUrl, {
1488
+ clientId,
1489
+ scope: scopes,
1490
+ resource: resourceUrl
1491
+ });
1492
+ if (options?.json) {
1493
+ const initEvent = {
1494
+ event: "device_authorization",
1495
+ user_code: authResponse.user_code,
1496
+ verification_uri: authResponse.verification_uri,
1497
+ verification_uri_complete: authResponse.verification_uri_complete,
1498
+ expires_in: authResponse.expires_in,
1499
+ interval: authResponse.interval
1500
+ };
1501
+ log(JSON.stringify(initEvent));
1502
+ errLog(`
1503
+ \u{1F449} USER CODE: ${authResponse.user_code}`);
1504
+ errLog(`\u{1F517} To verify, open: ${authResponse.verification_uri_complete}
1505
+ `);
1506
+ } else {
1507
+ log("\n============================================================");
1508
+ log(" IWAS MCP Device Authorization ");
1509
+ log("============================================================\n");
1510
+ log(`First, enter this code on the verification page:`);
1511
+ log(`
1512
+ \u{1F449} USER CODE: \x1B[1m\x1B[32m${authResponse.user_code}\x1B[0m
1513
+ `);
1514
+ log(`To verify, open:`);
1515
+ log(` \u{1F517} ${authResponse.verification_uri_complete}
1516
+ `);
1517
+ log("Waiting for approval in browser...");
1518
+ }
1519
+ const spawnFn = options?.spawnFn || import_node_child_process2.spawn;
1520
+ const opener = options?.browserOpener || ((url) => openBrowser(url, spawnFn, (warning) => {
1521
+ if (!options?.json) {
1522
+ log(`\u26A0\uFE0F ${warning}`);
1523
+ } else {
1524
+ errLog(`\u26A0\uFE0F ${warning}`);
1525
+ }
1526
+ }));
1527
+ if (!options?.noBrowser) {
1528
+ opener(authResponse.verification_uri_complete);
1529
+ }
1530
+ const pollTimeoutMs = options?.pollTimeoutMs ?? authResponse.expires_in * 1e3;
1531
+ const tokenResponse = await pollDeviceToken(
1532
+ serverUrl,
1533
+ {
1534
+ clientId,
1535
+ deviceCode: authResponse.device_code,
1536
+ resource: resourceUrl,
1537
+ initialInterval: authResponse.interval,
1538
+ timeoutMs: pollTimeoutMs
1539
+ },
1540
+ (status) => {
1541
+ if (!options?.json && status.startsWith("slow_down")) {
1542
+ log(`Rate limit reached: slowing poll interval.`);
1543
+ }
1544
+ }
1545
+ );
1546
+ const payload = decodeJwtPayload(tokenResponse.access_token);
1547
+ if (!payload || Object.keys(payload).length === 0) {
1548
+ throw new Error(
1549
+ "Server returned an opaque or non-JWT access token. Expected a JWT access token targeted for MCP resource."
1550
+ );
1551
+ }
1552
+ const tokenAud = Array.isArray(payload.aud) ? payload.aud : typeof payload.aud === "string" ? [payload.aud] : [];
1553
+ const normalizedResourceUrl = resourceUrl.replace(/\/+$/, "").toLowerCase();
1554
+ const hasResourceAud = tokenAud.some((aud) => {
1555
+ try {
1556
+ return new URL(String(aud)).href.replace(/\/+$/, "").toLowerCase() === normalizedResourceUrl;
1557
+ } catch {
1558
+ return String(aud).toLowerCase() === normalizedResourceUrl;
1559
+ }
1560
+ });
1561
+ if (!hasResourceAud) {
1562
+ throw new Error(
1563
+ `Access token audience [${tokenAud.join(", ")}] does not match MCP resource URL "${resourceUrl}".`
1564
+ );
1565
+ }
1566
+ const grantedScopesStr = typeof tokenResponse.scope === "string" && tokenResponse.scope || typeof payload.scope === "string" && payload.scope || "";
1567
+ const grantedList = grantedScopesStr.split(/\s+/).filter(Boolean);
1568
+ const requestedList = scopes.split(/\s+/).filter(Boolean);
1569
+ const matchedScopes = requestedList.filter((s) => grantedList.includes(s));
1570
+ const matchedCapabilities = matchedScopes.filter((s) => s !== import_permissions.OFFLINE_ACCESS_SCOPE);
1571
+ if (matchedCapabilities.length === 0) {
1572
+ throw new Error(
1573
+ `Access token lacks granted capability scopes (requested: [${requestedList.join(", ")}], granted: [${grantedList.join(", ")}]). At least one permission scope besides offline_access must be granted.`
1574
+ );
1575
+ }
1576
+ const organizationId = typeof payload.organizationId === "string" ? payload.organizationId : null;
1577
+ const expiresAtMs = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : typeof payload.exp === "number" ? payload.exp * 1e3 : Date.now() + 36e5;
1578
+ const credentials = {
1579
+ serverUrl,
1580
+ clientId,
1581
+ accessToken: tokenResponse.access_token,
1582
+ refreshToken: tokenResponse.refresh_token,
1583
+ tokenType: tokenResponse.token_type || "Bearer",
1584
+ scope: tokenResponse.scope || scopes,
1585
+ resource: resourceUrl,
1586
+ organizationId,
1587
+ expiresAt: expiresAtMs,
1588
+ updatedAt: Date.now()
1589
+ };
1590
+ const profileKey = normalizeProfileKey(serverUrl, clientId);
1591
+ await vault.save(profileKey, credentials);
1592
+ const result = {
1593
+ profileKey,
1594
+ serverUrl,
1595
+ clientId,
1596
+ organizationId,
1597
+ scope: credentials.scope,
1598
+ resource: credentials.resource,
1599
+ expiresAt: expiresAtMs
1600
+ };
1601
+ if (options?.json) {
1602
+ const successEvent = {
1603
+ event: "login_success",
1604
+ success: true,
1605
+ ...result
1606
+ };
1607
+ log(JSON.stringify(successEvent));
1608
+ } else {
1609
+ log("\n\u2705 Login successful!");
1610
+ log(`- Profile: ${profileKey}`);
1611
+ log(`- Server: ${serverUrl}`);
1612
+ log(`- Client ID: ${clientId}`);
1613
+ log(`- Organization: ${organizationId || "(none / platform scope)"}`);
1614
+ log(`- Scopes: ${credentials.scope || "none"}`);
1615
+ log(`- Resource: ${resourceUrl}`);
1616
+ log(`- Expires: ${new Date(expiresAtMs).toISOString()}
1617
+ `);
1618
+ }
1619
+ return result;
1620
+ }
1621
+ async function executeStatus(options) {
1622
+ const log = options?.logger || ((msg) => process.stdout.write(`${msg}
1623
+ `));
1624
+ const serverUrl = resolveAuthServerUrl(options?.server);
1625
+ const clientId = (options?.clientId || DEFAULT_CLI_CLIENT_ID).trim();
1626
+ const profileKey = normalizeProfileKey(serverUrl, clientId);
1627
+ const vault = options?.vault || new CredentialVault();
1628
+ const creds = await vault.load(profileKey);
1629
+ if (!creds) {
1630
+ if (options?.json) {
1631
+ log(JSON.stringify({ authenticated: false, profileKey, serverUrl, clientId }));
1632
+ } else {
1633
+ log(`No active session found for profile: ${profileKey}`);
1634
+ log(`Run \`iwas login --server ${serverUrl}\` to authenticate.
1635
+ `);
1636
+ }
1637
+ return;
1638
+ }
1639
+ const isExpired = isCredentialExpired(creds, 60);
1640
+ const statusData = {
1641
+ authenticated: !isExpired,
1642
+ expired: isExpired,
1643
+ profileKey,
1644
+ serverUrl: creds.serverUrl,
1645
+ clientId: creds.clientId,
1646
+ organizationId: creds.organizationId,
1647
+ scope: creds.scope,
1648
+ resource: creds.resource,
1649
+ expiresAt: new Date(creds.expiresAt).toISOString(),
1650
+ expiresInSeconds: Math.max(0, Math.round((creds.expiresAt - Date.now()) / 1e3))
1651
+ };
1652
+ if (options?.json) {
1653
+ log(JSON.stringify(statusData));
1654
+ } else {
1655
+ log("\n============================================================");
1656
+ log(" IWAS MCP Auth Status ");
1657
+ log("============================================================\n");
1658
+ log(`- Profile: ${profileKey}`);
1659
+ log(`- Server: ${creds.serverUrl}`);
1660
+ log(`- Client ID: ${creds.clientId}`);
1661
+ log(`- Status: ${isExpired ? "\u{1F534} Expired" : "\u{1F7E2} Active"}`);
1662
+ log(`- Organization: ${creds.organizationId || "(none / platform scope)"}`);
1663
+ log(`- Scopes: ${creds.scope || "none"}`);
1664
+ if (creds.resource) {
1665
+ log(`- Resource: ${creds.resource}`);
1666
+ }
1667
+ log(`- Expires: ${statusData.expiresAt} (${statusData.expiresInSeconds}s remaining)
1668
+ `);
1669
+ }
1670
+ }
1671
+
1672
+ // src/bin.ts
1673
+ var CLI_NAME = "iwas";
1674
+ var CLI_VERSION = "1.0.0";
1675
+ function printHelp(write = (msg) => process.stdout.write(msg)) {
1676
+ const helpText = `
1677
+ ${CLI_NAME} \u2014 Intelligent WiFi Access & Presence Service (IWAS) MCP CLI
1678
+
1679
+ USAGE:
1680
+ ${CLI_NAME} <command> [options]
1681
+ iwas-mcp <command> [options]
1682
+
1683
+ COMMANDS:
1684
+ login Authenticate using OAuth 2.0 Device Authorization Grant (RFC 8628)
1685
+ status Display current authentication status for a server profile
1686
+ logout Remove stored credentials for a server profile
1687
+ help Display help information
1688
+
1689
+ OPTIONS:
1690
+ -s, --server <url> Auth server origin (default: $IWAS_SERVER_URL or https://getiwas.com)
1691
+ -r, --resource <url> MCP resource endpoint (default: \${server}/mcp)
1692
+ -c, --client-id <id> OAuth client ID (default: iwas-cli)
1693
+ --scopes <scopes> OAuth scopes (default: read-only scopes + offline_access)
1694
+ --no-browser Do not open the verification URL in browser
1695
+ --json Output result in machine-readable JSON format
1696
+ -f, --force Force local logout if server is unreachable
1697
+ -h, --help Display help
1698
+ -v, --version Display CLI version
1699
+
1700
+ EXAMPLES:
1701
+ ${CLI_NAME} login
1702
+ ${CLI_NAME} login --server https://getiwas.com --no-browser
1703
+ ${CLI_NAME} status
1704
+ ${CLI_NAME} logout
1705
+ ${CLI_NAME} logout --force
1706
+ `;
1707
+ write(helpText.trimStart() + "\n");
1708
+ }
1709
+ async function runCli(argv = process.argv.slice(2), io) {
1710
+ const writeOut = io?.stdout || ((msg) => process.stdout.write(msg));
1711
+ const writeErr = io?.stderr || ((msg) => process.stderr.write(msg));
1712
+ const exitFn = io?.exit || ((code) => process.exit(code));
1713
+ let parsed;
1714
+ try {
1715
+ parsed = (0, import_node_util.parseArgs)({
1716
+ args: argv,
1717
+ options: {
1718
+ server: { type: "string", short: "s" },
1719
+ resource: { type: "string", short: "r" },
1720
+ "client-id": { type: "string", short: "c" },
1721
+ scopes: { type: "string" },
1722
+ "no-browser": { type: "boolean", default: false },
1723
+ json: { type: "boolean", default: false },
1724
+ force: { type: "boolean", short: "f", default: false },
1725
+ help: { type: "boolean", short: "h", default: false },
1726
+ version: { type: "boolean", short: "v", default: false }
1727
+ },
1728
+ allowPositionals: true,
1729
+ strict: true
1730
+ });
1731
+ } catch (err) {
1732
+ const msg = err instanceof Error ? err.message : String(err);
1733
+ writeErr(`Error: ${msg}
1734
+ `);
1735
+ writeErr(`Run \`${CLI_NAME} --help\` for usage.
1736
+ `);
1737
+ exitFn(1);
1738
+ return;
1739
+ }
1740
+ const { values, positionals } = parsed;
1741
+ if (values.version) {
1742
+ writeOut(`${CLI_NAME} v${CLI_VERSION}
1743
+ `);
1744
+ return;
1745
+ }
1746
+ const command = positionals[0] || "";
1747
+ if (values.help || !command || command === "help") {
1748
+ printHelp(writeOut);
1749
+ return;
1750
+ }
1751
+ const server = typeof values.server === "string" ? values.server : void 0;
1752
+ const resource = typeof values.resource === "string" ? values.resource : void 0;
1753
+ const clientId = typeof values["client-id"] === "string" ? values["client-id"] : void 0;
1754
+ const scopes = typeof values.scopes === "string" ? values.scopes : void 0;
1755
+ const noBrowser = values["no-browser"] === true;
1756
+ const json = values.json === true;
1757
+ const vault = io?.vault;
1758
+ try {
1759
+ switch (command) {
1760
+ case "login": {
1761
+ await executeLogin({
1762
+ server,
1763
+ resource,
1764
+ clientId,
1765
+ scopes,
1766
+ noBrowser,
1767
+ json,
1768
+ vault,
1769
+ logger: (msg) => writeOut(`${msg}
1770
+ `),
1771
+ errorLogger: (msg) => writeErr(`${msg}
1772
+ `)
1773
+ });
1774
+ break;
1775
+ }
1776
+ case "status": {
1777
+ await executeStatus({
1778
+ server,
1779
+ clientId,
1780
+ json,
1781
+ vault,
1782
+ logger: (msg) => writeOut(`${msg}
1783
+ `)
1784
+ });
1785
+ break;
1786
+ }
1787
+ case "logout": {
1788
+ const force = values.force === true;
1789
+ await executeLogout({
1790
+ server,
1791
+ clientId,
1792
+ force,
1793
+ json,
1794
+ vault,
1795
+ logger: (msg) => writeOut(`${msg}
1796
+ `),
1797
+ errorLogger: (msg) => writeErr(`${msg}
1798
+ `)
1799
+ });
1800
+ break;
1801
+ }
1802
+ default: {
1803
+ writeErr(`Unknown command: "${command}"
1804
+ `);
1805
+ writeErr(`Run \`${CLI_NAME} --help\` for list of available commands.
1806
+ `);
1807
+ exitFn(1);
1808
+ return;
1809
+ }
1810
+ }
1811
+ } catch (err) {
1812
+ const message = err instanceof Error ? err.message : String(err);
1813
+ if (values.json) {
1814
+ writeOut(JSON.stringify({ event: "error", error: message }) + "\n");
1815
+ } else {
1816
+ writeErr(`
1817
+ \u274C Error: ${message}
1818
+
1819
+ `);
1820
+ }
1821
+ exitFn(1);
1822
+ }
1823
+ }
1824
+ if (require.main === module) {
1825
+ runCli();
1826
+ }
1827
+ // Annotate the CommonJS export names for ESM import in node:
1828
+ 0 && (module.exports = {
1829
+ CLI_NAME,
1830
+ CLI_VERSION,
1831
+ printHelp,
1832
+ runCli
1833
+ });