aegis-desktop 0.8.31 → 0.8.32
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/lib/local/entitlement.js +99 -16
- package/main.js +9 -8
- package/package.json +1 -1
- package/renderer/app.js +7 -2
- package/vendor/aegis.js +10 -4
- package/vendor/auto-provision.js +12 -5
package/lib/local/entitlement.js
CHANGED
|
@@ -28,17 +28,37 @@
|
|
|
28
28
|
* THE SIGNAL (see the report / the module's `probe`): main injects a probe that
|
|
29
29
|
* calls `aegis.verifyApiKey()` — aegis1 `/api/verify-api-key`, which its own
|
|
30
30
|
* docstring describes as "Verify a shared api_key and return plan/email/
|
|
31
|
-
* memory_token". The plan field is the account's
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* no new
|
|
31
|
+
* memory_token". The plan field is the account's plan, and aegis1 owns the
|
|
32
|
+
* vocabulary for it (`services/billing_service.py`: `TIERS = ("free", "pro",
|
|
33
|
+
* "unlimited")`, `PAID_PLANS = ("pro", "unlimited")`, plus the legacy aliases
|
|
34
|
+
* that normalize onto one of them). This module MIRRORS that vocabulary rather
|
|
35
|
+
* than inventing one; there is no new billing anything here, no new endpoint,
|
|
36
|
+
* and no write to the server. The probe prefers the server's `plan_effective`
|
|
37
|
+
* (its own `is_pro()` verdict, trial included) over the stored `plan` string
|
|
38
|
+
* when it is present, because a welcome-trial account is a real Unlimited
|
|
39
|
+
* entitlement that `users.plan` does not record — see `classifyProbe`.
|
|
40
|
+
*
|
|
41
|
+
* THE BUG THIS VOCABULARY COST (fixed here). `PAID_PLANS` read `("pro",)`,
|
|
42
|
+
* mirroring the retired €2 line and MISSING `unlimited` — the €9/mo tier that
|
|
43
|
+
* is the only one sold. So `normalizePlan('unlimited')` fell through to `free`,
|
|
44
|
+
* and a paying Unlimited subscriber with a spent-out bank was handed the
|
|
45
|
+
* free-tier boundary: the fan-out refused, the cloud drain clamped to one local
|
|
46
|
+
* worker, and a "top up your balance" prompt — for an account whose plan is
|
|
47
|
+
* precisely what pays for the drain. aegis1's own `LEGACY_PLAN_ALIASES` carries
|
|
48
|
+
* the matching warning from the other side ("Deliberately NOT an alias:
|
|
49
|
+
* `unlimited` is a live tier you can subscribe to, so it must resolve through
|
|
50
|
+
* TIERS"), and its `test_tier_vocabulary` pins `PAID_PLANS` at two entries.
|
|
51
|
+
* Two implementations of "is this account paid" had already drifted once; this
|
|
52
|
+
* was the drift.
|
|
36
53
|
*
|
|
37
54
|
* PHASE 6 — THE SECOND SIGNAL, THE BALANCE. The plan is only half of how this
|
|
38
55
|
* product charges. aegis1's actual paywall is the Token Bank running dry: the
|
|
39
56
|
* balance is prepaid EUR the OPERATOR credits by hand, and `app.py` states the
|
|
40
57
|
* consequence plainly — "Pool-key users with token bank balance skip legacy
|
|
41
|
-
* credit checks." So entitlement here is `
|
|
58
|
+
* credit checks." So entitlement here is `paid_plan || balance > 0` — and the
|
|
59
|
+
* `paid_plan` half is what carries an Unlimited subscriber whose bank is empty;
|
|
60
|
+
* the bank is an OVERAGE lever, never the thing that decides whether a paying
|
|
61
|
+
* account may use the feature it pays for. When the
|
|
42
62
|
* bank reads zero the wall names the fix that exists (a top-up, campaign
|
|
43
63
|
* `topup_bank`) instead of a plan checkout the user may already own. Nothing in
|
|
44
64
|
* this module grants a balance, expires one, or auto-charges: the operator
|
|
@@ -64,8 +84,33 @@
|
|
|
64
84
|
/** The free-tier fan-out ceiling: one worker per task, i.e. a plain turn. */
|
|
65
85
|
const FREE_MAX_WORKERS = 1;
|
|
66
86
|
|
|
67
|
-
/**
|
|
68
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Aegis1's paid tiers. MIRROR of services/billing_service.py
|
|
89
|
+
* `PAID_PLANS = ("pro", "unlimited")` — pinned literal on purpose, because this
|
|
90
|
+
* is the line that has to move whenever the server sells another tier.
|
|
91
|
+
*
|
|
92
|
+
* `unlimited` (€9/mo) is the tier ON SALE and the one a paying customer almost
|
|
93
|
+
* certainly owns; `pro` is the grandfathered €2 line, kept because its
|
|
94
|
+
* subscribers keep their entitlements. Reading only `pro` was not a narrower
|
|
95
|
+
* version of this gate, it was a gate pointing at the wrong tier: every
|
|
96
|
+
* Unlimited account fell to the free boundary. `starter` stays out — aegis1
|
|
97
|
+
* itself calls that one free.
|
|
98
|
+
*/
|
|
99
|
+
const PAID_PLANS = Object.freeze(['pro', 'unlimited']);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* What each paid tier is CALLED, so a surface that names the account's plan
|
|
103
|
+
* names the one the server granted. The CLI has read the tier off the server's
|
|
104
|
+
* plan string since 6443a60 (`cli/src/entitlement.js::paymentView`) after it
|
|
105
|
+
* told a €9 subscriber they owned the retired €2 plan; this is the same value,
|
|
106
|
+
* resolved once in the shared module both hosts read.
|
|
107
|
+
*/
|
|
108
|
+
const TIER_NAMES = Object.freeze({ pro: 'Pro', unlimited: 'Unlimited' });
|
|
109
|
+
|
|
110
|
+
/** The display name for a plan string, or null when it is not a paid tier. */
|
|
111
|
+
function tierName(plan) {
|
|
112
|
+
return TIER_NAMES[normalizePlan(plan)] || null;
|
|
113
|
+
}
|
|
69
114
|
|
|
70
115
|
/**
|
|
71
116
|
* MIRROR of services/billing_service.py `LEGACY_PLAN_ALIASES`: every stored
|
|
@@ -251,11 +296,20 @@ function promptUrlFor(host) {
|
|
|
251
296
|
* card on the next refresh and long enough that a drain never probes per task. */
|
|
252
297
|
const DEFAULT_TTL_MS = 60 * 1000;
|
|
253
298
|
|
|
254
|
-
/** Collapse any stored/alias plan string onto a tier, exactly as aegis1 does.
|
|
299
|
+
/** Collapse any stored/alias plan string onto a tier, exactly as aegis1 does.
|
|
300
|
+
*
|
|
301
|
+
* Aegis1 checks its live `TIERS` BEFORE the alias table (`normalize_plan`), so
|
|
302
|
+
* an alias can never shadow a tier it also names — and `unlimited` is a live
|
|
303
|
+
* tier. It is therefore returned as itself and never collapsed onto `pro`: the
|
|
304
|
+
* two grant different entitlements server-side (the monthly Unlimited
|
|
305
|
+
* allowance and the BYOK fee waiver are `is_unlimited()`-gated), and telling a
|
|
306
|
+
* €9 customer they own the retired €2 plan is the mistake commit 6443a60
|
|
307
|
+
* exists to record.
|
|
308
|
+
*/
|
|
255
309
|
function normalizePlan(plan) {
|
|
256
310
|
const raw = String(plan == null ? '' : plan).trim().toLowerCase();
|
|
257
311
|
if (!raw) return 'free';
|
|
258
|
-
if (PAID_PLANS.includes(raw)) return
|
|
312
|
+
if (PAID_PLANS.includes(raw)) return raw;
|
|
259
313
|
const aliased = LEGACY_PLAN_ALIASES[raw];
|
|
260
314
|
return aliased || 'free';
|
|
261
315
|
}
|
|
@@ -265,9 +319,9 @@ function normalizePlan(plan) {
|
|
|
265
319
|
* drift into two different instructions. */
|
|
266
320
|
const ASK_EXAMPLE = 'Do X autonomously';
|
|
267
321
|
|
|
268
|
-
/** True only for a tier aegis1 itself would call paid. */
|
|
322
|
+
/** True only for a tier aegis1 itself would call paid (`pro` or `unlimited`). */
|
|
269
323
|
function isPaidPlan(plan) {
|
|
270
|
-
return normalizePlan(plan)
|
|
324
|
+
return normalizePlan(plan) !== 'free';
|
|
271
325
|
}
|
|
272
326
|
|
|
273
327
|
/**
|
|
@@ -367,7 +421,19 @@ function classifyProbe(result, error) {
|
|
|
367
421
|
const body = result && typeof result === 'object' ? result : {};
|
|
368
422
|
const bal = balanceOf(body);
|
|
369
423
|
if (body.valid === false) return { source: 'no-key', plan: 'free', degraded: false, ...bal };
|
|
370
|
-
|
|
424
|
+
// WHICH plan field. `plan_effective` is aegis1's own entitlement verdict —
|
|
425
|
+
// the same predicate its `is_pro()` gates pooled calls on, so it counts the
|
|
426
|
+
// 30-day welcome trial that never writes `users.plan` (see app.py's
|
|
427
|
+
// register_device: the trial lives in `subscriptions`). The stored `plan`
|
|
428
|
+
// column cannot express that account, so an installing user read as `free`
|
|
429
|
+
// here and only the €12 signup credit kept the gate open — spend it, or fail
|
|
430
|
+
// to receive it, and a real Unlimited account was refused the feature. The
|
|
431
|
+
// stored string is still the fallback: an older server that predates the
|
|
432
|
+
// field must keep working, and it is the same value when nothing is on trial.
|
|
433
|
+
const rawPlan = body.plan_effective != null && String(body.plan_effective).trim()
|
|
434
|
+
? body.plan_effective
|
|
435
|
+
: body.plan;
|
|
436
|
+
const stated = String(rawPlan == null ? '' : rawPlan).trim();
|
|
371
437
|
if (!stated) {
|
|
372
438
|
// A key that verified but carried no plan: aegis1's own default for such an
|
|
373
439
|
// account is `free` (the column default), and a missing field is not proof
|
|
@@ -411,6 +477,8 @@ function createEntitlement({
|
|
|
411
477
|
balanceZero: false,
|
|
412
478
|
wall: 'plan',
|
|
413
479
|
plan: 'free',
|
|
480
|
+
tier: null,
|
|
481
|
+
tierName: null,
|
|
414
482
|
planRaw: null,
|
|
415
483
|
maxWorkers: FREE_MAX_WORKERS,
|
|
416
484
|
source: 'unresolved',
|
|
@@ -424,12 +492,20 @@ function createEntitlement({
|
|
|
424
492
|
let inflight = null;
|
|
425
493
|
|
|
426
494
|
function fromProbe({ source, plan, degraded, planRaw, balanceKnown = false, balanceMicros = null }) {
|
|
427
|
-
|
|
495
|
+
// The tier itself, not a paid/not-paid bit: `unlimited` and `pro` are both
|
|
496
|
+
// paid and must stay distinguishable, because the surfaces that name the
|
|
497
|
+
// account's plan (the desktop's queue boundary, the CLI's `/billing`) name
|
|
498
|
+
// the one the server granted. `pro` below keeps its original meaning — "the
|
|
499
|
+
// PLAN is paid for", as opposed to a free plan carried by a funded bank.
|
|
500
|
+
const tier = normalizePlan(plan);
|
|
501
|
+
const pro = tier !== 'free';
|
|
428
502
|
const banked = balanceKnown && Number.isFinite(balanceMicros) && balanceMicros > 0;
|
|
429
503
|
// TWO independent ways to be paid for: a paid plan, or a funded bank. The
|
|
430
504
|
// operator controls the second by hand (aegis1 admin `add_funds`), which is
|
|
431
505
|
// the lever this wall is built around — nothing here grants, times out or
|
|
432
|
-
// auto-charges; it only reads the number the operator set.
|
|
506
|
+
// auto-charges; it only reads the number the operator set. The FIRST way is
|
|
507
|
+
// the one a paying subscriber holds, and it does not weaken when their bank
|
|
508
|
+
// runs dry: the balance is overage, the subscription is the entitlement.
|
|
433
509
|
const entitled = pro || banked;
|
|
434
510
|
// The bank was READABLE and EMPTY — the operator's own signal that this
|
|
435
511
|
// account is now paying, not the absence of a plan.
|
|
@@ -447,7 +523,12 @@ function createEntitlement({
|
|
|
447
523
|
balanceMicros: balanceKnown ? balanceMicros : null,
|
|
448
524
|
balanceZero,
|
|
449
525
|
wall,
|
|
450
|
-
|
|
526
|
+
// The tier a paid PLAN grants, else `free`. A funded bank on a free plan
|
|
527
|
+
// is still `free` here — the bank is not a tier, and naming one would put
|
|
528
|
+
// a plan on the account the server never granted.
|
|
529
|
+
plan: pro ? tier : 'free',
|
|
530
|
+
tier: pro ? tier : null,
|
|
531
|
+
tierName: pro ? (TIER_NAMES[tier] || null) : null,
|
|
451
532
|
planRaw: planRaw == null ? null : String(planRaw),
|
|
452
533
|
// A paid session has no ceiling of its own here: main.js still clamps the
|
|
453
534
|
// renderer's number to QUEUE_MAX_WORKERS, which stays the only upper bound.
|
|
@@ -543,6 +624,8 @@ module.exports = {
|
|
|
543
624
|
createEntitlement,
|
|
544
625
|
normalizePlan,
|
|
545
626
|
isPaidPlan,
|
|
627
|
+
tierName,
|
|
628
|
+
TIER_NAMES,
|
|
546
629
|
classifyProbe,
|
|
547
630
|
fanoutRefusal,
|
|
548
631
|
balanceRefusal,
|
package/main.js
CHANGED
|
@@ -3546,14 +3546,15 @@ function bootstrap() {
|
|
|
3546
3546
|
// Nothing configured anywhere (env, this app's encrypted copy, or the
|
|
3547
3547
|
// shared store): a fresh install. Auto-provision a free device key
|
|
3548
3548
|
// from aegis1 — same shared helper mcp/server.js and the CLI use, so
|
|
3549
|
-
// the desktop app works with no manual sign-in step.
|
|
3550
|
-
//
|
|
3551
|
-
//
|
|
3552
|
-
//
|
|
3553
|
-
//
|
|
3554
|
-
//
|
|
3555
|
-
//
|
|
3556
|
-
// launch tries
|
|
3549
|
+
// the desktop app works with no manual sign-in step. A brand-new
|
|
3550
|
+
// fingerprint is granted aegis1's 30-day welcome-trial `unlimited`
|
|
3551
|
+
// entitlement (token-bank credit + a subscriptions row) in the same
|
|
3552
|
+
// register-device call that mints the key — see client/
|
|
3553
|
+
// auto-provision.js's header for the aegis1 commit that made this a
|
|
3554
|
+
// real grant rather than a badge. Failure (offline, aegis1
|
|
3555
|
+
// unreachable) falls through exactly as before: the renderer's
|
|
3556
|
+
// normal "not signed in" state handles it, and the next launch tries
|
|
3557
|
+
// again.
|
|
3557
3558
|
const result = await autoProvision.autoProvisionApiKey(aegis, credentials);
|
|
3558
3559
|
if (result.key) settings.setAegisKey(result.key);
|
|
3559
3560
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.32",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/renderer/app.js
CHANGED
|
@@ -1536,9 +1536,14 @@ function renderQueueEntitlement(state) {
|
|
|
1536
1536
|
if (!resolved) els.queueBoundary.textContent = '';
|
|
1537
1537
|
else if (entitled) {
|
|
1538
1538
|
// Say which of the two ways this account is paid for, so a funded bank
|
|
1539
|
-
// does not read as a Pro plan somebody did not buy
|
|
1539
|
+
// does not read as a Pro plan somebody did not buy — and name WHICH tier
|
|
1540
|
+
// the plan is: `ent.tierName` is resolved in main from the plan string the
|
|
1541
|
+
// server stated (shared module's TIER_NAMES), so a €9 Unlimited subscriber
|
|
1542
|
+
// is not told they own the retired €2 plan. That mislabelling is the same
|
|
1543
|
+
// class CLI commit 6443a60 fixed in `/billing`; the literal is only a
|
|
1544
|
+
// fallback for a snapshot from a host that predates the field.
|
|
1540
1545
|
els.queueBoundary.textContent = ent.pro
|
|
1541
|
-
? 'Pro — the cloud drain and the fan-out are unlocked
|
|
1546
|
+
? `${ent.tierName || 'Pro'} — the cloud drain and the fan-out are unlocked.`
|
|
1542
1547
|
: 'Balance on your account — the cloud drain and the fan-out are unlocked.';
|
|
1543
1548
|
} else {
|
|
1544
1549
|
// The refusal names the Pro value AND keeps the free tier's real reach
|
package/vendor/aegis.js
CHANGED
|
@@ -841,10 +841,16 @@ function createClient(opts = {}) {
|
|
|
841
841
|
* Ask aegis1 for a free device key when the caller has none configured yet
|
|
842
842
|
* (`POST /api/v1/auth/register-device`). Idempotent server-side on
|
|
843
843
|
* `fingerprint` — the same fingerprint always returns the same account's
|
|
844
|
-
* key rather than minting a new one.
|
|
845
|
-
*
|
|
846
|
-
*
|
|
847
|
-
*
|
|
844
|
+
* key rather than minting a new one. A BRAND-NEW fingerprint (first call
|
|
845
|
+
* ever for this machine) is additionally granted aegis1's 30-day
|
|
846
|
+
* welcome-trial `unlimited` entitlement in the SAME request that mints the
|
|
847
|
+
* key — a token-bank credit of UNLIMITED_CLOUD_ALLOWANCE_MICROS plus a
|
|
848
|
+
* time-boxed `subscriptions` row (aegis1 commit 4372830), so a fresh
|
|
849
|
+
* install can use pooled cloud models immediately instead of starting at a
|
|
850
|
+
* zero balance. Re-registering an already-known fingerprint does NOT
|
|
851
|
+
* re-grant it; the pool balance gate still applies once the allowance is
|
|
852
|
+
* spent or the 30 days lapse. Sends no API key of its own — there isn't
|
|
853
|
+
* one yet.
|
|
848
854
|
*/
|
|
849
855
|
async function registerDevice(fingerprint, opts) {
|
|
850
856
|
return apiPost('/api/v1/auth/register-device', { fingerprint }, undefined, opts);
|
package/vendor/auto-provision.js
CHANGED
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
* device-fingerprint.js) so a wiped credential store still resolves back to
|
|
13
13
|
* the same account rather than minting a new, unfunded one.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* As of aegis1 commit 4372830, a brand-new fingerprint (first call ever for
|
|
16
|
+
* this machine) is granted the 30-day welcome-trial `unlimited` entitlement
|
|
17
|
+
* server-side — a token-bank credit (UNLIMITED_CLOUD_ALLOWANCE_MICROS) and a
|
|
18
|
+
* time-boxed `unlimited` row in `subscriptions` — in the SAME request that
|
|
19
|
+
* mints the key, not a separate step this client has to ask for. is_pro()'s
|
|
20
|
+
* pool gate and the BYOK fee waiver both see that grant immediately. This
|
|
21
|
+
* comment previously said the account "starts at ZERO balance" and "only
|
|
22
|
+
* unblocks BYOK" — that described pre-4372830 behavior and was stale; see
|
|
23
|
+
* services/billing_service.py's is_pro()/is_unlimited() docstrings on the
|
|
24
|
+
* aegis1 side for the current contract. A re-registration of an already-known
|
|
25
|
+
* fingerprint (the idempotent path) does NOT re-grant the trial — it only
|
|
26
|
+
* happens once per machine, ever.
|
|
20
27
|
*/
|
|
21
28
|
const { machineFingerprint } = require('./device-fingerprint.js');
|
|
22
29
|
|