@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.
- package/bin/canvas-flow.js +186 -33
- package/package.json +1 -1
- package/public/assets/{index-qV8twxcq.js → index-CAYKh0bn.js} +78 -78
- package/public/index.html +1 -1
- package/server/auth/auth-controller.d.ts +2 -0
- package/server/auth/auth-service.d.ts +2 -0
- package/server/auth/auth-service.js +5 -0
- package/server/auth/auth-service.js.map +1 -1
package/bin/canvas-flow.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
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