@walkeros/cli 4.5.0 → 4.6.0-next-1788817472881

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -139,9 +139,17 @@ function createCLILoggerConfig(options = {}) {
139
139
  // at DEBUG, ERROR always reaches the handler (and the ring) even without
140
140
  // --verbose.
141
141
  level: Level.DEBUG,
142
- handler: (level, message, _context, scope) => {
142
+ handler: (level, message, context2, scope) => {
143
143
  const scopePath = scope.length > 0 ? `[${scope.join(":")}] ` : "";
144
- const fullMessage = scrubSecrets(`${scopePath}${message}`);
144
+ let meta = "";
145
+ if (Object.keys(context2).length > 0) {
146
+ try {
147
+ meta = ` ${JSON.stringify(context2)}`;
148
+ } catch {
149
+ meta = " [unserializable context]";
150
+ }
151
+ }
152
+ const fullMessage = scrubSecrets(`${scopePath}${message}${meta}`);
145
153
  try {
146
154
  options.onLine?.(level, fullMessage);
147
155
  } catch {
@@ -260,7 +268,9 @@ import {
260
268
  writeFileSync,
261
269
  mkdirSync,
262
270
  unlinkSync,
263
- existsSync
271
+ existsSync,
272
+ chmodSync,
273
+ renameSync
264
274
  } from "fs";
265
275
  import { join } from "path";
266
276
  import { homedir } from "os";
@@ -281,15 +291,33 @@ function readConfig() {
281
291
  return null;
282
292
  }
283
293
  }
284
- function writeConfig(config) {
294
+ function replaceConfigFile(config) {
285
295
  const dir = getConfigDir();
286
- mkdirSync(dir, { recursive: true });
296
+ mkdirSync(dir, { recursive: true, mode: 448 });
287
297
  const configPath = getConfigPath();
288
- writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
298
+ const tempPath = `${configPath}.tmp`;
299
+ writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 384 });
300
+ chmodSync(tempPath, 384);
301
+ renameSync(tempPath, configPath);
302
+ }
303
+ function writeConfig(config) {
304
+ replaceConfigFile({ ...readConfig() ?? {}, ...config });
305
+ }
306
+ function clearAuthFields() {
307
+ const config = readConfig();
308
+ if (!config) return;
309
+ const {
310
+ token: _token,
311
+ accessToken: _accessToken,
312
+ accessTokenExpiresAt: _accessTokenExpiresAt,
313
+ refreshToken: _refreshToken,
314
+ email: _email,
315
+ ...rest
316
+ } = config;
317
+ replaceConfigFile(rest);
289
318
  }
290
319
  function writeTelemetryOnlyConfig(partial) {
291
- const existing = readConfig() ?? {};
292
- writeConfig({ ...existing, ...partial });
320
+ writeConfig(partial);
293
321
  }
294
322
  function deleteConfig() {
295
323
  const configPath = getConfigPath();
@@ -323,7 +351,7 @@ function clearDefaultProject() {
323
351
  const config = readConfig();
324
352
  if (!config) return;
325
353
  const { defaultProjectId: _removed, ...rest } = config;
326
- writeConfig(rest);
354
+ replaceConfigFile(rest);
327
355
  }
328
356
  function resolveToken() {
329
357
  const envToken = process.env.WALKEROS_TOKEN;
@@ -348,6 +376,336 @@ var init_config_file = __esm({
348
376
  }
349
377
  });
350
378
 
379
+ // src/lib/config-lock.ts
380
+ import { closeSync, mkdirSync as mkdirSync2, openSync, statSync, unlinkSync as unlinkSync2 } from "fs";
381
+ function getConfigLockPath() {
382
+ return `${getConfigPath()}.lock`;
383
+ }
384
+ function hasCode(value) {
385
+ return typeof value === "object" && value !== null && "code" in value;
386
+ }
387
+ function isAlreadyLocked(error) {
388
+ return hasCode(error) && error.code === "EEXIST";
389
+ }
390
+ function delay(ms) {
391
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
392
+ }
393
+ function breakIfStale(lockPath) {
394
+ try {
395
+ const age = Date.now() - statSync(lockPath).mtimeMs;
396
+ if (age < STALE_MS) return false;
397
+ unlinkSync2(lockPath);
398
+ return true;
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+ async function withConfigLock(fn) {
404
+ const lockPath = getConfigLockPath();
405
+ mkdirSync2(getConfigDir(), { recursive: true, mode: 448 });
406
+ const deadline = Date.now() + TIMEOUT_MS;
407
+ for (; ; ) {
408
+ let handle;
409
+ try {
410
+ handle = openSync(lockPath, "wx", 384);
411
+ } catch (error) {
412
+ if (!isAlreadyLocked(error)) throw error;
413
+ if (breakIfStale(lockPath)) continue;
414
+ if (Date.now() >= deadline) {
415
+ throw new Error(
416
+ `Timed out waiting for the walkerOS config lock at ${lockPath}. Remove the file if no other walkeros process is running.`
417
+ );
418
+ }
419
+ await delay(RETRY_MS);
420
+ continue;
421
+ }
422
+ closeSync(handle);
423
+ try {
424
+ return await fn();
425
+ } finally {
426
+ try {
427
+ unlinkSync2(lockPath);
428
+ } catch {
429
+ }
430
+ }
431
+ }
432
+ }
433
+ var STALE_MS, RETRY_MS, TIMEOUT_MS;
434
+ var init_config_lock = __esm({
435
+ "src/lib/config-lock.ts"() {
436
+ "use strict";
437
+ init_config_file();
438
+ STALE_MS = 15e3;
439
+ RETRY_MS = 100;
440
+ TIMEOUT_MS = 1e4;
441
+ }
442
+ });
443
+
444
+ // src/core/oauth-client.ts
445
+ import { z } from "zod";
446
+ async function readJson(response) {
447
+ try {
448
+ return await response.json();
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+ function errorCode(body) {
454
+ const parsed = ErrorSchema.safeParse(body);
455
+ return parsed.success ? parsed.data.error : null;
456
+ }
457
+ function describe(response, body) {
458
+ const parsed = ErrorSchema.safeParse(body);
459
+ if (parsed.success) {
460
+ return parsed.data.error_description ? `${parsed.data.error}: ${parsed.data.error_description}` : parsed.data.error;
461
+ }
462
+ return `HTTP ${response.status}`;
463
+ }
464
+ function post(fetchFn, url, form, signal) {
465
+ return fetchFn(url, {
466
+ method: "POST",
467
+ headers: { ...FORM_HEADERS },
468
+ body: new URLSearchParams(form).toString(),
469
+ ...signal ? { signal } : {}
470
+ });
471
+ }
472
+ function toTokenSet(body) {
473
+ const parsed = TokenSchema.safeParse(body);
474
+ if (!parsed.success) throw new Error("Malformed token response");
475
+ return {
476
+ accessToken: parsed.data.access_token,
477
+ accessTokenExpiresAt: new Date(
478
+ Date.now() + parsed.data.expires_in * 1e3
479
+ ).toISOString(),
480
+ refreshToken: parsed.data.refresh_token ?? null
481
+ };
482
+ }
483
+ async function startDeviceAuthorization(appUrl, fetchFn = globalThis.fetch) {
484
+ const response = await post(
485
+ fetchFn,
486
+ `${appUrl}/api/oauth/device_authorization`,
487
+ {
488
+ client_id: CLI_CLIENT_ID,
489
+ scope: CLI_SCOPE,
490
+ // RFC 8707. The token comes back bound to the API, so a leaked CLI token
491
+ // cannot be replayed against the MCP resource.
492
+ resource: `${appUrl}/api`
493
+ }
494
+ );
495
+ const body = await readJson(response);
496
+ if (!response.ok) throw new Error(describe(response, body));
497
+ const parsed = DeviceAuthorizationSchema.safeParse(body);
498
+ if (!parsed.success)
499
+ throw new Error("Malformed device authorization response");
500
+ return {
501
+ deviceCode: parsed.data.device_code,
502
+ userCode: parsed.data.user_code,
503
+ verificationUri: parsed.data.verification_uri,
504
+ verificationUriComplete: parsed.data.verification_uri_complete ?? parsed.data.verification_uri,
505
+ expiresIn: parsed.data.expires_in,
506
+ interval: parsed.data.interval
507
+ };
508
+ }
509
+ async function pollDeviceToken(appUrl, deviceCode, fetchFn = globalThis.fetch, signal) {
510
+ let response;
511
+ try {
512
+ response = await post(
513
+ fetchFn,
514
+ `${appUrl}/api/oauth/token`,
515
+ {
516
+ grant_type: DEVICE_CODE_GRANT,
517
+ device_code: deviceCode,
518
+ client_id: CLI_CLIENT_ID
519
+ },
520
+ signal
521
+ );
522
+ } catch (error) {
523
+ if (signal?.aborted) return { status: "pending" };
524
+ throw error;
525
+ }
526
+ const body = await readJson(response);
527
+ if (response.ok) {
528
+ try {
529
+ return { status: "ok", tokens: toTokenSet(body) };
530
+ } catch (error) {
531
+ return {
532
+ status: "error",
533
+ error: error instanceof Error ? error.message : String(error)
534
+ };
535
+ }
536
+ }
537
+ switch (errorCode(body)) {
538
+ case "authorization_pending":
539
+ return { status: "pending" };
540
+ case "slow_down":
541
+ return { status: "slow_down" };
542
+ case "access_denied":
543
+ return { status: "denied" };
544
+ case "expired_token":
545
+ return { status: "expired" };
546
+ default:
547
+ return { status: "error", error: describe(response, body) };
548
+ }
549
+ }
550
+ async function refreshTokens(appUrl, refreshToken, fetchFn = globalThis.fetch) {
551
+ const response = await post(
552
+ fetchFn,
553
+ `${appUrl}/api/oauth/token`,
554
+ {
555
+ grant_type: "refresh_token",
556
+ refresh_token: refreshToken,
557
+ client_id: CLI_CLIENT_ID
558
+ },
559
+ AbortSignal.timeout(REFRESH_TIMEOUT_MS)
560
+ );
561
+ const body = await readJson(response);
562
+ if (!response.ok) {
563
+ if (errorCode(body) === "invalid_grant") return null;
564
+ throw new Error(describe(response, body));
565
+ }
566
+ return toTokenSet(body);
567
+ }
568
+ async function revokeRefreshToken(appUrl, refreshToken, fetchFn = globalThis.fetch) {
569
+ try {
570
+ await post(
571
+ fetchFn,
572
+ `${appUrl}/api/oauth/revoke`,
573
+ {
574
+ token: refreshToken,
575
+ token_type_hint: "refresh_token",
576
+ client_id: CLI_CLIENT_ID
577
+ },
578
+ AbortSignal.timeout(REVOKE_TIMEOUT_MS)
579
+ );
580
+ } catch {
581
+ }
582
+ }
583
+ var CLI_CLIENT_ID, CLI_SCOPE, DEVICE_CODE_GRANT, REFRESH_TIMEOUT_MS, REVOKE_TIMEOUT_MS, FORM_HEADERS, DeviceAuthorizationSchema, TokenSchema, ErrorSchema;
584
+ var init_oauth_client = __esm({
585
+ "src/core/oauth-client.ts"() {
586
+ "use strict";
587
+ CLI_CLIENT_ID = "walkeros-cli";
588
+ CLI_SCOPE = "read write offline_access";
589
+ DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
590
+ REFRESH_TIMEOUT_MS = 1e4;
591
+ REVOKE_TIMEOUT_MS = 5e3;
592
+ FORM_HEADERS = {
593
+ "Content-Type": "application/x-www-form-urlencoded",
594
+ Accept: "application/json"
595
+ };
596
+ DeviceAuthorizationSchema = z.object({
597
+ device_code: z.string().min(1),
598
+ user_code: z.string().min(1),
599
+ verification_uri: z.string().min(1),
600
+ verification_uri_complete: z.string().min(1).optional(),
601
+ expires_in: z.number().int().nonnegative(),
602
+ interval: z.number().int().nonnegative()
603
+ });
604
+ TokenSchema = z.object({
605
+ access_token: z.string().min(1),
606
+ expires_in: z.number().int().nonnegative(),
607
+ refresh_token: z.string().min(1).optional()
608
+ });
609
+ ErrorSchema = z.object({
610
+ error: z.string().min(1),
611
+ error_description: z.string().optional()
612
+ });
613
+ }
614
+ });
615
+
616
+ // src/core/auth.ts
617
+ function noticeLegacyToken() {
618
+ if (legacyNoticeShown) return;
619
+ legacyNoticeShown = true;
620
+ process.stderr.write(
621
+ "walkerOS: using a static token from your config. Run `walkeros login` to switch to a session that refreshes automatically.\n"
622
+ );
623
+ }
624
+ function isFresh(config, nowMs) {
625
+ if (!config.accessToken || !config.accessTokenExpiresAt) return false;
626
+ const expiresAt = Date.parse(config.accessTokenExpiresAt);
627
+ if (Number.isNaN(expiresAt)) return false;
628
+ return expiresAt - nowMs > REFRESH_SKEW_MS;
629
+ }
630
+ async function resolveAccessToken(opts) {
631
+ const envToken = process.env.WALKEROS_TOKEN;
632
+ if (envToken) return envToken;
633
+ const now = opts?.now ?? Date.now;
634
+ const config = readConfig();
635
+ if (!config) return null;
636
+ if (config.token) {
637
+ noticeLegacyToken();
638
+ return config.token;
639
+ }
640
+ if (!config.refreshToken) {
641
+ return isFresh(config, now()) ? config.accessToken ?? null : null;
642
+ }
643
+ if (isFresh(config, now())) return config.accessToken ?? null;
644
+ return withConfigLock(async () => {
645
+ const current = readConfig();
646
+ if (!current?.refreshToken) return current?.accessToken ?? null;
647
+ if (isFresh(current, now())) return current.accessToken ?? null;
648
+ let rotated;
649
+ try {
650
+ rotated = await refreshTokens(
651
+ resolveAppUrl(),
652
+ current.refreshToken,
653
+ opts?.fetch
654
+ );
655
+ } catch (error) {
656
+ const reason = error instanceof Error ? error.message : String(error);
657
+ throw new Error(
658
+ `Could not reach ${resolveAppUrl()} to refresh your session: ${reason}. Your saved session was kept, so try again once the connection is back.`
659
+ );
660
+ }
661
+ if (rotated === null) {
662
+ clearAuthFields();
663
+ return null;
664
+ }
665
+ writeConfig({
666
+ accessToken: rotated.accessToken,
667
+ accessTokenExpiresAt: rotated.accessTokenExpiresAt,
668
+ // A server that rotates no new refresh token leaves the current one in
669
+ // force; overwriting it with null would end the session on the next call.
670
+ refreshToken: rotated.refreshToken ?? current.refreshToken
671
+ });
672
+ return rotated.accessToken;
673
+ });
674
+ }
675
+ async function getAuthHeaders() {
676
+ const token = await resolveAccessToken();
677
+ if (!token) return {};
678
+ return { Authorization: `Bearer ${token}` };
679
+ }
680
+ function credentialSource() {
681
+ if (process.env.WALKEROS_TOKEN) return "env";
682
+ const config = readConfig();
683
+ if (config?.token || config?.accessToken) return "config";
684
+ return null;
685
+ }
686
+ function resolveRunToken() {
687
+ return resolveDeployToken() ?? resolveToken()?.token ?? null;
688
+ }
689
+ function requireProjectId() {
690
+ const projectId = process.env.WALKEROS_PROJECT_ID || getDefaultProject();
691
+ if (!projectId)
692
+ throw new Error(
693
+ "No project selected. Set WALKEROS_PROJECT_ID or configure a default project."
694
+ );
695
+ return projectId;
696
+ }
697
+ var REFRESH_SKEW_MS, legacyNoticeShown;
698
+ var init_auth = __esm({
699
+ "src/core/auth.ts"() {
700
+ "use strict";
701
+ init_config_file();
702
+ init_config_lock();
703
+ init_oauth_client();
704
+ REFRESH_SKEW_MS = 6e4;
705
+ legacyNoticeShown = false;
706
+ }
707
+ });
708
+
351
709
  // src/core/client-context.ts
352
710
  function setClientContext(input) {
353
711
  const envType = process.env.WALKEROS_CLIENT_TYPE;
@@ -395,7 +753,7 @@ function buildHeaders(token, headers) {
395
753
  }
396
754
  async function apiFetch(path20, init) {
397
755
  const baseUrl = resolveAppUrl();
398
- const token = resolveToken()?.token;
756
+ const token = await resolveAccessToken();
399
757
  return fetch(`${baseUrl}${path20}`, {
400
758
  ...init,
401
759
  headers: buildHeaders(token, init?.headers)
@@ -413,7 +771,7 @@ async function publicFetch(path20, init) {
413
771
  }
414
772
  async function deployFetch(path20, init) {
415
773
  const baseUrl = resolveAppUrl();
416
- const token = resolveDeployToken() ?? resolveToken()?.token;
774
+ const token = resolveDeployToken() ?? await resolveAccessToken();
417
775
  if (!token)
418
776
  throw new Error(
419
777
  "No authentication token available. Set WALKEROS_DEPLOY_TOKEN or run walkeros auth login."
@@ -427,6 +785,7 @@ var init_http = __esm({
427
785
  "src/core/http.ts"() {
428
786
  "use strict";
429
787
  init_config_file();
788
+ init_auth();
430
789
  init_client_context();
431
790
  }
432
791
  });
@@ -442,8 +801,15 @@ function isUrl(str) {
442
801
  return false;
443
802
  }
444
803
  }
804
+ function isAppOrigin(url) {
805
+ try {
806
+ return new URL(url).origin === new URL(resolveAppUrl()).origin;
807
+ } catch {
808
+ return false;
809
+ }
810
+ }
445
811
  async function fetchContentString(url) {
446
- const token = resolveToken()?.token;
812
+ const token = isAppOrigin(url) ? await resolveAccessToken() : null;
447
813
  const response = await fetch(url, {
448
814
  headers: mergeAuthHeaders(token)
449
815
  });
@@ -516,6 +882,7 @@ var init_utils = __esm({
516
882
  "src/config/utils.ts"() {
517
883
  "use strict";
518
884
  init_http();
885
+ init_auth();
519
886
  init_config_file();
520
887
  }
521
888
  });
@@ -720,34 +1087,6 @@ var init_stdin = __esm({
720
1087
  }
721
1088
  });
722
1089
 
723
- // src/core/auth.ts
724
- function getToken() {
725
- const result = resolveToken();
726
- return result?.token;
727
- }
728
- function getAuthHeaders() {
729
- const token = getToken();
730
- if (!token) return {};
731
- return { Authorization: `Bearer ${token}` };
732
- }
733
- function resolveRunToken() {
734
- return resolveDeployToken() ?? resolveToken()?.token ?? null;
735
- }
736
- function requireProjectId() {
737
- const projectId = process.env.WALKEROS_PROJECT_ID || getDefaultProject();
738
- if (!projectId)
739
- throw new Error(
740
- "No project selected. Set WALKEROS_PROJECT_ID or configure a default project."
741
- );
742
- return projectId;
743
- }
744
- var init_auth = __esm({
745
- "src/core/auth.ts"() {
746
- "use strict";
747
- init_config_file();
748
- }
749
- });
750
-
751
1090
  // src/core/sse.ts
752
1091
  function parseSSEEvents(buffer) {
753
1092
  const events = [];
@@ -1222,13 +1561,13 @@ function validateReference(type, name, ref) {
1222
1561
  return;
1223
1562
  }
1224
1563
  const hasPackage = !!ref.package;
1225
- const hasCode = hasCodeReference(ref.code);
1226
- if (hasPackage && hasCode) {
1564
+ const hasCode2 = hasCodeReference(ref.code);
1565
+ if (hasPackage && hasCode2) {
1227
1566
  throw new Error(
1228
1567
  `${type} "${name}": Cannot specify both package and code. Use one or the other.`
1229
1568
  );
1230
1569
  }
1231
- if (!hasPackage && !hasCode) {
1570
+ if (!hasPackage && !hasCode2) {
1232
1571
  throw new Error(`${type} "${name}": Must specify either package or code.`);
1233
1572
  }
1234
1573
  }
@@ -1500,7 +1839,7 @@ function backoffForAttempt(index) {
1500
1839
  const spread = base * JITTER;
1501
1840
  return base + (Math.random() * 2 - 1) * spread;
1502
1841
  }
1503
- function delay(ms) {
1842
+ function delay2(ms) {
1504
1843
  return new Promise((resolve5) => {
1505
1844
  setTimeout(resolve5, ms);
1506
1845
  });
@@ -1542,7 +1881,7 @@ async function withPacoteRetry(fn, label, options = {}) {
1542
1881
  maxTotalMs - (Date.now() - start)
1543
1882
  );
1544
1883
  if (sleepMs <= 0) break;
1545
- await delay(sleepMs);
1884
+ await delay2(sleepMs);
1546
1885
  }
1547
1886
  throw new Error(
1548
1887
  `Failed ${label} after ${made} attempts: ${describeError(lastError)}`
@@ -3969,14 +4308,14 @@ __export(cache_exports, {
3969
4308
  });
3970
4309
  import {
3971
4310
  existsSync as existsSync3,
3972
- mkdirSync as mkdirSync4,
4311
+ mkdirSync as mkdirSync5,
3973
4312
  copyFileSync,
3974
4313
  writeFileSync as writeFileSync4,
3975
4314
  readFileSync as readFileSync3
3976
4315
  } from "fs";
3977
4316
  import { join as join6 } from "path";
3978
4317
  function writeCache(cacheDir, bundlePath, configContent, version) {
3979
- mkdirSync4(cacheDir, { recursive: true });
4318
+ mkdirSync5(cacheDir, { recursive: true });
3980
4319
  copyFileSync(bundlePath, join6(cacheDir, "flow.mjs"));
3981
4320
  writeFileSync4(join6(cacheDir, "config.json"), configContent, "utf-8");
3982
4321
  const meta = { version, timestamp: Date.now() };
@@ -4057,7 +4396,7 @@ var env_file_exports = {};
4057
4396
  __export(env_file_exports, {
4058
4397
  loadEnvFile: () => loadEnvFile
4059
4398
  });
4060
- import { readFileSync as readFileSync5, statSync } from "fs";
4399
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
4061
4400
  function parseLine(line) {
4062
4401
  const trimmed = line.trim();
4063
4402
  if (trimmed === "" || trimmed.startsWith("#")) return void 0;
@@ -4072,7 +4411,7 @@ function parseLine(line) {
4072
4411
  return { key, value };
4073
4412
  }
4074
4413
  function loadEnvFile(filePath) {
4075
- const stats = statSync(filePath);
4414
+ const stats = statSync2(filePath);
4076
4415
  if (process.platform !== "win32") {
4077
4416
  const mode = stats.mode & 511;
4078
4417
  if (mode & 36) {
@@ -4316,13 +4655,13 @@ function installTimerInterception(options = {}) {
4316
4655
  setInterval: Reflect.get(target, "setInterval"),
4317
4656
  clearInterval: Reflect.get(target, "clearInterval")
4318
4657
  });
4319
- const trackedSetTimeout = (callback, delay3, ...args) => {
4658
+ const trackedSetTimeout = (callback, delay5, ...args) => {
4320
4659
  if (typeof callback !== "function") return 0;
4321
4660
  const id = nextId++;
4322
4661
  pending.set(id, {
4323
4662
  id,
4324
4663
  callback,
4325
- delay: delay3 ?? 0,
4664
+ delay: delay5 ?? 0,
4326
4665
  type: "timeout",
4327
4666
  args,
4328
4667
  cleared: false
@@ -4335,13 +4674,13 @@ function installTimerInterception(options = {}) {
4335
4674
  const entry = pending.get(numId);
4336
4675
  if (entry) entry.cleared = true;
4337
4676
  };
4338
- const trackedSetInterval = (callback, delay3, ...args) => {
4677
+ const trackedSetInterval = (callback, delay5, ...args) => {
4339
4678
  if (typeof callback !== "function") return 0;
4340
4679
  const id = nextId++;
4341
4680
  pending.set(id, {
4342
4681
  id,
4343
4682
  callback,
4344
- delay: delay3 ?? 0,
4683
+ delay: delay5 ?? 0,
4345
4684
  type: "interval",
4346
4685
  args,
4347
4686
  cleared: false
@@ -5688,14 +6027,14 @@ import { join as join8 } from "path";
5688
6027
  import { Level as Level2 } from "@walkeros/core";
5689
6028
 
5690
6029
  // src/commands/run/error-sink.ts
5691
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
6030
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
5692
6031
  import { join as join4 } from "path";
5693
6032
  function errorSinkPath(cacheDir) {
5694
6033
  return join4(cacheDir, "errors.jsonl");
5695
6034
  }
5696
6035
  function ensureSinkDir(cacheDir) {
5697
6036
  try {
5698
- mkdirSync2(cacheDir, { recursive: true });
6037
+ mkdirSync3(cacheDir, { recursive: true });
5699
6038
  } catch {
5700
6039
  }
5701
6040
  }
@@ -5833,7 +6172,7 @@ init_stdin();
5833
6172
  import {
5834
6173
  createReadStream,
5835
6174
  existsSync as existsSync2,
5836
- mkdirSync as mkdirSync3,
6175
+ mkdirSync as mkdirSync4,
5837
6176
  rmSync,
5838
6177
  writeFileSync as writeFileSync3
5839
6178
  } from "fs";
@@ -5903,7 +6242,7 @@ function backoffForAttempt2(index) {
5903
6242
  const spread = base * JITTER2;
5904
6243
  return base + (Math.random() * 2 - 1) * spread;
5905
6244
  }
5906
- function delay2(ms) {
6245
+ function delay3(ms) {
5907
6246
  return new Promise((resolve5) => {
5908
6247
  setTimeout(resolve5, ms);
5909
6248
  });
@@ -5950,7 +6289,7 @@ async function fetchWithRetry(url, options = {}) {
5950
6289
  maxTotalMs - (Date.now() - start)
5951
6290
  );
5952
6291
  if (sleepMs <= 0) break;
5953
- await delay2(sleepMs);
6292
+ await delay3(sleepMs);
5954
6293
  }
5955
6294
  const cause = lastReason ? describeReason(lastReason) : "no attempts made";
5956
6295
  throw new Error(`Fetch failed after ${made} attempts: ${cause}`);
@@ -5977,7 +6316,7 @@ function isArchive(value, contentType) {
5977
6316
  function writeBundleToDisk(writePath, content) {
5978
6317
  const dir = dirname4(writePath);
5979
6318
  if (!existsSync2(dir)) {
5980
- mkdirSync3(dir, { recursive: true });
6319
+ mkdirSync4(dir, { recursive: true });
5981
6320
  }
5982
6321
  writeFileSync3(writePath, content, "utf-8");
5983
6322
  }
@@ -6000,7 +6339,7 @@ async function fetchTextToDisk(response, writePath) {
6000
6339
  }
6001
6340
  async function extractToDir(source, destDir) {
6002
6341
  if (!existsSync2(destDir)) {
6003
- mkdirSync3(destDir, { recursive: true });
6342
+ mkdirSync4(destDir, { recursive: true });
6004
6343
  }
6005
6344
  const entryPath = join5(destDir, ARCHIVE_ENTRY);
6006
6345
  for (const member of [ARCHIVE_ENTRY, "node_modules", "package.json"]) {
@@ -6655,93 +6994,93 @@ init_asset_resolver();
6655
6994
  import { existsSync as existsSync4 } from "fs";
6656
6995
 
6657
6996
  // src/schemas/primitives.ts
6658
- import { z } from "@walkeros/core/dev";
6659
- var PortSchema = z.number().int("Port must be an integer").min(1, "Port must be at least 1").max(65535, "Port must be at most 65535").describe("HTTP server port number");
6660
- var FilePathSchema = z.string().min(1, "File path cannot be empty").describe("Path to configuration file");
6997
+ import { z as z2 } from "@walkeros/core/dev";
6998
+ var PortSchema = z2.number().int("Port must be an integer").min(1, "Port must be at least 1").max(65535, "Port must be at most 65535").describe("HTTP server port number");
6999
+ var FilePathSchema = z2.string().min(1, "File path cannot be empty").describe("Path to configuration file");
6661
7000
 
6662
7001
  // src/schemas/run.ts
6663
- import { z as z2 } from "@walkeros/core/dev";
6664
- var RunOptionsSchema = z2.object({
7002
+ import { z as z3 } from "@walkeros/core/dev";
7003
+ var RunOptionsSchema = z3.object({
6665
7004
  flow: FilePathSchema,
6666
7005
  port: PortSchema.default(8080),
6667
- flowName: z2.string().optional().describe("Specific flow name to run")
7006
+ flowName: z3.string().optional().describe("Specific flow name to run")
6668
7007
  });
6669
7008
 
6670
7009
  // src/schemas/validate.ts
6671
- import { z as z3 } from "@walkeros/core/dev";
6672
- var ValidationTypeSchema = z3.enum(["contract", "event", "flow", "mapping"]).describe('Validation type: "event", "flow", "mapping", or "contract"');
6673
- var ValidateOptionsSchema = z3.object({
6674
- flow: z3.string().optional().describe("Flow name for multi-flow configs"),
6675
- path: z3.string().optional().describe(
7010
+ import { z as z4 } from "@walkeros/core/dev";
7011
+ var ValidationTypeSchema = z4.enum(["contract", "event", "flow", "mapping"]).describe('Validation type: "event", "flow", "mapping", or "contract"');
7012
+ var ValidateOptionsSchema = z4.object({
7013
+ flow: z4.string().optional().describe("Flow name for multi-flow configs"),
7014
+ path: z4.string().optional().describe(
6676
7015
  'Entry path for package schema validation (e.g., "destinations.snowplow", "sources.browser")'
6677
7016
  )
6678
7017
  });
6679
7018
  var ValidateInputShape = {
6680
7019
  type: ValidationTypeSchema,
6681
- input: z3.string().min(1).describe("JSON string, file path, or URL to validate"),
6682
- flow: z3.string().optional().describe("Flow name for multi-flow configs"),
6683
- path: z3.string().optional().describe(
7020
+ input: z4.string().min(1).describe("JSON string, file path, or URL to validate"),
7021
+ flow: z4.string().optional().describe("Flow name for multi-flow configs"),
7022
+ path: z4.string().optional().describe(
6684
7023
  'Entry path for package schema validation (e.g., "destinations.snowplow"). When provided, validates the entry against its package JSON Schema instead of using --type.'
6685
7024
  )
6686
7025
  };
6687
- var ValidateInputSchema = z3.object(ValidateInputShape);
7026
+ var ValidateInputSchema = z4.object(ValidateInputShape);
6688
7027
 
6689
7028
  // src/schemas/bundle.ts
6690
- import { z as z4 } from "@walkeros/core/dev";
6691
- var BundleOptionsSchema = z4.object({
6692
- silent: z4.boolean().optional().describe("Suppress all output"),
6693
- verbose: z4.boolean().optional().describe("Enable verbose logging"),
6694
- stats: z4.boolean().optional().default(true).describe("Return bundle statistics"),
6695
- cache: z4.boolean().optional().default(true).describe("Enable package caching"),
6696
- flowName: z4.string().optional().describe("Flow name for multi-flow configs")
7029
+ import { z as z5 } from "@walkeros/core/dev";
7030
+ var BundleOptionsSchema = z5.object({
7031
+ silent: z5.boolean().optional().describe("Suppress all output"),
7032
+ verbose: z5.boolean().optional().describe("Enable verbose logging"),
7033
+ stats: z5.boolean().optional().default(true).describe("Return bundle statistics"),
7034
+ cache: z5.boolean().optional().default(true).describe("Enable package caching"),
7035
+ flowName: z5.string().optional().describe("Flow name for multi-flow configs")
6697
7036
  });
6698
7037
  var BundleInputShape = {
6699
7038
  configPath: FilePathSchema.describe(
6700
7039
  "Path to flow configuration file (JSON or JavaScript), URL, or inline JSON string"
6701
7040
  ),
6702
- flow: z4.string().optional().describe("Flow name for multi-flow configs"),
6703
- stats: z4.boolean().optional().default(true).describe("Return bundle statistics"),
6704
- output: z4.string().optional().describe("Output file path (defaults to config-defined)")
7041
+ flow: z5.string().optional().describe("Flow name for multi-flow configs"),
7042
+ stats: z5.boolean().optional().default(true).describe("Return bundle statistics"),
7043
+ output: z5.string().optional().describe("Output file path (defaults to config-defined)")
6705
7044
  };
6706
- var BundleInputSchema = z4.object(BundleInputShape);
7045
+ var BundleInputSchema = z5.object(BundleInputShape);
6707
7046
 
6708
7047
  // src/schemas/simulate.ts
6709
- import { z as z5 } from "@walkeros/core/dev";
6710
- var PlatformSchema = z5.enum(["web", "server"]).describe("Platform type for event processing");
6711
- var SimulateOptionsSchema = z5.object({
6712
- silent: z5.boolean().optional().describe("Suppress all output"),
6713
- verbose: z5.boolean().optional().describe("Enable verbose logging"),
6714
- json: z5.boolean().optional().describe("Format output as JSON")
7048
+ import { z as z6 } from "@walkeros/core/dev";
7049
+ var PlatformSchema = z6.enum(["web", "server"]).describe("Platform type for event processing");
7050
+ var SimulateOptionsSchema = z6.object({
7051
+ silent: z6.boolean().optional().describe("Suppress all output"),
7052
+ verbose: z6.boolean().optional().describe("Enable verbose logging"),
7053
+ json: z6.boolean().optional().describe("Format output as JSON")
6715
7054
  });
6716
7055
  var SimulateInputShape = {
6717
7056
  configPath: FilePathSchema.describe(
6718
7057
  "Path to flow configuration file, URL, or inline JSON string"
6719
7058
  ),
6720
- event: z5.string().min(1).optional().describe(
7059
+ event: z6.string().min(1).optional().describe(
6721
7060
  "Event as JSON string, file path, or URL. For sources: { content, trigger?, env? }."
6722
7061
  ),
6723
- flow: z5.string().optional().describe("Flow name for multi-flow configs"),
7062
+ flow: z6.string().optional().describe("Flow name for multi-flow configs"),
6724
7063
  platform: PlatformSchema.optional().describe("Override platform detection"),
6725
- step: z5.string().optional().describe(
7064
+ step: z6.string().optional().describe(
6726
7065
  'Step target in type.name format (e.g. "source.browser", "destination.gtag")'
6727
7066
  )
6728
7067
  };
6729
- var SimulateInputSchema = z5.object(SimulateInputShape);
7068
+ var SimulateInputSchema = z6.object(SimulateInputShape);
6730
7069
 
6731
7070
  // src/schemas/push.ts
6732
- import { z as z6 } from "@walkeros/core/dev";
6733
- var PushOptionsSchema = z6.object({
6734
- silent: z6.boolean().optional().describe("Suppress all output"),
6735
- verbose: z6.boolean().optional().describe("Enable verbose logging"),
6736
- json: z6.boolean().optional().describe("Format output as JSON")
7071
+ import { z as z7 } from "@walkeros/core/dev";
7072
+ var PushOptionsSchema = z7.object({
7073
+ silent: z7.boolean().optional().describe("Suppress all output"),
7074
+ verbose: z7.boolean().optional().describe("Enable verbose logging"),
7075
+ json: z7.boolean().optional().describe("Format output as JSON")
6737
7076
  });
6738
7077
  var PushInputShape = {
6739
7078
  configPath: FilePathSchema.describe("Path to flow configuration file"),
6740
- event: z6.string().min(1).describe("Event as JSON string, file path, or URL"),
6741
- flow: z6.string().optional().describe("Flow name for multi-flow configs"),
7079
+ event: z7.string().min(1).describe("Event as JSON string, file path, or URL"),
7080
+ flow: z7.string().optional().describe("Flow name for multi-flow configs"),
6742
7081
  platform: PlatformSchema.optional().describe("Override platform detection")
6743
7082
  };
6744
- var PushInputSchema = z6.object(PushInputShape);
7083
+ var PushInputSchema = z7.object(PushInputShape);
6745
7084
 
6746
7085
  // src/commands/run/validators.ts
6747
7086
  function validateFlowFile(filePath) {
@@ -8262,7 +8601,7 @@ function validateMapping(input) {
8262
8601
  // src/commands/validate/validators/entry.ts
8263
8602
  import Ajv from "ajv";
8264
8603
  import { fetchPackageSchema } from "@walkeros/core";
8265
- var CLIENT_HEADER = "walkeros-cli/4.5.0";
8604
+ var CLIENT_HEADER = "walkeros-cli/4.6.0-next-1788817472881";
8266
8605
  var SECTIONS = ["destinations", "sources", "transformers"];
8267
8606
  function resolveEntry(path20, flowConfig) {
8268
8607
  const flows = flowConfig.flows;
@@ -8514,29 +8853,23 @@ async function validateCommand(options) {
8514
8853
 
8515
8854
  // src/commands/login/index.ts
8516
8855
  init_cli_logger();
8856
+ init_oauth_client();
8517
8857
  init_config_file();
8518
- import { hostname } from "os";
8519
- import { z as z7 } from "zod";
8520
- var DeviceCodeResponseSchema = z7.object({
8521
- deviceCode: z7.string().min(1),
8522
- userCode: z7.string().min(1),
8523
- verificationUri: z7.string().min(1),
8524
- verificationUriComplete: z7.string().optional(),
8525
- // Server protocol allows 0 for both (e.g. fast retry / already expired).
8526
- expiresIn: z7.number().int().nonnegative(),
8527
- interval: z7.number().int().nonnegative()
8528
- });
8529
- var TokenResponseSchema = z7.object({
8530
- token: z7.string().min(1),
8531
- email: z7.string().min(1)
8532
- });
8858
+ import { z as z8 } from "zod";
8533
8859
  var POLL_TIMEOUT_BUFFER_MS = 5e3;
8534
- var DEFAULT_POLL_TIMEOUT_MS = 6e4;
8535
8860
  var DEFAULT_POLL_INTERVAL_MS = 5e3;
8861
+ var MIN_SERVER_POLL_INTERVAL_MS = 1e3;
8862
+ var DEFAULT_POLL_TIMEOUT_MS = 9e5;
8863
+ var SLOW_DOWN_STEP_MS = 5e3;
8864
+ var TIMED_OUT = "Authorization timed out. Please try again.";
8865
+ var WhoamiSchema = z8.object({ email: z8.string().min(1) });
8536
8866
  async function openInBrowser(url) {
8537
8867
  const { default: open } = await import("open");
8538
8868
  await open(url);
8539
8869
  }
8870
+ function delay4(ms) {
8871
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
8872
+ }
8540
8873
  async function loginCommand(options) {
8541
8874
  const logger = createCLILogger(options);
8542
8875
  try {
@@ -8544,8 +8877,11 @@ async function loginCommand(options) {
8544
8877
  if (options.json) {
8545
8878
  logger.json(result);
8546
8879
  } else if (result.success) {
8547
- logger.info(`Logged in as ${result.email}`);
8548
- logger.info(`Token stored in ${result.configPath}`);
8880
+ if (result.email) logger.info(`Logged in as ${result.email}`);
8881
+ else logger.info("Logged in.");
8882
+ logger.info(`Session stored in ${result.configPath}`);
8883
+ } else if (result.error) {
8884
+ logger.error(result.error);
8549
8885
  }
8550
8886
  process.exit(result.success ? 0 : 1);
8551
8887
  } catch (error) {
@@ -8558,264 +8894,141 @@ async function loginCommand(options) {
8558
8894
  process.exit(1);
8559
8895
  }
8560
8896
  }
8561
- async function requestDeviceCode(options = {}) {
8562
- const appUrl = options.url || resolveAppUrl();
8563
- const f = options.fetch ?? globalThis.fetch;
8564
- const response = await f(`${appUrl}/api/auth/device/code`, {
8565
- method: "POST",
8566
- headers: { "Content-Type": "application/json" },
8567
- body: JSON.stringify({})
8568
- });
8569
- if (!response.ok) {
8570
- throw new Error("Failed to request device code");
8571
- }
8572
- let raw;
8897
+ async function fetchEmail(appUrl, accessToken, fetchFn) {
8573
8898
  try {
8574
- raw = await response.json();
8899
+ const response = await fetchFn(`${appUrl}/api/auth/whoami`, {
8900
+ headers: { Authorization: `Bearer ${accessToken}` }
8901
+ });
8902
+ if (!response.ok) return void 0;
8903
+ const parsed = WhoamiSchema.safeParse(await response.json());
8904
+ return parsed.success ? parsed.data.email : void 0;
8575
8905
  } catch {
8576
- throw new Error("Malformed device code response");
8577
- }
8578
- const parsed = DeviceCodeResponseSchema.safeParse(raw);
8579
- if (!parsed.success) {
8580
- throw new Error("Malformed device code response");
8906
+ return void 0;
8581
8907
  }
8582
- return {
8583
- deviceCode: parsed.data.deviceCode,
8584
- userCode: parsed.data.userCode,
8585
- verificationUri: parsed.data.verificationUri,
8586
- verificationUriComplete: parsed.data.verificationUriComplete,
8587
- expiresIn: parsed.data.expiresIn,
8588
- interval: parsed.data.interval
8589
- };
8590
8908
  }
8591
- async function pollForToken(deviceCode, options = {}) {
8909
+ async function persistSession(appUrl, tokens, fetchFn) {
8910
+ writeConfig({
8911
+ accessToken: tokens.accessToken,
8912
+ accessTokenExpiresAt: tokens.accessTokenExpiresAt,
8913
+ refreshToken: tokens.refreshToken ?? void 0,
8914
+ appUrl,
8915
+ // Drop the static token this session replaces, so the deprecated path
8916
+ // cannot outlive the login that retired it.
8917
+ token: void 0
8918
+ });
8919
+ const email = await fetchEmail(appUrl, tokens.accessToken, fetchFn);
8920
+ writeConfig({ email });
8921
+ }
8922
+ async function completeDeviceLogin(deviceCode, options = {}) {
8923
+ const fetchFn = options.fetch ?? globalThis.fetch;
8592
8924
  const appUrl = options.url || resolveAppUrl();
8593
- const f = options.fetch ?? globalThis.fetch;
8594
- const timeoutMs = options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
8925
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS);
8595
8926
  let intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
8596
- const deadline = Date.now() + timeoutMs;
8597
- while (Date.now() < deadline) {
8598
- await new Promise((r) => setTimeout(r, intervalMs));
8599
- if (Date.now() >= deadline) break;
8600
- const remaining = Math.max(1, deadline - Date.now());
8601
- const controller = new AbortController();
8602
- const timeoutHandle = setTimeout(() => controller.abort(), remaining);
8603
- let tokenResponse;
8604
- try {
8605
- tokenResponse = await f(`${appUrl}/api/auth/device/token`, {
8606
- method: "POST",
8607
- headers: { "Content-Type": "application/json" },
8608
- body: JSON.stringify({ deviceCode, hostname: hostname() }),
8609
- signal: controller.signal
8610
- });
8611
- } catch (err) {
8612
- clearTimeout(timeoutHandle);
8613
- if (err instanceof Error && err.name === "AbortError") {
8614
- break;
8615
- }
8616
- throw err;
8617
- } finally {
8618
- clearTimeout(timeoutHandle);
8619
- }
8620
- const data = await safeJsonParse(tokenResponse);
8621
- if (data === MALFORMED) {
8622
- return {
8623
- success: false,
8624
- status: "error",
8625
- error: "Server returned malformed response"
8626
- };
8627
- }
8628
- if (tokenResponse.ok) {
8629
- if (data.token === void 0) {
8630
- continue;
8631
- }
8632
- const tokenParsed = TokenResponseSchema.safeParse(data);
8633
- if (!tokenParsed.success) {
8634
- return {
8635
- success: false,
8636
- status: "error",
8637
- error: "Server returned malformed token response"
8638
- };
8639
- }
8640
- const { token, email } = tokenParsed.data;
8641
- writeConfig({ token, email, appUrl });
8642
- const configPath = getConfigPath();
8643
- return {
8644
- success: true,
8645
- status: "authenticated",
8646
- email,
8647
- configPath
8648
- };
8649
- }
8650
- if (data.error === "authorization_pending") continue;
8651
- if (data.error === "slow_down") {
8652
- intervalMs += 5e3;
8927
+ let attempts = 0;
8928
+ let waiting = "pending";
8929
+ while (Date.now() + intervalMs <= deadline) {
8930
+ if (options.maxPollAttempts !== void 0 && attempts >= options.maxPollAttempts)
8931
+ break;
8932
+ attempts += 1;
8933
+ await delay4(intervalMs);
8934
+ const poll = await pollDeviceToken(
8935
+ appUrl,
8936
+ deviceCode,
8937
+ fetchFn,
8938
+ AbortSignal.timeout(Math.max(1, deadline - Date.now()))
8939
+ );
8940
+ if (poll.status === "pending") {
8941
+ waiting = "pending";
8653
8942
  continue;
8654
8943
  }
8655
- const errField = data.error;
8656
- let errorMsg;
8657
- if (typeof errField === "string") {
8658
- errorMsg = errField;
8659
- } else if (errField && typeof errField === "object" && "message" in errField && typeof errField.message === "string") {
8660
- errorMsg = errField.message;
8661
- } else {
8662
- errorMsg = "Authorization failed";
8663
- }
8664
- return { success: false, status: "error", error: errorMsg };
8665
- }
8666
- return { success: false, status: "pending" };
8667
- }
8668
- var MALFORMED = /* @__PURE__ */ Symbol("malformed-json");
8669
- async function safeJsonParse(response) {
8670
- try {
8671
- const parsed = await response.json();
8672
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
8673
- return parsed;
8944
+ if (poll.status === "slow_down") {
8945
+ waiting = "slow_down";
8946
+ intervalMs += SLOW_DOWN_STEP_MS;
8947
+ continue;
8674
8948
  }
8675
- return MALFORMED;
8676
- } catch {
8677
- return MALFORMED;
8949
+ if (poll.status !== "ok") return poll;
8950
+ await persistSession(appUrl, poll.tokens, fetchFn);
8951
+ return { status: "ok" };
8678
8952
  }
8953
+ return { status: waiting };
8679
8954
  }
8680
8955
  async function login(options = {}) {
8681
- const fetchOption = options.fetch ?? globalThis.fetch;
8682
- const urlOption = options.url;
8683
- let codeResult;
8956
+ const fetchFn = options.fetch ?? globalThis.fetch;
8957
+ const appUrl = options.url || resolveAppUrl();
8958
+ let authorization;
8684
8959
  try {
8685
- codeResult = await requestDeviceCode({
8686
- url: urlOption,
8687
- fetch: fetchOption
8688
- });
8960
+ authorization = await startDeviceAuthorization(appUrl, fetchFn);
8689
8961
  } catch {
8690
8962
  return { success: false, error: "Failed to request device code" };
8691
8963
  }
8692
- const {
8693
- userCode,
8694
- verificationUri,
8695
- verificationUriComplete,
8696
- expiresIn,
8697
- interval,
8698
- deviceCode
8699
- } = codeResult;
8700
- const prompt = (msg) => process.stderr.write(msg + "\n");
8964
+ const target = authorization.verificationUriComplete;
8965
+ const prompt = (message) => process.stderr.write(message + "\n");
8701
8966
  prompt(`
8702
- ! Your one-time code: ${userCode}`);
8703
- prompt(` Authorize here: ${verificationUriComplete || verificationUri}
8967
+ ! Your one-time code: ${authorization.userCode}`);
8968
+ prompt(` Authorize here: ${target}
8704
8969
  `);
8705
8970
  const opener = options.openUrl ?? openInBrowser;
8706
8971
  try {
8707
- await opener(verificationUriComplete || verificationUri);
8972
+ await opener(target);
8708
8973
  prompt(" Opening browser...");
8709
8974
  } catch {
8710
8975
  prompt(" Could not open browser. Visit the URL manually.");
8711
8976
  }
8712
8977
  prompt(" Waiting for authorization... (press Ctrl+C to cancel)\n");
8713
- const timeoutMs = expiresIn * 1e3 + POLL_TIMEOUT_BUFFER_MS;
8714
- const intervalMs = (interval ?? 5) * 1e3;
8715
- if (options.maxPollAttempts !== void 0) {
8716
- const appUrl = urlOption || resolveAppUrl();
8717
- const f = fetchOption;
8718
- let pollInterval = intervalMs;
8719
- let attempts = 0;
8720
- const deadline = Date.now() + timeoutMs;
8721
- while (Date.now() < deadline && attempts < options.maxPollAttempts) {
8722
- attempts++;
8723
- await new Promise((r) => setTimeout(r, pollInterval));
8724
- const remaining = Math.max(1, deadline - Date.now());
8725
- const controller = new AbortController();
8726
- const timeoutHandle = setTimeout(() => controller.abort(), remaining);
8727
- let tokenResponse;
8728
- try {
8729
- tokenResponse = await f(`${appUrl}/api/auth/device/token`, {
8730
- method: "POST",
8731
- headers: { "Content-Type": "application/json" },
8732
- body: JSON.stringify({ deviceCode, hostname: hostname() }),
8733
- signal: controller.signal
8734
- });
8735
- } catch (err) {
8736
- clearTimeout(timeoutHandle);
8737
- if (err instanceof Error && err.name === "AbortError") {
8738
- break;
8739
- }
8740
- throw err;
8741
- } finally {
8742
- clearTimeout(timeoutHandle);
8743
- }
8744
- const data = await safeJsonParse(tokenResponse);
8745
- if (data === MALFORMED) {
8746
- return {
8747
- success: false,
8748
- error: "Server returned malformed response"
8749
- };
8750
- }
8751
- if (tokenResponse.ok) {
8752
- if (data.token === void 0) {
8753
- continue;
8754
- }
8755
- const tokenParsed = TokenResponseSchema.safeParse(data);
8756
- if (!tokenParsed.success) {
8757
- return {
8758
- success: false,
8759
- error: "Server returned malformed token response"
8760
- };
8761
- }
8762
- const { token, email } = tokenParsed.data;
8763
- writeConfig({ token, email, appUrl });
8764
- const configPath = getConfigPath();
8765
- return { success: true, email, configPath };
8766
- }
8767
- if (data.error === "authorization_pending") continue;
8768
- if (data.error === "slow_down") {
8769
- pollInterval += 5e3;
8770
- continue;
8771
- }
8772
- const errField = data.error;
8773
- const errorMsg = typeof errField === "string" ? errField : "Authorization failed";
8774
- return { success: false, error: errorMsg };
8775
- }
8776
- return {
8777
- success: false,
8778
- error: "Authorization timed out. Please try again."
8779
- };
8780
- }
8781
- const pollResult = await pollForToken(deviceCode, {
8782
- url: urlOption,
8783
- fetch: fetchOption,
8784
- timeoutMs,
8785
- intervalMs
8978
+ const outcome = await completeDeviceLogin(authorization.deviceCode, {
8979
+ url: appUrl,
8980
+ fetch: fetchFn,
8981
+ timeoutMs: authorization.expiresIn * 1e3 + POLL_TIMEOUT_BUFFER_MS,
8982
+ // Clamped here, at the one place a server's number enters the loop. The
8983
+ // helper takes its caller's interval as stated, which is what keeps a
8984
+ // stated 0 from becoming a poll flood without making the helper's own
8985
+ // option untestably slow.
8986
+ intervalMs: options.pollIntervalMs ?? Math.max(MIN_SERVER_POLL_INTERVAL_MS, authorization.interval * 1e3),
8987
+ ...options.maxPollAttempts !== void 0 ? { maxPollAttempts: options.maxPollAttempts } : {}
8786
8988
  });
8787
- if (pollResult.success) {
8788
- return {
8789
- success: true,
8790
- email: pollResult.email,
8791
- configPath: pollResult.configPath
8792
- };
8793
- }
8794
- if (pollResult.status === "error") {
8795
- return { success: false, error: pollResult.error };
8989
+ switch (outcome.status) {
8990
+ case "ok": {
8991
+ const email = readConfig()?.email;
8992
+ return {
8993
+ success: true,
8994
+ ...email ? { email } : {},
8995
+ configPath: getConfigPath()
8996
+ };
8997
+ }
8998
+ case "denied":
8999
+ return { success: false, error: "Authorization was denied." };
9000
+ case "error":
9001
+ return { success: false, error: outcome.error };
9002
+ default:
9003
+ return { success: false, error: TIMED_OUT };
8796
9004
  }
8797
- return {
8798
- success: false,
8799
- error: "Authorization timed out. Please try again."
8800
- };
8801
9005
  }
8802
9006
 
8803
9007
  // src/commands/logout/index.ts
8804
9008
  init_cli_logger();
9009
+ init_oauth_client();
8805
9010
  init_config_file();
8806
9011
  async function logoutCommand(options) {
8807
9012
  const logger = createCLILogger(options);
8808
- const deleted = deleteConfig();
9013
+ const { deleted } = await logout();
8809
9014
  const configPath = getConfigPath();
8810
9015
  if (options.json) {
8811
9016
  logger.json({ success: true, deleted });
8812
9017
  } else if (deleted) {
8813
- logger.info(`Logged out. Token removed from ${configPath}`);
9018
+ logger.info(`Logged out. Session removed from ${configPath}`);
8814
9019
  } else {
8815
9020
  logger.info("No stored credentials found.");
8816
9021
  }
8817
9022
  process.exit(0);
8818
9023
  }
9024
+ async function logout() {
9025
+ const config = readConfig();
9026
+ const appUrl = resolveAppUrl();
9027
+ if (config?.refreshToken) {
9028
+ await revokeRefreshToken(appUrl, config.refreshToken);
9029
+ }
9030
+ return { deleted: deleteConfig() };
9031
+ }
8819
9032
 
8820
9033
  // src/core/api-client.ts
8821
9034
  init_auth();
@@ -8827,8 +9040,8 @@ import createClient from "openapi-fetch";
8827
9040
  init_config_file();
8828
9041
  import { createHash } from "crypto";
8829
9042
  import semver4 from "semver";
8830
- var bakedContractVersion = true ? "4.3.0" : PLACEHOLDER;
8831
- var bakedContractHash = true ? "78d9f32be512ebb51e8c6c262aab05dd4eda28a84269fc53ff87b88e41940e6d" : "";
9043
+ var bakedContractVersion = true ? "4.5.1" : PLACEHOLDER;
9044
+ var bakedContractHash = true ? "335b9bd11acaf7c516e3fcd47c74b4ab212032375e00f2b58b8459f4cb30e3a6" : "";
8832
9045
  function isRecord3(value) {
8833
9046
  return value !== null && typeof value === "object" && !Array.isArray(value);
8834
9047
  }
@@ -8939,17 +9152,26 @@ function emitDriftWarning(responseHeaders) {
8939
9152
  );
8940
9153
  }
8941
9154
  function createApiClient() {
8942
- const token = getToken();
8943
- if (!token) throw new Error("WALKEROS_TOKEN not set.");
8944
9155
  const client = createClient({
8945
9156
  baseUrl: resolveAppUrl(),
8946
9157
  headers: {
8947
- Authorization: `Bearer ${token}`,
8948
9158
  "Content-Type": "application/json",
8949
9159
  ...clientContextHeaders()
8950
9160
  }
8951
9161
  });
8952
9162
  client.use({
9163
+ // Authorization is attached per request, not at construction: the stdio
9164
+ // MCP server builds one client and keeps it for hours, so a token captured
9165
+ // here would go stale and never pick up a refresh.
9166
+ async onRequest({ request }) {
9167
+ const token = await resolveAccessToken();
9168
+ if (!token)
9169
+ throw new Error("Not authenticated. Run `walkeros login` first.");
9170
+ request.headers.set("Authorization", `Bearer ${token}`);
9171
+ return request;
9172
+ },
9173
+ // Surface contract drift once per process from any response's version
9174
+ // headers. openapi-fetch ^0.17 supports `use({ onResponse })`.
8953
9175
  onResponse({ response }) {
8954
9176
  emitDriftWarning(response.headers);
8955
9177
  return void 0;
@@ -10212,6 +10434,7 @@ async function wrapSkeleton(options) {
10212
10434
 
10213
10435
  // src/index.ts
10214
10436
  init_auth();
10437
+ init_oauth_client();
10215
10438
  init_http();
10216
10439
  init_client_context();
10217
10440
 
@@ -10361,7 +10584,7 @@ async function listJourneys(options) {
10361
10584
  init_auth();
10362
10585
  init_http();
10363
10586
  init_output();
10364
- init_config_file();
10587
+ init_auth();
10365
10588
  async function startObserveSession(options) {
10366
10589
  const pid = options.projectId ?? requireProjectId();
10367
10590
  const body = {
@@ -10413,6 +10636,149 @@ async function endObserveSession(options) {
10413
10636
  }
10414
10637
  }
10415
10638
 
10639
+ // src/commands/hub/index.ts
10640
+ init_auth();
10641
+ init_http();
10642
+ async function readJson2(response, fallback) {
10643
+ if (!response.ok) {
10644
+ const body = await response.json().catch(() => ({}));
10645
+ throwApiResponseError(response, body, fallback);
10646
+ }
10647
+ return response.json();
10648
+ }
10649
+ async function listReleases(options) {
10650
+ const pid = options.projectId ?? requireProjectId();
10651
+ const params = new URLSearchParams({ rationale: "true" });
10652
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
10653
+ if (options.offset !== void 0)
10654
+ params.set("offset", String(options.offset));
10655
+ const response = await apiFetch(
10656
+ `/api/projects/${pid}/flows/${options.flowId}/releases?${params.toString()}`
10657
+ );
10658
+ return readJson2(response, "Failed to list releases");
10659
+ }
10660
+ async function getRelease(options) {
10661
+ const pid = options.projectId ?? requireProjectId();
10662
+ const segment = "versionId" in options.ref ? options.ref.versionId : String(options.ref.versionNumber);
10663
+ const response = await apiFetch(
10664
+ `/api/projects/${pid}/flows/${options.flowId}/releases/${encodeURIComponent(segment)}`
10665
+ );
10666
+ return readJson2(response, "Failed to read release");
10667
+ }
10668
+ async function listStepHistory(options) {
10669
+ const pid = options.projectId ?? requireProjectId();
10670
+ const params = new URLSearchParams({ step: options.step });
10671
+ if (options.flow !== void 0) params.set("flow", options.flow);
10672
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
10673
+ const response = await apiFetch(
10674
+ `/api/projects/${pid}/flows/${options.flowId}/releases/step-history?${params.toString()}`
10675
+ );
10676
+ return readJson2(response, "Failed to read step history");
10677
+ }
10678
+ async function setReleaseRationale(options) {
10679
+ const pid = options.projectId ?? requireProjectId();
10680
+ const response = await apiFetch(
10681
+ `/api/projects/${pid}/flows/${options.flowId}/releases/annotations`,
10682
+ {
10683
+ method: "PUT",
10684
+ headers: { "Content-Type": "application/json" },
10685
+ body: JSON.stringify({
10686
+ versionId: options.versionId,
10687
+ humanText: options.text
10688
+ })
10689
+ }
10690
+ );
10691
+ return readJson2(response, "Failed to write release rationale");
10692
+ }
10693
+ async function listThreads(options) {
10694
+ const pid = options.projectId ?? requireProjectId();
10695
+ const params = new URLSearchParams();
10696
+ if (options.anchorType !== void 0)
10697
+ params.set("anchorType", options.anchorType);
10698
+ if (options.anchorKey !== void 0)
10699
+ params.set("anchorKey", options.anchorKey);
10700
+ if (options.status !== void 0) params.set("status", options.status);
10701
+ params.set("includeMessages", options.includeMessages ? "true" : "false");
10702
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
10703
+ const response = await apiFetch(
10704
+ `/api/projects/${pid}/flows/${options.flowId}/threads?${params.toString()}`
10705
+ );
10706
+ return readJson2(response, "Failed to list threads");
10707
+ }
10708
+ async function createThread(options) {
10709
+ const pid = options.projectId ?? requireProjectId();
10710
+ const response = await apiFetch(
10711
+ `/api/projects/${pid}/flows/${options.flowId}/threads`,
10712
+ {
10713
+ method: "POST",
10714
+ headers: { "Content-Type": "application/json" },
10715
+ body: JSON.stringify({
10716
+ anchorType: options.anchorType,
10717
+ anchorKey: options.anchorKey,
10718
+ ...options.anchorLabel !== void 0 ? { anchorLabel: options.anchorLabel } : {},
10719
+ text: options.text
10720
+ })
10721
+ }
10722
+ );
10723
+ return readJson2(response, "Failed to open thread");
10724
+ }
10725
+ async function addThreadMessage(options) {
10726
+ const pid = options.projectId ?? requireProjectId();
10727
+ const response = await apiFetch(
10728
+ `/api/projects/${pid}/flows/${options.flowId}/threads/${encodeURIComponent(options.threadId)}/messages`,
10729
+ {
10730
+ method: "POST",
10731
+ headers: { "Content-Type": "application/json" },
10732
+ body: JSON.stringify({ text: options.text })
10733
+ }
10734
+ );
10735
+ return readJson2(response, "Failed to add message");
10736
+ }
10737
+ async function listKnowledge(options) {
10738
+ const pid = options.projectId ?? requireProjectId();
10739
+ const params = new URLSearchParams();
10740
+ if (options.pageKey !== void 0) params.set("pageKey", options.pageKey);
10741
+ if (options.frameId !== void 0) params.set("frameId", options.frameId);
10742
+ if (options.markId !== void 0) params.set("markId", options.markId);
10743
+ params.set("includeMessages", options.includeMessages ? "true" : "false");
10744
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
10745
+ const response = await apiFetch(
10746
+ `/api/projects/${pid}/knowledge?${params.toString()}`
10747
+ );
10748
+ return readJson2(response, "Failed to read knowledge");
10749
+ }
10750
+
10751
+ // src/commands/frames/index.ts
10752
+ init_auth();
10753
+ init_http();
10754
+ async function readJson3(response, fallback) {
10755
+ if (!response.ok) {
10756
+ const body = await response.json().catch(() => ({}));
10757
+ throwApiResponseError(response, body, fallback);
10758
+ }
10759
+ return response.json();
10760
+ }
10761
+ async function listFrames(options = {}) {
10762
+ const pid = options.projectId ?? requireProjectId();
10763
+ const response = await apiFetch(`/api/projects/${pid}/frames`);
10764
+ return readJson3(response, "Failed to list frames");
10765
+ }
10766
+ async function listPageFrames(options) {
10767
+ const pid = options.projectId ?? requireProjectId();
10768
+ const params = new URLSearchParams({ pageKey: options.pageKey });
10769
+ const response = await apiFetch(
10770
+ `/api/projects/${pid}/frames?${params.toString()}`
10771
+ );
10772
+ return readJson3(response, "Failed to list page frames");
10773
+ }
10774
+ async function getFrame(options) {
10775
+ const pid = options.projectId ?? requireProjectId();
10776
+ const response = await apiFetch(
10777
+ `/api/projects/${pid}/frames/${encodeURIComponent(options.frameId)}`
10778
+ );
10779
+ return readJson3(response, "Failed to read frame");
10780
+ }
10781
+
10416
10782
  // src/commands/secrets/index.ts
10417
10783
  init_auth();
10418
10784
  init_http();
@@ -10784,6 +11150,7 @@ export {
10784
11150
  ApiError,
10785
11151
  DeploymentAmbiguityError,
10786
11152
  VERSION,
11153
+ addThreadMessage,
10787
11154
  annotateErrorWithDrift,
10788
11155
  apiFetch,
10789
11156
  bakedContractHash,
@@ -10793,9 +11160,11 @@ export {
10793
11160
  bundleCommand,
10794
11161
  canonicalContractHash,
10795
11162
  classifyStepProperties,
11163
+ clearAuthFields,
10796
11164
  clientContextHeaders,
10797
11165
  compareContract,
10798
11166
  compareOutput,
11167
+ completeDeviceLogin,
10799
11168
  containsCodeMarkers,
10800
11169
  createApiClient,
10801
11170
  createDeployCommand,
@@ -10807,6 +11176,8 @@ export {
10807
11176
  createProject,
10808
11177
  createProjectCommand,
10809
11178
  createSecret,
11179
+ createThread,
11180
+ credentialSource,
10810
11181
  deleteConfig,
10811
11182
  deleteDeployment,
10812
11183
  deleteDeploymentByFlowId,
@@ -10837,36 +11208,44 @@ export {
10837
11208
  getFeedbackPreference,
10838
11209
  getFlow,
10839
11210
  getFlowCommand,
11211
+ getFrame,
10840
11212
  getObserveSession,
10841
11213
  getPreview,
10842
11214
  getProject,
10843
11215
  getProjectCommand,
10844
- getToken,
11216
+ getRelease,
10845
11217
  listAllFlows,
10846
11218
  listDeployments,
10847
11219
  listDeploymentsCommand,
10848
11220
  listFlows,
10849
11221
  listFlowsCommand,
11222
+ listFrames,
10850
11223
  listJourneys,
11224
+ listKnowledge,
11225
+ listPageFrames,
10851
11226
  listPreviews,
10852
11227
  listProjects,
10853
11228
  listProjectsCommand,
11229
+ listReleases,
10854
11230
  listSecrets,
11231
+ listStepHistory,
11232
+ listThreads,
10855
11233
  loadConfig,
10856
11234
  loadJsonConfig,
11235
+ login,
10857
11236
  loginCommand,
11237
+ logout,
10858
11238
  logoutCommand,
10859
11239
  mergeAuthHeaders,
10860
11240
  parseSSEEvents,
10861
- pollForToken,
10862
11241
  publicFetch,
10863
11242
  push,
10864
11243
  pushCommand,
10865
11244
  readConfig,
10866
11245
  regrantPreview,
10867
- requestDeviceCode,
10868
11246
  requireProjectId,
10869
11247
  resetClientContext,
11248
+ resolveAccessToken,
10870
11249
  resolveAppUrl,
10871
11250
  resolveToken,
10872
11251
  run,
@@ -10874,10 +11253,12 @@ export {
10874
11253
  setClientContext,
10875
11254
  setDefaultProject,
10876
11255
  setFeedbackPreference,
11256
+ setReleaseRationale,
10877
11257
  simulateCollector,
10878
11258
  simulateDestination,
10879
11259
  simulateSource,
10880
11260
  simulateTransformer,
11261
+ startDeviceAuthorization,
10881
11262
  startObserveSession,
10882
11263
  telemetry_exports as telemetry,
10883
11264
  telemetryDisableCommand,