@floless/app 0.9.2 → 0.11.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/floless-server.cjs +112 -17
- package/dist/web/aware.js +52 -1
- package/dist/web/index.html +6 -1
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -51978,6 +51978,18 @@ function env() {
|
|
|
51978
51978
|
webBase: process.env.FLOLESS_WEB_BASE || base.webBase
|
|
51979
51979
|
};
|
|
51980
51980
|
}
|
|
51981
|
+
function isLoopback(host) {
|
|
51982
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host.endsWith(".localhost");
|
|
51983
|
+
}
|
|
51984
|
+
var TRUSTED_API_ORIGINS = /* @__PURE__ */ new Set(["https://api.floless.io", "https://api.stage.floless.io"]);
|
|
51985
|
+
function isTrustedFlolessHost(url) {
|
|
51986
|
+
try {
|
|
51987
|
+
const u = new URL(url);
|
|
51988
|
+
return isLoopback(u.hostname) || TRUSTED_API_ORIGINS.has(u.origin);
|
|
51989
|
+
} catch {
|
|
51990
|
+
return false;
|
|
51991
|
+
}
|
|
51992
|
+
}
|
|
51981
51993
|
var OFFLINE_GRACE_MS = 24 * 60 * 60 * 1e3;
|
|
51982
51994
|
var MEM_CACHE_MS = 60 * 1e3;
|
|
51983
51995
|
var REFRESH_SKEW_MS = 60 * 1e3;
|
|
@@ -52059,6 +52071,7 @@ async function resolveToken() {
|
|
|
52059
52071
|
const t = readTokens();
|
|
52060
52072
|
if (!t) return { token: null, reason: "signed-out" };
|
|
52061
52073
|
if (Date.now() < t.expiresAt - REFRESH_SKEW_MS) return { token: t.accessToken };
|
|
52074
|
+
if (!isTrustedFlolessHost(env().apiBase)) return { token: null, reason: "session-expired" };
|
|
52062
52075
|
let res;
|
|
52063
52076
|
try {
|
|
52064
52077
|
res = await fetch(`${env().apiBase}/auth/refresh`, {
|
|
@@ -52361,7 +52374,7 @@ function appVersion() {
|
|
|
52361
52374
|
return resolveVersion({
|
|
52362
52375
|
isSea: isSea2(),
|
|
52363
52376
|
sqVersionXml: readSqVersionXml(),
|
|
52364
|
-
define: true ? "0.
|
|
52377
|
+
define: true ? "0.11.0" : void 0,
|
|
52365
52378
|
pkgVersion: readPkgVersion()
|
|
52366
52379
|
});
|
|
52367
52380
|
}
|
|
@@ -52371,7 +52384,7 @@ function resolveChannel(s) {
|
|
|
52371
52384
|
return "dev";
|
|
52372
52385
|
}
|
|
52373
52386
|
function appChannel() {
|
|
52374
|
-
return resolveChannel({ isSea: isSea2(), define: true ? "0.
|
|
52387
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.11.0" : void 0 });
|
|
52375
52388
|
}
|
|
52376
52389
|
|
|
52377
52390
|
// oauth-presets.ts
|
|
@@ -52468,6 +52481,45 @@ function bakeFloSource(source, inputs) {
|
|
|
52468
52481
|
return doc.toString();
|
|
52469
52482
|
}
|
|
52470
52483
|
|
|
52484
|
+
// report-badge.ts
|
|
52485
|
+
var BADGE_MARKER = "fla-credit";
|
|
52486
|
+
var DEFAULT_URL = "https://floless.io/made-with-floless?utm_source=report_badge";
|
|
52487
|
+
function escapeAttr(value) {
|
|
52488
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
52489
|
+
}
|
|
52490
|
+
function badgeHtml(url) {
|
|
52491
|
+
return `
|
|
52492
|
+
<style>
|
|
52493
|
+
#fla-credit { margin-top: 32px; padding-top: 16px; border-top: 1px solid rgba(0,0,0,0.10); display: flex; justify-content: flex-end; }
|
|
52494
|
+
#fla-badge { display: inline-flex; align-items: center; height: 26px; padding: 0 10px; border-radius: 999px; background: #161d28; border: 1px solid #2a3548; box-shadow: 0 1px 4px rgba(0,0,0,0.18); text-decoration: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; font-size: 11px; line-height: 1; transition: background 0.15s ease, border-color 0.15s ease; }
|
|
52495
|
+
#fla-badge:hover { background: rgba(74,158,255,0.10); border-color: #2c5f9e; }
|
|
52496
|
+
#fla-badge:focus-visible { outline: 2px solid #4a9eff; outline-offset: 2px; }
|
|
52497
|
+
#fla-badge .fla-prefix { color: #9aa5b6; font-weight: 400; }
|
|
52498
|
+
#fla-badge .fla-name { color: #4a9eff; font-weight: 600; margin-left: 4px; }
|
|
52499
|
+
#fla-badge:hover .fla-name { color: #67b3ff; }
|
|
52500
|
+
@media print { #fla-badge { background: transparent; border-color: #9aa5b6; box-shadow: none; } #fla-badge .fla-prefix { color: #555; } #fla-badge .fla-name { color: #1a56c4; } }
|
|
52501
|
+
</style>
|
|
52502
|
+
<div id="${BADGE_MARKER}">
|
|
52503
|
+
<a id="fla-badge" href="${escapeAttr(url)}" target="_blank" rel="noopener noreferrer" aria-label="Made with FloLess \u2014 visit floless.io">
|
|
52504
|
+
<span class="fla-prefix">Made with</span><span class="fla-name">FloLess</span>
|
|
52505
|
+
</a>
|
|
52506
|
+
</div>`.trim();
|
|
52507
|
+
}
|
|
52508
|
+
function injectBadge(html, opts = {}) {
|
|
52509
|
+
if (!html || !html.trim()) return html;
|
|
52510
|
+
if (html.includes(`id="${BADGE_MARKER}"`)) return html;
|
|
52511
|
+
const badge = badgeHtml(opts.url ?? DEFAULT_URL);
|
|
52512
|
+
let last = -1;
|
|
52513
|
+
for (const m of html.matchAll(/<\/body\s*>/gi)) last = m.index ?? last;
|
|
52514
|
+
return last === -1 ? `${html}
|
|
52515
|
+
${badge}` : `${html.slice(0, last)}${badge}
|
|
52516
|
+
${html.slice(last)}`;
|
|
52517
|
+
}
|
|
52518
|
+
function withBadge(report, opts) {
|
|
52519
|
+
if (!report) return report;
|
|
52520
|
+
return { ...report, html: injectBadge(report.html, opts) };
|
|
52521
|
+
}
|
|
52522
|
+
|
|
52471
52523
|
// index.ts
|
|
52472
52524
|
var import_node_crypto5 = require("node:crypto");
|
|
52473
52525
|
|
|
@@ -53763,18 +53815,6 @@ function feedUrl() {
|
|
|
53763
53815
|
const env2 = (process.env.FLOLESS_UPDATE_URL ?? "").trim().replace(/\/+$/, "");
|
|
53764
53816
|
return env2 || updateApiBase();
|
|
53765
53817
|
}
|
|
53766
|
-
function isLoopback(host) {
|
|
53767
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host.endsWith(".localhost");
|
|
53768
|
-
}
|
|
53769
|
-
var TRUSTED_FEED_ORIGINS = /* @__PURE__ */ new Set(["https://api.floless.io", "https://api.stage.floless.io"]);
|
|
53770
|
-
function isTrustedFlolessHost(url) {
|
|
53771
|
-
try {
|
|
53772
|
-
const u = new URL(url);
|
|
53773
|
-
return isLoopback(u.hostname) || TRUSTED_FEED_ORIGINS.has(u.origin);
|
|
53774
|
-
} catch {
|
|
53775
|
-
return false;
|
|
53776
|
-
}
|
|
53777
|
-
}
|
|
53778
53818
|
async function authedFetch(url, init = {}) {
|
|
53779
53819
|
const headers = new Headers(init.headers);
|
|
53780
53820
|
if (isTrustedFlolessHost(url)) {
|
|
@@ -53941,6 +53981,43 @@ async function reportIssue(input) {
|
|
|
53941
53981
|
return { ok: true, ref };
|
|
53942
53982
|
}
|
|
53943
53983
|
|
|
53984
|
+
// report-share.ts
|
|
53985
|
+
var SHARE_TIMEOUT_MS = 3e4;
|
|
53986
|
+
async function shareReport(input) {
|
|
53987
|
+
let token;
|
|
53988
|
+
try {
|
|
53989
|
+
token = await accessToken();
|
|
53990
|
+
} catch {
|
|
53991
|
+
return { ok: false, error: "offline" };
|
|
53992
|
+
}
|
|
53993
|
+
if (!token) return { ok: false, error: "signed_out" };
|
|
53994
|
+
const url = `${apiBaseUrl()}/reports`;
|
|
53995
|
+
let res;
|
|
53996
|
+
try {
|
|
53997
|
+
res = await authedFetch(url, {
|
|
53998
|
+
method: "POST",
|
|
53999
|
+
headers: { "Content-Type": "application/json" },
|
|
54000
|
+
body: JSON.stringify(input),
|
|
54001
|
+
signal: AbortSignal.timeout(SHARE_TIMEOUT_MS)
|
|
54002
|
+
});
|
|
54003
|
+
} catch {
|
|
54004
|
+
return { ok: false, error: "offline" };
|
|
54005
|
+
}
|
|
54006
|
+
if (res.status === 401) return { ok: false, error: "signed_out" };
|
|
54007
|
+
if (res.status === 403) return { ok: false, error: "no_subscription" };
|
|
54008
|
+
if (res.status === 429) return { ok: false, error: "rate_limited" };
|
|
54009
|
+
if (res.status === 413) return { ok: false, error: "too_large" };
|
|
54010
|
+
if (!res.ok) return { ok: false, error: "offline" };
|
|
54011
|
+
try {
|
|
54012
|
+
const b = await res.json();
|
|
54013
|
+
if (typeof b.url === "string" && b.url && new URL(b.url).protocol === "https:") {
|
|
54014
|
+
return { ok: true, url: b.url };
|
|
54015
|
+
}
|
|
54016
|
+
} catch {
|
|
54017
|
+
}
|
|
54018
|
+
return { ok: false, error: "offline" };
|
|
54019
|
+
}
|
|
54020
|
+
|
|
53944
54021
|
// release-notes.ts
|
|
53945
54022
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
53946
54023
|
var AWARE_REPO = "aware-aeco/aware";
|
|
@@ -56509,6 +56586,9 @@ async function startServer() {
|
|
|
56509
56586
|
app.log.warn({ detail: err.detail }, err.message);
|
|
56510
56587
|
return reply.status(502).send({ ok: false, error: err.message, detail: err.detail ?? null });
|
|
56511
56588
|
}
|
|
56589
|
+
if (err.statusCode === 413) {
|
|
56590
|
+
return reply.status(413).send({ ok: false, error: "too_large" });
|
|
56591
|
+
}
|
|
56512
56592
|
app.log.error(err);
|
|
56513
56593
|
return reply.status(500).send({ ok: false, error: err.message });
|
|
56514
56594
|
});
|
|
@@ -56589,6 +56669,20 @@ async function startServer() {
|
|
|
56589
56669
|
return reply.status(status).send({ ok: false, error: result.error });
|
|
56590
56670
|
}
|
|
56591
56671
|
);
|
|
56672
|
+
app.post(
|
|
56673
|
+
"/api/share-report",
|
|
56674
|
+
{ bodyLimit: 6 * 1024 * 1024 },
|
|
56675
|
+
async (req, reply) => {
|
|
56676
|
+
const b = req.body ?? {};
|
|
56677
|
+
const html = typeof b.html === "string" ? b.html : "";
|
|
56678
|
+
const title = typeof b.title === "string" ? b.title.trim().slice(0, 200) : "";
|
|
56679
|
+
if (!html) return reply.status(400).send({ ok: false, error: "html is required" });
|
|
56680
|
+
const result = await shareReport({ title: title || "FloLess report", html });
|
|
56681
|
+
if (result.ok) return { ok: true, url: result.url };
|
|
56682
|
+
const status = result.error === "signed_out" ? 401 : result.error === "no_subscription" ? 403 : result.error === "rate_limited" ? 429 : result.error === "too_large" ? 413 : 503;
|
|
56683
|
+
return reply.status(status).send({ ok: false, error: result.error });
|
|
56684
|
+
}
|
|
56685
|
+
);
|
|
56592
56686
|
app.get("/api/autostart", async () => {
|
|
56593
56687
|
const supported = autostartSupported();
|
|
56594
56688
|
return { ok: true, supported, enabled: supported ? autostartPresent() : false };
|
|
@@ -56866,7 +56960,8 @@ async function startServer() {
|
|
|
56866
56960
|
return { id: c.integration, label: friendlyIntegrationName(c.integration) };
|
|
56867
56961
|
}
|
|
56868
56962
|
}
|
|
56869
|
-
} catch {
|
|
56963
|
+
} catch (err) {
|
|
56964
|
+
console.warn(`[floless] credential-issue detection failed for app "${id}" (treating as no credential issue):`, err);
|
|
56870
56965
|
}
|
|
56871
56966
|
return null;
|
|
56872
56967
|
}
|
|
@@ -56907,7 +57002,7 @@ async function startServer() {
|
|
|
56907
57002
|
broadcast({ type: "run-started", id, dryRun: !!dryRun, simulate: !!simulate, runId: result.runId });
|
|
56908
57003
|
for (const ev of events) broadcast({ type: "trace", id, event: ev });
|
|
56909
57004
|
broadcast({ type: "run-ended", id, runId: result.runId });
|
|
56910
|
-
const report = extractReportHtml(events);
|
|
57005
|
+
const report = withBadge(extractReportHtml(events));
|
|
56911
57006
|
const credentialIssue = !simulate && hasAuthFailure(events) ? await detectCredentialIssue(id) : null;
|
|
56912
57007
|
return { ok: true, runId: result.runId, tracePath: result.tracePath, events, report, credentialIssue };
|
|
56913
57008
|
}
|
|
@@ -56965,7 +57060,7 @@ async function startServer() {
|
|
|
56965
57060
|
broadcast({ type: "debug-ended", id, nodeId });
|
|
56966
57061
|
const r = result.result ?? {};
|
|
56967
57062
|
const html = typeof r.html === "string" ? r.html : null;
|
|
56968
|
-
return { ok: true, result, report: html ? { nodeId, html } : null };
|
|
57063
|
+
return { ok: true, result, report: withBadge(html ? { nodeId, html } : null) };
|
|
56969
57064
|
}
|
|
56970
57065
|
);
|
|
56971
57066
|
app.get("/api/routines", async () => ({
|
package/dist/web/aware.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const $reportTitle = document.getElementById('report-title');
|
|
23
23
|
const $reportSub = document.getElementById('report-sub');
|
|
24
24
|
const $reportOpen = document.getElementById('report-open');
|
|
25
|
+
const $reportShare = document.getElementById('report-share');
|
|
25
26
|
const $reportClose = document.getElementById('report-close');
|
|
26
27
|
|
|
27
28
|
// One persistent array the inspect Execution tab reads; we mutate in place so
|
|
@@ -908,7 +909,9 @@
|
|
|
908
909
|
try { await api('/api/run/stop', { method: 'POST' }); } catch { /* the run's own catch surfaces it */ }
|
|
909
910
|
}
|
|
910
911
|
|
|
911
|
-
|
|
912
|
+
// Share is visible exactly when the frame holds a report (the empty state HIDES it —
|
|
913
|
+
// never a disabled affordance with no path forward).
|
|
914
|
+
function paintReport(html) { $reportFrame.srcdoc = html; $reportOverlay.hidden = true; $reportOverlay.replaceChildren(); $reportShare.hidden = false; }
|
|
912
915
|
|
|
913
916
|
// Double-click the HTML Viewer node → LOAD the last report (no run; running is
|
|
914
917
|
// the header "▶ Run workflow" button's job). Shows a prompt if nothing has run yet.
|
|
@@ -925,6 +928,7 @@
|
|
|
925
928
|
paintReport(cached.html);
|
|
926
929
|
} else {
|
|
927
930
|
$reportFrame.srcdoc = '';
|
|
931
|
+
$reportShare.hidden = true; // nothing to share — hide, don't disable
|
|
928
932
|
$reportOverlay.hidden = false;
|
|
929
933
|
$reportOverlay.innerHTML = `<div>No report yet. Click <strong>▶ Run workflow</strong> (top right) to run it against the live model, then double-click to view it any time.</div>`;
|
|
930
934
|
$reportSub.textContent = 'Double-click loads the last report; ▶ Run workflow generates a fresh one.';
|
|
@@ -948,6 +952,7 @@
|
|
|
948
952
|
$reportTitle.textContent = `HTML Viewer · ${app.displayName}`;
|
|
949
953
|
$reportSub.innerHTML = `Live run of <code>${escapeHtml(nodeId)}</code>${inputBadge ? ' · ' + escapeHtml(inputBadge) : ''} — rendered from the node's output, never composed by the UI.`;
|
|
950
954
|
$reportFrame.srcdoc = ''; // clear any previously rendered report while this run is in flight
|
|
955
|
+
$reportShare.hidden = true; // the frame is blank — only a painted report is shareable
|
|
951
956
|
$reportOverlay.hidden = false;
|
|
952
957
|
$reportOverlay.innerHTML = opts.debug
|
|
953
958
|
? `<div class="spinner"></div><div>Launching the .NET debugger for <code>${escapeHtml(nodeId)}</code> — a Windows picker will pop; choose your <strong>Visual Studio</strong> to attach to <code>aware-tekla.exe</code>, then step through. The report renders when you let it finish.</div>`
|
|
@@ -1022,6 +1027,7 @@
|
|
|
1022
1027
|
|
|
1023
1028
|
function showCredentialReconnect(cred) {
|
|
1024
1029
|
$reportFrame.srcdoc = '';
|
|
1030
|
+
$reportShare.hidden = true;
|
|
1025
1031
|
$reportOverlay.hidden = false;
|
|
1026
1032
|
$reportOverlay.replaceChildren();
|
|
1027
1033
|
|
|
@@ -1267,6 +1273,51 @@
|
|
|
1267
1273
|
window.open(url, '_blank');
|
|
1268
1274
|
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
|
1269
1275
|
};
|
|
1276
|
+
|
|
1277
|
+
// Share the on-screen report → a hosted floless.io link. The local server does the
|
|
1278
|
+
// upload (the bearer never reaches this page); we relay exactly the HTML the run
|
|
1279
|
+
// produced and surface the returned link. Disabled while in flight (double-click =
|
|
1280
|
+
// double upload), and the link lands in $reportSub as a real DOM anchor (mkEl, not
|
|
1281
|
+
// markup) so a server-returned URL can never inject HTML.
|
|
1282
|
+
$reportShare.onclick = async () => {
|
|
1283
|
+
const cached = currentId && lastReportByApp.get(currentId);
|
|
1284
|
+
// Share exactly what's on screen — the frame's srcdoc — so the link can never carry
|
|
1285
|
+
// a different report than the one the user is looking at (e.g. another app's cache).
|
|
1286
|
+
const html = $reportFrame.srcdoc;
|
|
1287
|
+
if (!html || !cached) return; // button is hidden in this state; belt-and-braces
|
|
1288
|
+
const app = apps.get(currentId);
|
|
1289
|
+
const title = `${app ? app.displayName : currentId}${cached.label ? ' · ' + cached.label : ''}`;
|
|
1290
|
+
$reportShare.disabled = true;
|
|
1291
|
+
const prev = $reportShare.textContent;
|
|
1292
|
+
$reportShare.textContent = 'Sharing…';
|
|
1293
|
+
try {
|
|
1294
|
+
const res = await api('/api/share-report', { method: 'POST', body: JSON.stringify({ title, html }) });
|
|
1295
|
+
await navigator.clipboard.writeText(res.url).catch(() => {});
|
|
1296
|
+
const link = mkEl('a', null, res.url.replace(/^https:\/\//, ''));
|
|
1297
|
+
link.href = res.url;
|
|
1298
|
+
link.target = '_blank';
|
|
1299
|
+
link.rel = 'noopener';
|
|
1300
|
+
$reportSub.replaceChildren(
|
|
1301
|
+
document.createTextNode('Shared — '),
|
|
1302
|
+
link,
|
|
1303
|
+
document.createTextNode(' — copied to clipboard. Anyone with the link can view this frozen report.'),
|
|
1304
|
+
);
|
|
1305
|
+
showToast('Link copied to clipboard', 'ok');
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
const err = e && e.body && e.body.error;
|
|
1308
|
+
const msg =
|
|
1309
|
+
err === 'signed_out' ? 'Your FloLess sign-in has expired — sign in again to share reports.' :
|
|
1310
|
+
err === 'no_subscription' || err === 'subscription required'
|
|
1311
|
+
? 'A FloLess subscription is required to share reports — sign in to continue.' :
|
|
1312
|
+
err === 'rate_limited' ? 'Share limit reached (30 per hour) — try again in a few minutes.' :
|
|
1313
|
+
err === 'too_large' ? 'Report is too large to share (over 5 MB).' :
|
|
1314
|
+
'Couldn’t reach FloLess — check your connection and try sharing again.';
|
|
1315
|
+
showToast(msg, 'warn');
|
|
1316
|
+
} finally {
|
|
1317
|
+
$reportShare.disabled = false;
|
|
1318
|
+
$reportShare.textContent = prev;
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1270
1321
|
$promptSel.onchange = () => { loadApp($promptSel.value).catch(reportErr); };
|
|
1271
1322
|
|
|
1272
1323
|
$compileBtn.onclick = async () => {
|
package/dist/web/index.html
CHANGED
|
@@ -246,12 +246,17 @@
|
|
|
246
246
|
<div class="modal-sub" id="report-sub">Rendered from the live run — never composed by the UI.</div>
|
|
247
247
|
</div>
|
|
248
248
|
<div class="report-actions">
|
|
249
|
+
<button id="report-share" class="ghost" hidden data-tip="Share this report as a link anyone can view — hosted on floless.io">↗ Share</button>
|
|
249
250
|
<button id="report-open" class="ghost" data-tip="Open the report in a new browser tab">↗ Open</button>
|
|
250
251
|
<button id="report-close" data-tip="Close">×</button>
|
|
251
252
|
</div>
|
|
252
253
|
</div>
|
|
253
254
|
<div class="report-stage" id="report-stage">
|
|
254
|
-
|
|
255
|
+
<!-- allow-popups (+ escape): the report's own links — the "Made with FloLess"
|
|
256
|
+
badge above all — must open in a real, unsandboxed tab. Scripts stay
|
|
257
|
+
blocked and there is NO allow-same-origin: the report remains an opaque
|
|
258
|
+
origin with zero access to the app page. -->
|
|
259
|
+
<iframe id="report-frame" sandbox="allow-popups allow-popups-to-escape-sandbox" title="Report"></iframe>
|
|
255
260
|
<div class="report-overlay" id="report-overlay" hidden></div>
|
|
256
261
|
</div>
|
|
257
262
|
</div>
|