@floless/app 0.9.0 → 0.9.2
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 +31 -17
- package/dist/web/aware.js +29 -8
- package/package.json +1 -1
package/dist/floless-server.cjs
CHANGED
|
@@ -52055,10 +52055,10 @@ function jwtExpiry(jwt) {
|
|
|
52055
52055
|
var NetworkError = class extends Error {
|
|
52056
52056
|
network = true;
|
|
52057
52057
|
};
|
|
52058
|
-
async function
|
|
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
|
|
52124
|
+
let resolved;
|
|
52122
52125
|
try {
|
|
52123
|
-
|
|
52126
|
+
resolved = await resolveToken();
|
|
52124
52127
|
} catch {
|
|
52125
52128
|
return finish(offlineFallback(url));
|
|
52126
52129
|
}
|
|
52127
|
-
if (
|
|
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.
|
|
52364
|
+
define: true ? "0.9.2" : 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.
|
|
52374
|
+
return resolveChannel({ isSea: isSea2(), define: true ? "0.9.2" : void 0 });
|
|
52371
52375
|
}
|
|
52372
52376
|
|
|
52373
52377
|
// oauth-presets.ts
|
|
@@ -52406,13 +52410,23 @@ function managedProfileYaml(preset) {
|
|
|
52406
52410
|
function oauthDir() {
|
|
52407
52411
|
return process.env.AWARE_HOME ? (0, import_node_path6.join)(process.env.AWARE_HOME, "oauth") : (0, import_node_path6.join)((0, import_node_os6.homedir)(), ".aware", "oauth");
|
|
52408
52412
|
}
|
|
52413
|
+
function isUpgradableLegacyProfile(existing, preset) {
|
|
52414
|
+
if (existing.startsWith(MANAGED_HEADER)) return false;
|
|
52415
|
+
const content = existing.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
52416
|
+
const only = content.length === 1 ? content[0] : void 0;
|
|
52417
|
+
if (only === void 0) return false;
|
|
52418
|
+
const m = /^client_id\s*:\s*(.+?)\s*$/.exec(only);
|
|
52419
|
+
return m !== null && m[1] === preset.clientId;
|
|
52420
|
+
}
|
|
52409
52421
|
function ensureManagedProfile(id) {
|
|
52410
52422
|
const preset = OAUTH_PRESETS[id];
|
|
52411
52423
|
if (!preset) return "not-managed";
|
|
52412
52424
|
const dir = oauthDir();
|
|
52413
52425
|
const file = (0, import_node_path6.join)(dir, `${id}.yaml`);
|
|
52414
52426
|
const existing = (0, import_node_fs7.existsSync)(file) ? (0, import_node_fs7.readFileSync)(file, "utf8") : null;
|
|
52415
|
-
if (existing !== null && !existing.startsWith(MANAGED_HEADER))
|
|
52427
|
+
if (existing !== null && !existing.startsWith(MANAGED_HEADER) && !isUpgradableLegacyProfile(existing, preset)) {
|
|
52428
|
+
return "skipped";
|
|
52429
|
+
}
|
|
52416
52430
|
const desired = managedProfileYaml(preset);
|
|
52417
52431
|
if (existing === desired) return "unchanged";
|
|
52418
52432
|
(0, import_node_fs7.mkdirSync)(dir, { recursive: true });
|
|
@@ -56519,9 +56533,9 @@ async function startServer() {
|
|
|
56519
56533
|
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,
|
|
56520
56534
|
// no workspace data, never spawns `aware`. Exempt like the other update routes.
|
|
56521
56535
|
req.url.startsWith("/api/release-notes")) return;
|
|
56522
|
-
const { state: state2, signInUrl: signInUrl2 } = await getLicenseStatus();
|
|
56536
|
+
const { state: state2, reason, signInUrl: signInUrl2 } = await getLicenseStatus();
|
|
56523
56537
|
if (state2 !== "valid" && state2 !== "offline-grace") {
|
|
56524
|
-
return reply.status(402).send({ ok: false, error: "subscription required", state: state2, signInUrl: signInUrl2 });
|
|
56538
|
+
return reply.status(402).send({ ok: false, error: "subscription required", state: state2, reason, signInUrl: signInUrl2 });
|
|
56525
56539
|
}
|
|
56526
56540
|
});
|
|
56527
56541
|
app.addHook("onRequest", async (req, reply) => {
|
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('
|
|
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>${
|
|
3872
|
-
<p>${
|
|
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
|
-
|
|
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: '
|
|
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
|