@floless/app 0.9.1 → 0.10.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.
@@ -52055,10 +52055,10 @@ function jwtExpiry(jwt) {
52055
52055
  var NetworkError = class extends Error {
52056
52056
  network = true;
52057
52057
  };
52058
- async function ensureFreshToken() {
52058
+ async function resolveToken() {
52059
52059
  const t = readTokens();
52060
- if (!t) return null;
52061
- if (Date.now() < t.expiresAt - REFRESH_SKEW_MS) return t.accessToken;
52060
+ if (!t) return { token: null, reason: "signed-out" };
52061
+ if (Date.now() < t.expiresAt - REFRESH_SKEW_MS) return { token: t.accessToken };
52062
52062
  let res;
52063
52063
  try {
52064
52064
  res = await fetch(`${env().apiBase}/auth/refresh`, {
@@ -52070,13 +52070,16 @@ async function ensureFreshToken() {
52070
52070
  throw new NetworkError("refresh failed (network)");
52071
52071
  }
52072
52072
  if (res.status >= 500) throw new NetworkError(`refresh failed (${res.status})`);
52073
- if (!res.ok) return null;
52073
+ if (!res.ok) return { token: null, reason: "session-expired" };
52074
52074
  const body = await res.json();
52075
52075
  const access = body.data?.tokens?.access;
52076
52076
  const refresh = body.data?.tokens?.refresh ?? t.refreshToken;
52077
- if (!access) return null;
52077
+ if (!access) return { token: null, reason: "session-expired" };
52078
52078
  storeTokens({ accessToken: access, refreshToken: refresh, expiresAt: jwtExpiry(access), issuedAt: Date.now() });
52079
- return access;
52079
+ return { token: access };
52080
+ }
52081
+ async function ensureFreshToken() {
52082
+ return (await resolveToken()).token;
52080
52083
  }
52081
52084
  function mapUserStatus(u) {
52082
52085
  switch (u.accountType) {
@@ -52087,7 +52090,7 @@ function mapUserStatus(u) {
52087
52090
  case "expired_trial":
52088
52091
  return { state: "expired", plan: u.subscription?.tier, expiresAt: u.trial?.trialExpiresAt };
52089
52092
  default:
52090
- return { state: "unlicensed" };
52093
+ return { state: "unlicensed", reason: "no-subscription" };
52091
52094
  }
52092
52095
  }
52093
52096
  function signInUrl() {
@@ -52111,27 +52114,28 @@ async function getLicenseStatus(opts = {}) {
52111
52114
  const url = signInUrl();
52112
52115
  if (seatLost) {
52113
52116
  mem = null;
52114
- return { state: "unlicensed", signInUrl: url };
52117
+ return { state: "unlicensed", reason: "seat-taken", signInUrl: url };
52115
52118
  }
52116
52119
  if (!opts.force && mem && Date.now() - mem.at < MEM_CACHE_MS) return mem.status;
52117
52120
  const finish = (s) => {
52118
52121
  mem = { at: Date.now(), status: s };
52119
52122
  return s;
52120
52123
  };
52121
- let token;
52124
+ let resolved;
52122
52125
  try {
52123
- token = await ensureFreshToken();
52126
+ resolved = await resolveToken();
52124
52127
  } catch {
52125
52128
  return finish(offlineFallback(url));
52126
52129
  }
52127
- if (!token) return finish({ state: "unlicensed", signInUrl: url });
52130
+ if (resolved.token === null) return finish({ state: "unlicensed", reason: resolved.reason, signInUrl: url });
52131
+ const token = resolved.token;
52128
52132
  let res;
52129
52133
  try {
52130
52134
  res = await fetch(`${env().apiBase}/user/status`, { headers: { Authorization: `Bearer ${token}` } });
52131
52135
  } catch {
52132
52136
  return finish(offlineFallback(url));
52133
52137
  }
52134
- if (res.status === 401) return finish({ state: "unlicensed", signInUrl: url });
52138
+ if (res.status === 401) return finish({ state: "unlicensed", reason: "session-expired", signInUrl: url });
52135
52139
  if (res.status >= 500) return finish(offlineFallback(url));
52136
52140
  if (!res.ok) return finish({ state: "unlicensed", signInUrl: url });
52137
52141
  let mapped;
@@ -52357,7 +52361,7 @@ function appVersion() {
52357
52361
  return resolveVersion({
52358
52362
  isSea: isSea2(),
52359
52363
  sqVersionXml: readSqVersionXml(),
52360
- define: true ? "0.9.1" : void 0,
52364
+ define: true ? "0.10.0" : void 0,
52361
52365
  pkgVersion: readPkgVersion()
52362
52366
  });
52363
52367
  }
@@ -52367,7 +52371,7 @@ function resolveChannel(s) {
52367
52371
  return "dev";
52368
52372
  }
52369
52373
  function appChannel() {
52370
- return resolveChannel({ isSea: isSea2(), define: true ? "0.9.1" : void 0 });
52374
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.10.0" : void 0 });
52371
52375
  }
52372
52376
 
52373
52377
  // oauth-presets.ts
@@ -52464,6 +52468,45 @@ function bakeFloSource(source, inputs) {
52464
52468
  return doc.toString();
52465
52469
  }
52466
52470
 
52471
+ // report-badge.ts
52472
+ var BADGE_MARKER = "fla-credit";
52473
+ var DEFAULT_URL = "https://floless.io/made-with-floless?utm_source=report_badge";
52474
+ function escapeAttr(value) {
52475
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
52476
+ }
52477
+ function badgeHtml(url) {
52478
+ return `
52479
+ <style>
52480
+ #fla-credit { margin-top: 32px; padding-top: 16px; border-top: 1px solid rgba(0,0,0,0.10); display: flex; justify-content: flex-end; }
52481
+ #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; }
52482
+ #fla-badge:hover { background: rgba(74,158,255,0.10); border-color: #2c5f9e; }
52483
+ #fla-badge:focus-visible { outline: 2px solid #4a9eff; outline-offset: 2px; }
52484
+ #fla-badge .fla-prefix { color: #9aa5b6; font-weight: 400; }
52485
+ #fla-badge .fla-name { color: #4a9eff; font-weight: 600; margin-left: 4px; }
52486
+ #fla-badge:hover .fla-name { color: #67b3ff; }
52487
+ @media print { #fla-badge { background: transparent; border-color: #9aa5b6; box-shadow: none; } #fla-badge .fla-prefix { color: #555; } #fla-badge .fla-name { color: #1a56c4; } }
52488
+ </style>
52489
+ <div id="${BADGE_MARKER}">
52490
+ <a id="fla-badge" href="${escapeAttr(url)}" target="_blank" rel="noopener noreferrer" aria-label="Made with FloLess \u2014 visit floless.io">
52491
+ <span class="fla-prefix">Made with</span><span class="fla-name">FloLess</span>
52492
+ </a>
52493
+ </div>`.trim();
52494
+ }
52495
+ function injectBadge(html, opts = {}) {
52496
+ if (!html || !html.trim()) return html;
52497
+ if (html.includes(`id="${BADGE_MARKER}"`)) return html;
52498
+ const badge = badgeHtml(opts.url ?? DEFAULT_URL);
52499
+ let last = -1;
52500
+ for (const m of html.matchAll(/<\/body\s*>/gi)) last = m.index ?? last;
52501
+ return last === -1 ? `${html}
52502
+ ${badge}` : `${html.slice(0, last)}${badge}
52503
+ ${html.slice(last)}`;
52504
+ }
52505
+ function withBadge(report, opts) {
52506
+ if (!report) return report;
52507
+ return { ...report, html: injectBadge(report.html, opts) };
52508
+ }
52509
+
52467
52510
  // index.ts
52468
52511
  var import_node_crypto5 = require("node:crypto");
52469
52512
 
@@ -56529,9 +56572,9 @@ async function startServer() {
56529
56572
  req.url.startsWith("/api/report-issue") || req.url.startsWith("/api/update") || req.url.startsWith("/api/aware/update") || // Release notes are a read-only display surface for the update pills — a local control,
56530
56573
  // no workspace data, never spawns `aware`. Exempt like the other update routes.
56531
56574
  req.url.startsWith("/api/release-notes")) return;
56532
- const { state: state2, signInUrl: signInUrl2 } = await getLicenseStatus();
56575
+ const { state: state2, reason, signInUrl: signInUrl2 } = await getLicenseStatus();
56533
56576
  if (state2 !== "valid" && state2 !== "offline-grace") {
56534
- return reply.status(402).send({ ok: false, error: "subscription required", state: state2, signInUrl: signInUrl2 });
56577
+ return reply.status(402).send({ ok: false, error: "subscription required", state: state2, reason, signInUrl: signInUrl2 });
56535
56578
  }
56536
56579
  });
56537
56580
  app.addHook("onRequest", async (req, reply) => {
@@ -56862,7 +56905,8 @@ async function startServer() {
56862
56905
  return { id: c.integration, label: friendlyIntegrationName(c.integration) };
56863
56906
  }
56864
56907
  }
56865
- } catch {
56908
+ } catch (err) {
56909
+ console.warn(`[floless] credential-issue detection failed for app "${id}" (treating as no credential issue):`, err);
56866
56910
  }
56867
56911
  return null;
56868
56912
  }
@@ -56903,7 +56947,7 @@ async function startServer() {
56903
56947
  broadcast({ type: "run-started", id, dryRun: !!dryRun, simulate: !!simulate, runId: result.runId });
56904
56948
  for (const ev of events) broadcast({ type: "trace", id, event: ev });
56905
56949
  broadcast({ type: "run-ended", id, runId: result.runId });
56906
- const report = extractReportHtml(events);
56950
+ const report = withBadge(extractReportHtml(events));
56907
56951
  const credentialIssue = !simulate && hasAuthFailure(events) ? await detectCredentialIssue(id) : null;
56908
56952
  return { ok: true, runId: result.runId, tracePath: result.tracePath, events, report, credentialIssue };
56909
56953
  }
@@ -56961,7 +57005,7 @@ async function startServer() {
56961
57005
  broadcast({ type: "debug-ended", id, nodeId });
56962
57006
  const r = result.result ?? {};
56963
57007
  const html = typeof r.html === "string" ? r.html : null;
56964
- return { ok: true, result, report: html ? { nodeId, html } : null };
57008
+ return { ok: true, result, report: withBadge(html ? { nodeId, html } : null) };
56965
57009
  }
56966
57010
  );
56967
57011
  app.get("/api/routines", async () => ({
package/dist/web/aware.js CHANGED
@@ -2654,7 +2654,7 @@
2654
2654
  reloadTimer = setTimeout(() => loadApp(currentId).catch(reportErr), 250);
2655
2655
  } else if (m.type === 'seat-taken') {
2656
2656
  // This session's seat was claimed by another device (newest-login-wins).
2657
- showToast('Your session was taken over on another device — sign in to continue.', 'err');
2657
+ showToast('Signed in on another device — sign in here to continue.', 'err');
2658
2658
  recheckLicenseAndGate();
2659
2659
  }
2660
2660
  };
@@ -3832,10 +3832,33 @@
3832
3832
  // We fetch NO workspace data while ungated. The license endpoints stay open.
3833
3833
  function renderGate(status) {
3834
3834
  if (document.getElementById('license-gate')) return;
3835
- const expired = status.state === 'expired';
3836
3835
  // signInUrl is server-generated, but escape it anyway before it lands in an
3837
3836
  // innerHTML href — a crafted FLOLESS_WEB_BASE shouldn't be able to break out.
3838
3837
  const subUrl = escapeHtml(status.signInUrl || 'https://floless.io');
3838
+ // Gate copy is driven by WHY the user is locked out. `state==='expired'` is a
3839
+ // distinct (trial-expiry) state; every other gated case carries a `reason`.
3840
+ // `secondary:null` omits the cross-sell link (session-expired / seat-taken).
3841
+ // Keys must mirror the server's LicenseReason values (+ the 'expired' state);
3842
+ // an unmapped/new reason falls back to 'signed-out' copy below.
3843
+ const reason = status.state === 'expired' ? 'expired' : status.reason;
3844
+ const COPY = {
3845
+ expired: { headline: 'Your subscription has expired',
3846
+ body: 'Renew your FloLess subscription to keep using floless.app.',
3847
+ secondary: { label: 'Manage subscription →', href: subUrl } },
3848
+ 'no-subscription': { headline: 'Subscription required',
3849
+ body: 'A FloLess subscription is required to use floless.app. Sign in to continue.',
3850
+ secondary: { label: 'No subscription yet? Subscribe →', href: subUrl } },
3851
+ 'session-expired': { headline: 'Please sign in again',
3852
+ body: 'Your sign-in has expired. Signing in takes a few seconds.',
3853
+ secondary: null },
3854
+ 'seat-taken': { headline: 'Signed in on another device',
3855
+ body: 'floless.app can run on one device at a time. Sign in here to use it on this device.',
3856
+ secondary: null },
3857
+ 'signed-out': { headline: 'Sign in to continue',
3858
+ body: 'Sign in with your FloLess account to use floless.app.',
3859
+ secondary: { label: 'No subscription yet? Subscribe →', href: subUrl } },
3860
+ };
3861
+ const copy = COPY[reason] || COPY['signed-out']; // neutral fallback for an unknown/absent reason
3839
3862
  const style = document.createElement('style');
3840
3863
  style.textContent = `
3841
3864
  #license-gate{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;
@@ -3868,13 +3891,11 @@
3868
3891
  <path class="mark-node" d="M138.092 174.908C148.53 174.908 156.992 166.446 156.992 156.008C156.992 145.57 148.53 137.108 138.092 137.108C127.654 137.108 119.192 145.57 119.192 156.008C119.192 166.446 127.654 174.908 138.092 174.908Z" fill="white" stroke="currentColor" stroke-width="8.72093" stroke-linecap="round" stroke-linejoin="round"/>
3869
3892
  <path class="mark-node" d="M149.714 43.6142C160.152 43.6142 168.614 35.1523 168.614 24.7141C168.614 14.2758 160.152 5.81396 149.714 5.81396C139.276 5.81396 130.814 14.2758 130.814 24.7141C130.814 35.1523 139.276 43.6142 149.714 43.6142Z" fill="white" stroke="currentColor" stroke-width="8.72093" stroke-linecap="round" stroke-linejoin="round"/>
3870
3893
  </svg></div>
3871
- <h1>${expired ? 'Your subscription has expired' : 'Subscription required'}</h1>
3872
- <p>${expired
3873
- ? 'Renew your FloLess subscription to keep using floless.app.'
3874
- : 'floless.app runs on your FloLess subscription. Sign in to continue.'}</p>
3894
+ <h1>${escapeHtml(copy.headline)}</h1>
3895
+ <p>${escapeHtml(copy.body)}</p>
3875
3896
  <button id="lg-signin" class="primary">Sign in at floless.io</button>
3876
3897
  <p class="lg-status" id="lg-status"></p>
3877
- <a class="lg-sub" href="${subUrl}" target="_blank" rel="noopener">${expired ? 'Manage subscription →' : 'No subscription yet? Subscribe →'}</a>
3898
+ ${copy.secondary ? `<a class="lg-sub" href="${copy.secondary.href}" target="_blank" rel="noopener">${escapeHtml(copy.secondary.label)}</a>` : ''}
3878
3899
  <p class="lg-version" id="lg-version"></p>
3879
3900
  </div>`;
3880
3901
  document.body.appendChild(wrap);
@@ -3946,7 +3967,7 @@
3946
3967
  } catch { /* still failing — fall through to gate */ }
3947
3968
  }
3948
3969
  if (effectivelyLicensed) bootWorkspace();
3949
- else renderGate(effectiveStatus || { state: 'none' });
3970
+ else renderGate(effectiveStatus || { state: 'unlicensed' });
3950
3971
  };
3951
3972
 
3952
3973
  // Setup-first: if AWARE isn't ready, show the overlay and DEFER routing until it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.9.1",
3
+ "version": "0.10.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": {