@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.
@@ -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.9.2" : 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.9.2" : void 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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
- 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.9.2",
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": {