@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.
@@ -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.10.0" : void 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.10.0" : void 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
- function paintReport(html) { $reportFrame.srcdoc = html; $reportOverlay.hidden = true; $reportOverlay.innerHTML = ''; }
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 () => {
@@ -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
- <iframe id="report-frame" sandbox="allow-same-origin" title="Report"></iframe>
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {