@igoruehara/canvas-flow 0.1.10 → 0.1.12

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.
@@ -465,6 +465,19 @@ function joinCorsOrigins(config, publicUrl, port) {
465
465
  ].join(',');
466
466
  }
467
467
 
468
+ function isLoopbackUrl(value) {
469
+ try {
470
+ const hostname = new URL(value).hostname.toLowerCase();
471
+ return hostname === 'localhost'
472
+ || hostname === '::1'
473
+ || hostname === '[::1]'
474
+ || hostname === '0.0.0.0'
475
+ || hostname.startsWith('127.');
476
+ } catch {
477
+ return false;
478
+ }
479
+ }
480
+
468
481
  function applyEnvironment(config, paths, flags) {
469
482
  const port = Number(flags.port || config.server.port || 3333);
470
483
  const publicUrl = String(flags['public-url'] || config.server.publicUrl || `http://localhost:${port}`).replace(/\/$/, '');
@@ -515,11 +528,18 @@ function applyEnvironment(config, paths, flags) {
515
528
  setEnv('MONGO_SERVER_SELECTION_TIMEOUT_MS', config.database.mongoServerSelectionTimeoutMs);
516
529
  setEnv('MONGO_CONNECT_TIMEOUT_MS', config.database.mongoConnectTimeoutMs);
517
530
 
518
- setBoolEnv('CANVAS_FLOW_LOGIN', asBool(config.auth.login));
531
+ const loginRequired = asBool(config.auth.login);
532
+ const exposeApiTokenToFrontend = config.auth.exposeApiTokenToFrontend === true
533
+ || (!loginRequired && isLoopbackUrl(publicUrl));
534
+ setBoolEnv('CANVAS_FLOW_LOGIN', loginRequired);
519
535
  setEnv('CANVAS_FLOW_LOGIN_TTL_HOURS', config.auth.loginTtlHours);
520
536
  setEnv('CANVAS_FLOW_LOGIN_THROTTLE_WINDOW_MS', config.auth.loginThrottleWindowMs || 600000);
521
537
  setEnv('CANVAS_FLOW_LOGIN_MAX_ATTEMPTS', config.auth.loginMaxAttempts || 8);
522
538
  setEnv('CANVAS_FLOW_API_TOKEN', config.auth.apiToken);
539
+ delete process.env.CANVAS_FLOW_FRONTEND_API_TOKEN;
540
+ if (!loginRequired && exposeApiTokenToFrontend) {
541
+ setEnv('CANVAS_FLOW_FRONTEND_API_TOKEN', config.auth.apiToken);
542
+ }
523
543
  setEnv('CANVAS_FLOW_JWT_SECRET', config.auth.jwtSecret);
524
544
  setEnv('CANVAS_FLOW_MEDIA_PROXY_SECRET', config.auth.mediaProxySecret);
525
545
  setEnv('CANVAS_FLOW_MEDIA_PROXY_TTL_SECONDS', config.auth.mediaProxyTtlSeconds);
@@ -815,6 +835,123 @@ function sleep(ms) {
815
835
  return new Promise((resolve) => setTimeout(resolve, ms));
816
836
  }
817
837
 
838
+ function createStartupProgress() {
839
+ const frames = ['-', '\\', '|', '/'];
840
+ const useTty = Boolean(process.stdout.isTTY && !process.env.CI);
841
+ let frameIndex = 0;
842
+ let lastLength = 0;
843
+ let interval;
844
+ let percent = 0;
845
+ let message = 'starting';
846
+
847
+ const line = () => `${frames[frameIndex]} Canvas Flow startup ${String(percent).padStart(3, ' ')}% - ${message}`;
848
+ const clearLine = () => {
849
+ if (!useTty || !lastLength) return;
850
+ process.stdout.write(`\r${' '.repeat(lastLength)}\r`);
851
+ lastLength = 0;
852
+ };
853
+ const render = () => {
854
+ if (!useTty) return;
855
+ const text = line();
856
+ const padded = text.padEnd(lastLength, ' ');
857
+ lastLength = Math.max(lastLength, text.length);
858
+ process.stdout.write(`\r${padded}`);
859
+ };
860
+ const ensureInterval = () => {
861
+ if (!useTty || interval) return;
862
+ interval = setInterval(() => {
863
+ frameIndex = (frameIndex + 1) % frames.length;
864
+ render();
865
+ }, 140);
866
+ if (typeof interval.unref === 'function') interval.unref();
867
+ };
868
+ const stopInterval = () => {
869
+ if (!interval) return;
870
+ clearInterval(interval);
871
+ interval = undefined;
872
+ };
873
+
874
+ return {
875
+ update(nextPercent, nextMessage) {
876
+ percent = Math.max(percent, Math.min(99, Number(nextPercent) || percent));
877
+ message = nextMessage || message;
878
+ ensureInterval();
879
+ if (useTty) render();
880
+ else console.log(`Canvas Flow startup ${percent}% - ${message}`);
881
+ },
882
+ log(text) {
883
+ clearLine();
884
+ console.log(text);
885
+ render();
886
+ },
887
+ done(text) {
888
+ percent = 100;
889
+ message = 'ready';
890
+ stopInterval();
891
+ clearLine();
892
+ console.log(text || 'Canvas Flow ready (100%)');
893
+ },
894
+ fail(text) {
895
+ stopInterval();
896
+ clearLine();
897
+ if (text) console.log(text);
898
+ },
899
+ };
900
+ }
901
+
902
+ async function checkHttpOk(url, timeoutMs = 1200) {
903
+ const controller = new AbortController();
904
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
905
+ try {
906
+ const response = await fetch(url, { signal: controller.signal });
907
+ return response.ok;
908
+ } catch {
909
+ return false;
910
+ } finally {
911
+ clearTimeout(timer);
912
+ }
913
+ }
914
+
915
+ function startStartupStatus(publicUrl, options = {}) {
916
+ const healthUrl = `${String(publicUrl || '').replace(/\/$/, '')}/health`;
917
+ const startedAt = Date.now();
918
+ const progress = options.progress || createStartupProgress();
919
+ let stopped = false;
920
+ let nextHealthLogAt = 0;
921
+
922
+ const stop = () => {
923
+ if (stopped) return;
924
+ stopped = true;
925
+ };
926
+
927
+ void (async () => {
928
+ const deadline = Date.now() + 90000;
929
+ progress.update(90, `waiting for backend health at ${healthUrl}`);
930
+ while (!stopped && Date.now() < deadline) {
931
+ if (await checkHttpOk(healthUrl)) {
932
+ stop();
933
+ progress.done(`Canvas Flow ready (100%): ${publicUrl}`);
934
+ if (options.openBrowser) openBrowser(publicUrl);
935
+ return;
936
+ }
937
+ const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
938
+ if (elapsedSeconds >= nextHealthLogAt) {
939
+ const nextPercent = Math.min(98, 90 + Math.floor(elapsedSeconds / 15));
940
+ progress.update(nextPercent, `waiting for backend health (${elapsedSeconds}s elapsed)`);
941
+ nextHealthLogAt = elapsedSeconds + 3;
942
+ }
943
+ await sleep(500);
944
+ }
945
+
946
+ if (!stopped) {
947
+ stop();
948
+ progress.fail('Canvas Flow startup: health check is still pending. Keep this terminal open and watch the backend logs above.');
949
+ }
950
+ })();
951
+
952
+ return { stop };
953
+ }
954
+
818
955
  function mongoConnectionOptions(config) {
819
956
  return {
820
957
  serverSelectionTimeoutMS: Number(config.database?.mongoServerSelectionTimeoutMs || 8000),
@@ -1088,8 +1225,11 @@ async function doctor(flags) {
1088
1225
  reporter.finish();
1089
1226
  }
1090
1227
 
1091
- async function waitForMongo(config, flags, paths) {
1092
- if (flags['skip-mongo-check'] === true) return;
1228
+ async function waitForMongo(config, flags, paths, progress) {
1229
+ if (flags['skip-mongo-check'] === true) {
1230
+ if (progress) progress.update(45, 'MongoDB preflight skipped');
1231
+ return;
1232
+ }
1093
1233
  if (!config.database.mongoUrl) {
1094
1234
  throw new Error(`database.mongoUrl is required. Edit the config with: canvas-flow config --edit`);
1095
1235
  }
@@ -1101,15 +1241,18 @@ async function waitForMongo(config, flags, paths) {
1101
1241
  let lastMessage = '';
1102
1242
 
1103
1243
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
1244
+ if (progress) progress.update(30, `checking MongoDB (${attempt}/${attempts})`);
1104
1245
  const result = await checkMongoConnection(config.database.mongoUrl, options);
1105
1246
  if (result.ok) {
1106
- console.log('MongoDB preflight: connected');
1247
+ if (progress) progress.update(55, 'MongoDB connected');
1248
+ else console.log('MongoDB preflight: connected');
1107
1249
  return;
1108
1250
  }
1109
1251
 
1110
1252
  lastMessage = result.message;
1111
1253
  if (attempt < attempts) {
1112
- console.log(`MongoDB preflight waiting (${attempt}/${attempts}): ${result.message}`);
1254
+ if (progress) progress.update(35, `waiting for MongoDB (${attempt}/${attempts})`);
1255
+ else console.log(`MongoDB preflight waiting (${attempt}/${attempts}): ${result.message}`);
1113
1256
  await sleep(1500);
1114
1257
  }
1115
1258
  }
@@ -1123,36 +1266,46 @@ async function waitForMongo(config, flags, paths) {
1123
1266
  }
1124
1267
 
1125
1268
  async function start(flags) {
1126
- assertBundleExists();
1127
- addSourceDependencyFallback();
1128
- if (flags['with-docker'] === true || flags.infra === true) {
1129
- infra('up', flags);
1130
- }
1131
- const paths = resolvePaths(flags);
1132
- ensureDir(paths.homeDir);
1133
- const configExisted = fs.existsSync(paths.configPath);
1134
- const config = loadConfig(paths.configPath);
1135
- const runtime = applyEnvironment(config, paths, flags);
1136
- await waitForMongo(config, flags, paths);
1137
-
1138
- process.chdir(paths.homeDir);
1139
-
1140
- console.log(`Canvas Flow config: ${paths.configPath}`);
1141
- console.log(`Canvas Flow home: ${paths.homeDir}`);
1142
- console.log(`Canvas Flow URL: ${runtime.publicUrl}`);
1143
- if (!configExisted) {
1144
- console.log('Created the default config.json.');
1145
- console.log('Edit it with: canvas-flow config --edit');
1146
- console.log('Show it with: canvas-flow config --show');
1147
- }
1269
+ const progress = createStartupProgress();
1270
+ let startupStatus;
1271
+ try {
1272
+ progress.update(5, 'checking package bundle');
1273
+ assertBundleExists();
1274
+ progress.update(10, 'loading runtime dependencies');
1275
+ addSourceDependencyFallback();
1276
+ if (flags['with-docker'] === true || flags.infra === true) {
1277
+ progress.log('Canvas Flow startup: starting Docker infrastructure...');
1278
+ infra('up', flags);
1279
+ }
1280
+ progress.update(15, 'loading config');
1281
+ const paths = resolvePaths(flags);
1282
+ ensureDir(paths.homeDir);
1283
+ const configExisted = fs.existsSync(paths.configPath);
1284
+ const config = loadConfig(paths.configPath);
1285
+ progress.update(25, 'applying environment');
1286
+ const runtime = applyEnvironment(config, paths, flags);
1287
+ await waitForMongo(config, flags, paths, progress);
1288
+
1289
+ process.chdir(paths.homeDir);
1290
+
1291
+ progress.log(`Canvas Flow config: ${paths.configPath}`);
1292
+ progress.log(`Canvas Flow home: ${paths.homeDir}`);
1293
+ progress.log(`Canvas Flow URL: ${runtime.publicUrl}`);
1294
+ if (!configExisted) {
1295
+ progress.log('Created the default config.json.');
1296
+ progress.log('Edit it with: canvas-flow config --edit');
1297
+ progress.log('Show it with: canvas-flow config --show');
1298
+ }
1148
1299
 
1149
- const shouldOpen = flags.open === true || (flags.open !== false && config.server.openBrowser === true);
1150
- if (shouldOpen) {
1151
- const timer = setTimeout(() => openBrowser(runtime.publicUrl), 1200);
1152
- if (typeof timer.unref === 'function') timer.unref();
1300
+ const shouldOpen = flags.open === true || (flags.open !== false && config.server.openBrowser === true);
1301
+ progress.update(75, 'starting Canvas Flow API');
1302
+ startupStatus = startStartupStatus(runtime.publicUrl, { openBrowser: shouldOpen, progress });
1303
+ require(SERVER_ENTRY);
1304
+ } catch (error) {
1305
+ if (startupStatus) startupStatus.stop();
1306
+ progress.fail('Canvas Flow startup failed.');
1307
+ throw error;
1153
1308
  }
1154
-
1155
- require(SERVER_ENTRY);
1156
1309
  }
1157
1310
 
1158
1311
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igoruehara/canvas-flow",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Standalone npm launcher for Canvas Flow multi-agent GenAI workflows.",
5
5
  "homepage": "https://github.com/igoruehara/canvas-flow#readme",
6
6
  "repository": {