@floless/app 0.10.0 → 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 +69 -14
- 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
|
|
@@ -53802,18 +53815,6 @@ function feedUrl() {
|
|
|
53802
53815
|
const env2 = (process.env.FLOLESS_UPDATE_URL ?? "").trim().replace(/\/+$/, "");
|
|
53803
53816
|
return env2 || updateApiBase();
|
|
53804
53817
|
}
|
|
53805
|
-
function isLoopback(host) {
|
|
53806
|
-
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host.endsWith(".localhost");
|
|
53807
|
-
}
|
|
53808
|
-
var TRUSTED_FEED_ORIGINS = /* @__PURE__ */ new Set(["https://api.floless.io", "https://api.stage.floless.io"]);
|
|
53809
|
-
function isTrustedFlolessHost(url) {
|
|
53810
|
-
try {
|
|
53811
|
-
const u = new URL(url);
|
|
53812
|
-
return isLoopback(u.hostname) || TRUSTED_FEED_ORIGINS.has(u.origin);
|
|
53813
|
-
} catch {
|
|
53814
|
-
return false;
|
|
53815
|
-
}
|
|
53816
|
-
}
|
|
53817
53818
|
async function authedFetch(url, init = {}) {
|
|
53818
53819
|
const headers = new Headers(init.headers);
|
|
53819
53820
|
if (isTrustedFlolessHost(url)) {
|
|
@@ -53980,6 +53981,43 @@ async function reportIssue(input) {
|
|
|
53980
53981
|
return { ok: true, ref };
|
|
53981
53982
|
}
|
|
53982
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
|
+
|
|
53983
54021
|
// release-notes.ts
|
|
53984
54022
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
53985
54023
|
var AWARE_REPO = "aware-aeco/aware";
|
|
@@ -56548,6 +56586,9 @@ async function startServer() {
|
|
|
56548
56586
|
app.log.warn({ detail: err.detail }, err.message);
|
|
56549
56587
|
return reply.status(502).send({ ok: false, error: err.message, detail: err.detail ?? null });
|
|
56550
56588
|
}
|
|
56589
|
+
if (err.statusCode === 413) {
|
|
56590
|
+
return reply.status(413).send({ ok: false, error: "too_large" });
|
|
56591
|
+
}
|
|
56551
56592
|
app.log.error(err);
|
|
56552
56593
|
return reply.status(500).send({ ok: false, error: err.message });
|
|
56553
56594
|
});
|
|
@@ -56628,6 +56669,20 @@ async function startServer() {
|
|
|
56628
56669
|
return reply.status(status).send({ ok: false, error: result.error });
|
|
56629
56670
|
}
|
|
56630
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
|
+
);
|
|
56631
56686
|
app.get("/api/autostart", async () => {
|
|
56632
56687
|
const supported = autostartSupported();
|
|
56633
56688
|
return { ok: true, supported, enabled: supported ? autostartPresent() : false };
|
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>
|