aegis-desktop 1.0.13 → 1.0.15

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.
Files changed (96) hide show
  1. package/SOURCE-OFFER.txt +9 -0
  2. package/bin/aegis.js +1 -32
  3. package/lib/account-key.js +1 -56
  4. package/lib/auto.js +1 -430
  5. package/lib/avatar/companion.js +1 -698
  6. package/lib/avatar/cosmetics.js +1 -532
  7. package/lib/avatar/events.js +1 -344
  8. package/lib/avatar/holder-turn.js +1 -350
  9. package/lib/avatar/holders.js +1 -612
  10. package/lib/avatar/identity.js +1 -641
  11. package/lib/avatar/level.js +1 -368
  12. package/lib/avatar/mirror.js +1 -524
  13. package/lib/avatar/persona.js +1 -388
  14. package/lib/avatar/profile.js +1 -977
  15. package/lib/avatar/register.js +1 -307
  16. package/lib/avatar/store.js +1 -472
  17. package/lib/avatar/tiers.js +1 -38
  18. package/lib/avatar/transfer.js +1 -415
  19. package/lib/avatar/turn.js +1 -216
  20. package/lib/avatar/voice.js +1 -328
  21. package/lib/avatar/xp.js +1 -404
  22. package/lib/deep-link.js +1 -120
  23. package/lib/fingerprint.js +2 -347
  24. package/lib/google-connect.js +1 -169
  25. package/lib/local/agents.js +1 -105
  26. package/lib/local/autonomous.js +1 -1045
  27. package/lib/local/claude-pro.js +1 -993
  28. package/lib/local/context.js +1 -95
  29. package/lib/local/engine.js +1 -3228
  30. package/lib/local/entitlement.js +1 -654
  31. package/lib/local/git-scope.js +1 -228
  32. package/lib/local/local.js +1 -2135
  33. package/lib/local/prompt.js +1 -275
  34. package/lib/local/queue.js +1 -560
  35. package/lib/local/session-rounds.js +1 -208
  36. package/lib/local/shell.js +1 -680
  37. package/lib/local/telemetry.js +1 -470
  38. package/lib/local/tools.js +1 -1238
  39. package/lib/local/turn-guard.js +1 -300
  40. package/lib/local/worktree-lock.js +1 -235
  41. package/lib/provider-key-mirror.js +1 -252
  42. package/lib/quick-launcher.js +1 -84
  43. package/lib/settings.js +1 -890
  44. package/lib/sync/memory-queue.js +1 -60
  45. package/lib/sync/persist-gate.js +1 -148
  46. package/lib/sync/sessions.js +1 -106
  47. package/lib/window-state.js +1 -66
  48. package/main.js +3 -4390
  49. package/package.json +3 -5
  50. package/preload.js +3 -628
  51. package/renderer/app.js +1 -6560
  52. package/renderer/avatar/assemble.js +1 -335
  53. package/renderer/avatar/avatar.js +1 -380
  54. package/renderer/avatar/cards.js +1 -141
  55. package/renderer/avatar/hud.js +1 -206
  56. package/renderer/avatar/machine.js +1 -213
  57. package/renderer/avatar/pane.js +1 -439
  58. package/renderer/avatar/parts.js +1 -437
  59. package/renderer/budget.js +1 -168
  60. package/renderer/companion.js +1 -223
  61. package/renderer/diff.js +1 -359
  62. package/renderer/diffview.js +1 -336
  63. package/renderer/fingerprint.js +3 -720
  64. package/renderer/markdown.js +1 -168
  65. package/renderer/provider-toolbar.js +1 -134
  66. package/renderer/providers.js +1 -259
  67. package/renderer/quick.js +1 -191
  68. package/renderer/stream-policy.js +1 -306
  69. package/renderer/transcript-view.js +1 -323
  70. package/renderer/usage.js +1 -1239
  71. package/vendor/aegis.js +1 -2054
  72. package/vendor/attest.js +1 -442
  73. package/vendor/authorship-key.js +1 -410
  74. package/vendor/auto-provision.js +1 -134
  75. package/vendor/brain-catalog.js +1 -268
  76. package/vendor/byok-catalog.js +1 -230
  77. package/vendor/credentials.js +1 -494
  78. package/vendor/device-fingerprint.js +1 -141
  79. package/vendor/drive-connect.js +1 -85
  80. package/vendor/drive-upload.js +1 -174
  81. package/vendor/env-file.js +1 -1273
  82. package/vendor/fingerprint.js +1 -1064
  83. package/vendor/foreign-memory.js +1 -669
  84. package/vendor/google-signin.js +1 -125
  85. package/vendor/hooks.js +1 -430
  86. package/vendor/model-discovery.js +1 -330
  87. package/vendor/platform.js +1 -937
  88. package/vendor/prefs.js +1 -351
  89. package/vendor/presence-constants.js +1 -107
  90. package/vendor/presence.js +1 -438
  91. package/vendor/record-vault.js +3 -1055
  92. package/vendor/session-store.js +1 -548
  93. package/vendor/topup.js +1 -297
  94. package/vendor/touch.js +1 -458
  95. package/vendor/update.js +1 -704
  96. package/vendor/user-models.js +1 -381
package/vendor/topup.js CHANGED
@@ -1,299 +1,3 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-or-later
2
2
  // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
- // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
- // Conveying a modified copy, or running one as a network service, owes the
5
- // corresponding source under this licence (§5/§13) — see LICENSING.md.
6
- 'use strict';
7
-
8
- /**
9
- * topup.js — the shared token-bank top-up: what to ask for, how to read the
10
- * answer, and how to know the money arrived.
11
- *
12
- * WHY THIS IS SHARED AND NOT HOST CODE. A top-up ends server-side: Stripe's
13
- * webhook credits the bank (aegis1 `services/billing_service.py`,
14
- * `add_funds()`), and the browser landing page is pure UI on top of that. But
15
- * that page (`/topup`) is `@login_required` against a browser SESSION, and
16
- * `POST /api/v1/auth/register-device` mints the account this product
17
- * auto-provisions with a password that is never returned and an email at
18
- * `@device.aegiscloud`. So the very account auto-provision creates cannot sign
19
- * in to the page it was just sent to: following that link lands on a login
20
- * wall, and the user concludes the payment failed while the money and the
21
- * credit are both real.
22
- *
23
- * The key that STARTED the checkout is enough to READ the result —
24
- * `login_required` accepts `X-API-Key`/Bearer, and the same credential
25
- * authenticates `GET /api/token-bank/balance` — so each host confirms the
26
- * credit itself. Three hosts need that: the desktop (`desktop/main.js`), the
27
- * terminal (`aegiscode topup` / `/topup`), and whatever comes next.
28
- *
29
- * What must never differ between them, and therefore lives here:
30
- *
31
- * 1. the AMOUNT rule — aegis1 rejects anything outside 2..1000 EUR with 400,
32
- * and a client that accepts 1 EUR is a round trip the user pays for with
33
- * a rejection message;
34
- * 2. the PAYLOAD reading (`bankAmount`) — aegis1 has shipped
35
- * `balance_micros`, `balance_eur`, `balance` and a bare number, and two
36
- * hosts guessing differently is how one screen shows a credit the other
37
- * misses;
38
- * 3. the UNKNOWN-vs-ZERO rule — an unreadable bank stays unknown. Reading it
39
- * as zero would make the `{}` case look like a real baseline, so the FIRST
40
- * credit would be silently swallowed as "no change", and elsewhere a zero
41
- * manufactures a paywall out of a timeout;
42
- * 4. the RISING-READ rule — announce on the first read strictly above the
43
- * baseline, once, then stop.
44
- *
45
- * What is deliberately NOT here: the timer. The desktop's watch must live in
46
- * the main process (test/auto.test.mjs asserts "neither renderer starts a timer
47
- * of its own"), unref'd, because the window stays open and a payment may never
48
- * complete. The terminal's must block, because the process exits and a
49
- * detached timer would be killed before the credit lands. Those are two
50
- * schedulers over one state machine — and the state machine (`createCreditWatch`)
51
- * is the part that must not drift, so that is what this file owns.
52
- *
53
- * No I/O of its own and no host imports: it takes the client it is handed.
54
- */
55
-
56
- /** aegis1's own bounds (`app.py` token_bank_topup): outside these is a 400. */
57
- const TOPUP_MIN_EUR = 2;
58
- const TOPUP_MAX_EUR = 1000;
59
- /** The server's default when the body omits the amount. */
60
- const TOPUP_DEFAULT_EUR = 10;
61
-
62
- /** Poll period and window for the credit watch: ~2 minutes, then stop asking. */
63
- const TOPUP_POLL_MS = 4000;
64
- const TOPUP_POLL_TRIES = 30;
65
-
66
- /**
67
- * Read a euro amount out of an aegis1 token-bank payload, across the shapes the
68
- * server has used (`balance_micros`, `balance_eur`, `balance`, or a bare
69
- * number). Returns null when nothing numeric is present — an unreadable bank
70
- * must stay UNKNOWN rather than read as a zero.
71
- */
72
- function bankAmount(res) {
73
- if (res == null) return null;
74
- if (typeof res === 'number') return isFinite(res) ? res : null;
75
- if (typeof res === 'string') {
76
- const n = Number(res.trim());
77
- return res.trim() !== '' && isFinite(n) ? n : null;
78
- }
79
- if (typeof res !== 'object') return null;
80
- if (res.balance_micros != null) {
81
- const n = Number(res.balance_micros);
82
- return isFinite(n) ? n / 1e6 : null;
83
- }
84
- for (const key of ['balance_eur', 'balance', 'bank', 'balance_eur_cents']) {
85
- if (!(key in res)) continue;
86
- const n = Number(res[key]);
87
- if (!isFinite(n)) continue;
88
- return key === 'balance_eur_cents' ? n / 100 : n;
89
- }
90
- if (res.data) return bankAmount(res.data);
91
- return null;
92
- }
93
-
94
- /**
95
- * Parse a user-typed amount against the server's own bounds.
96
- *
97
- * Rejecting here rather than letting aegis1 answer 400 is not cosmetic: the
98
- * whole point of `/topup 1` failing locally is that it says WHY in the same
99
- * breath, instead of printing the server's "Amount must be between 2 and 1000
100
- * EUR" after a round trip on a slow connection.
101
- *
102
- * @param {string|number} raw the typed argument ('' or undefined ⇒ the default)
103
- * @param {number} [fallback] default when nothing was typed
104
- * @returns {{ok:boolean, amount:number|null, message:string|null}}
105
- */
106
- function parseTopupAmount(raw, fallback) {
107
- const str = String(raw == null ? '' : raw).trim();
108
- if (!str) {
109
- const d = fallback == null ? TOPUP_DEFAULT_EUR : Number(fallback);
110
- return { ok: true, amount: d, message: null };
111
- }
112
- // A euro figure with the separators a person actually types: `$20`, `20 EUR`,
113
- // `20eur`. No `\b` before the word — `25eur` has its boundary between the 5
114
- // and the e, so a boundary-anchored pattern strips nothing and the figure is
115
- // refused as "not an amount".
116
- const cleaned = str.replace(/[€$]|eur(o|os)?\b/gi, '').trim();
117
- const n = Number(cleaned);
118
- if (!isFinite(n) || cleaned === '') {
119
- return { ok: false, amount: null, message: `"${str}" is not an amount — give a number of euro, e.g. /topup 20` };
120
- }
121
- if (!Number.isInteger(n)) {
122
- return { ok: false, amount: null, message: `${n} EUR is not whole — Stripe charges whole euro here (e.g. /topup 20)` };
123
- }
124
- if (n < TOPUP_MIN_EUR || n > TOPUP_MAX_EUR) {
125
- return {
126
- ok: false,
127
- amount: null,
128
- message: `${n} EUR is outside the accepted range — top-ups are ${TOPUP_MIN_EUR}..${TOPUP_MAX_EUR} EUR`,
129
- };
130
- }
131
- return { ok: true, amount: n, message: null };
132
- }
133
-
134
- /**
135
- * Classify a failed checkout the way a user needs to hear it. The shared client
136
- * attaches `err.status` / `err.data` while the failure is still in the host, so
137
- * the status is read here rather than string-matched out of a message later.
138
- *
139
- * `setupRequired` is aegis1's 503 `setup_required` — Stripe is not configured
140
- * on the deployment, which is NOT the user's problem to fix and must not be
141
- * reported as one.
142
- *
143
- * @returns {{status:number, message:string, hint:string|null, setupRequired:boolean}}
144
- */
145
- function classifyTopupError(err) {
146
- const status = (err && (err.status || (err.response && err.response.status))) || 0;
147
- const data = (err && err.data) || {};
148
- const message = err && err.message ? err.message : String(err);
149
- const setupRequired = Boolean(data.setup_required) || status === 503;
150
- if (setupRequired) {
151
- return {
152
- status: status || 503,
153
- message,
154
- hint: 'this deployment has no payment provider configured — top-ups are unavailable here, not on your side',
155
- setupRequired: true,
156
- };
157
- }
158
- if (status === 400) {
159
- return { status, message, hint: `pick an amount between ${TOPUP_MIN_EUR} and ${TOPUP_MAX_EUR} EUR`, setupRequired: false };
160
- }
161
- if (status === 401 || status === 403) {
162
- return {
163
- status,
164
- message,
165
- hint: 'the account key was refused — save a fresh one (aegiscode login) and try again',
166
- setupRequired: false,
167
- };
168
- }
169
- if (status === 402) {
170
- return { status, message, hint: 'the server refused the checkout for this account — see /billing for the account state', setupRequired: false };
171
- }
172
- if (!status) {
173
- return { status: 0, message, hint: 'the server could not be reached — check the connection and retry', setupRequired: false };
174
- }
175
- return { status, message, hint: null, setupRequired: false };
176
- }
177
-
178
- /**
179
- * Create a token-bank checkout and hand back the Stripe-hosted URL.
180
- *
181
- * Never throws: a host that has to tell 401 from 400 from 503 must receive the
182
- * fields, and an exception crossing an IPC boundary loses `status` and `data`.
183
- *
184
- * @param {object} aegis the client whose key pays
185
- * @param {number} amount EUR, already validated by parseTopupAmount
186
- * @returns {Promise<{ok:boolean, url:string|null, status:number, message:string|null,
187
- * hint:string|null, setupRequired:boolean, amount:number}>}
188
- */
189
- async function startTopup(aegis, amount) {
190
- const eur = amount == null ? TOPUP_DEFAULT_EUR : Number(amount);
191
- if (!aegis || typeof aegis.tokenBankTopup !== 'function') {
192
- return {
193
- ok: false, url: null, status: 0, amount: eur, setupRequired: false,
194
- message: 'this host has no billing client', hint: null,
195
- };
196
- }
197
- try {
198
- const data = await aegis.tokenBankTopup(eur);
199
- const url = data && data.url ? String(data.url) : null;
200
- if (!url) {
201
- // A 2xx with no URL is a success shape we cannot act on. Saying so beats
202
- // opening "undefined" in a browser and calling it a payment.
203
- return {
204
- ok: false, url: null, status: 0, amount: eur, setupRequired: false,
205
- message: 'the server accepted the request but returned no checkout URL',
206
- hint: 'check the account in the browser at /billing, then retry',
207
- };
208
- }
209
- return { ok: true, url, status: 0, message: null, hint: null, setupRequired: false, amount: eur };
210
- } catch (err) {
211
- const info = classifyTopupError(err);
212
- return { ok: false, url: null, amount: eur, ...info };
213
- }
214
- }
215
-
216
- /**
217
- * The credit watch, as a state machine a host drives.
218
- *
219
- * `tick()` reads the bank once and returns the next state; the HOST decides
220
- * when to call it again (an unref'd interval in the desktop's main process, an
221
- * awaited sleep loop in the terminal). One read per call, so a slow or failing
222
- * balance call can never overlap itself and double-announce.
223
- *
224
- * States:
225
- * `pending` — nothing conclusive yet; call again
226
- * `credited` — the bank read strictly above the baseline: announce, then stop
227
- * `expired` — the window ran out with no rising read
228
- *
229
- * A failed or unreadable read is `pending`, never a zero: offline is unknown,
230
- * and unknown must not become a baseline (see the header).
231
- *
232
- * @param {object} aegis the client whose key read the balance
233
- * @param {object} [opts]
234
- * @param {number} [opts.maxTries] reads before giving up (default TOPUP_POLL_TRIES)
235
- * @returns {{tick:function():Promise<{state:string,amount:number|null,baseline:number|null}>,
236
- * stop:function():void, baseline:number|null, credited:number|null, tries:number}}
237
- */
238
- function createCreditWatch(aegis, opts) {
239
- const options = opts || {};
240
- const maxTries = options.maxTries || TOPUP_POLL_TRIES;
241
- const state = { baseline: null, credited: null, tries: 0, stopped: false };
242
- return {
243
- get baseline() {
244
- return state.baseline;
245
- },
246
- get credited() {
247
- return state.credited;
248
- },
249
- get tries() {
250
- return state.tries;
251
- },
252
- stop() {
253
- state.stopped = true;
254
- },
255
- async tick() {
256
- const answer = (s, amount) => ({ state: s, amount: amount == null ? null : amount, baseline: state.baseline });
257
- if (state.stopped) return answer(state.credited != null ? 'credited' : 'stopped', state.credited);
258
- state.tries += 1;
259
- if (state.tries > maxTries) {
260
- state.stopped = true;
261
- return answer('expired', null);
262
- }
263
- let now = null;
264
- try {
265
- now = bankAmount(await aegis.tokenBankBalance());
266
- } catch {
267
- return answer('pending', null); // offline or restarting: the next read retries
268
- }
269
- if (now === null) return answer('pending', null);
270
- if (state.baseline === null) {
271
- // The first successful read is the pre-payment figure. Stripe checkout
272
- // takes longer than one poll, so this is normally still the old balance
273
- // — but if it is already higher we still announce on the NEXT read,
274
- // because the baseline is what "higher" is measured against.
275
- state.baseline = now;
276
- return answer('pending', null);
277
- }
278
- if (now > state.baseline) {
279
- state.stopped = true;
280
- state.credited = now;
281
- return answer('credited', now);
282
- }
283
- return answer('pending', null);
284
- },
285
- };
286
- }
287
-
288
- module.exports = {
289
- TOPUP_MIN_EUR,
290
- TOPUP_MAX_EUR,
291
- TOPUP_DEFAULT_EUR,
292
- TOPUP_POLL_MS,
293
- TOPUP_POLL_TRIES,
294
- bankAmount,
295
- parseTopupAmount,
296
- classifyTopupError,
297
- startTopup,
298
- createCreditWatch,
299
- };
3
+ "use strict";const TOPUP_MIN_EUR=2,TOPUP_MAX_EUR=1e3,TOPUP_DEFAULT_EUR=10,TOPUP_POLL_MS=4e3,TOPUP_POLL_TRIES=30;function bankAmount(e){if(null==e)return null;if("number"==typeof e)return isFinite(e)?e:null;if("string"==typeof e){const t=Number(e.trim());return""!==e.trim()&&isFinite(t)?t:null}if("object"!=typeof e)return null;if(null!=e.balance_micros){const t=Number(e.balance_micros);return isFinite(t)?t/1e6:null}for(const t of["balance_eur","balance","bank","balance_eur_cents"]){if(!(t in e))continue;const n=Number(e[t]);if(isFinite(n))return"balance_eur_cents"===t?n/100:n}return e.data?bankAmount(e.data):null}function parseTopupAmount(e,t){const n=String(null==e?"":e).trim();if(!n)return{ok:!0,amount:null==t?10:Number(t),message:null};const u=n.replace(/[€$]|eur(o|os)?\b/gi,"").trim(),r=Number(u);return isFinite(r)&&""!==u?Number.isInteger(r)?r<TOPUP_MIN_EUR||r>TOPUP_MAX_EUR?{ok:!1,amount:null,message:`${r} EUR is outside the accepted range — top-ups are ${TOPUP_MIN_EUR}..${TOPUP_MAX_EUR} EUR`}:{ok:!0,amount:r,message:null}:{ok:!1,amount:null,message:`${r} EUR is not whole — Stripe charges whole euro here (e.g. /topup 20)`}:{ok:!1,amount:null,message:`"${n}" is not an amount — give a number of euro, e.g. /topup 20`}}function classifyTopupError(e){const t=e&&(e.status||e.response&&e.response.status)||0,n=e&&e.data||{},u=e&&e.message?e.message:String(e);return Boolean(n.setup_required)||503===t?{status:t||503,message:u,hint:"this deployment has no payment provider configured — top-ups are unavailable here, not on your side",setupRequired:!0}:400===t?{status:t,message:u,hint:`pick an amount between ${TOPUP_MIN_EUR} and ${TOPUP_MAX_EUR} EUR`,setupRequired:!1}:401===t||403===t?{status:t,message:u,hint:"the account key was refused — save a fresh one (aegiscode login) and try again",setupRequired:!1}:402===t?{status:t,message:u,hint:"the server refused the checkout for this account — see /billing for the account state",setupRequired:!1}:t?{status:t,message:u,hint:null,setupRequired:!1}:{status:0,message:u,hint:"the server could not be reached — check the connection and retry",setupRequired:!1}}async function startTopup(e,t){const n=null==t?10:Number(t);if(!e||"function"!=typeof e.tokenBankTopup)return{ok:!1,url:null,status:0,amount:n,setupRequired:!1,message:"this host has no billing client",hint:null};try{const t=await e.tokenBankTopup(n),u=t&&t.url?String(t.url):null;return u?{ok:!0,url:u,status:0,message:null,hint:null,setupRequired:!1,amount:n}:{ok:!1,url:null,status:0,amount:n,setupRequired:!1,message:"the server accepted the request but returned no checkout URL",hint:"check the account in the browser at /billing, then retry"}}catch(e){return{ok:!1,url:null,amount:n,...classifyTopupError(e)}}}function createCreditWatch(e,t){const n=(t||{}).maxTries||30,u={baseline:null,credited:null,tries:0,stopped:!1};return{get baseline(){return u.baseline},get credited(){return u.credited},get tries(){return u.tries},stop(){u.stopped=!0},async tick(){const t=(e,t)=>({state:e,amount:null==t?null:t,baseline:u.baseline});if(u.stopped)return t(null!=u.credited?"credited":"stopped",u.credited);if(u.tries+=1,u.tries>n)return u.stopped=!0,t("expired",null);let r=null;try{r=bankAmount(await e.tokenBankBalance())}catch{return t("pending",null)}return null===r?t("pending",null):null===u.baseline?(u.baseline=r,t("pending",null)):r>u.baseline?(u.stopped=!0,u.credited=r,t("credited",r)):t("pending",null)}}}module.exports={TOPUP_MIN_EUR:TOPUP_MIN_EUR,TOPUP_MAX_EUR:TOPUP_MAX_EUR,TOPUP_DEFAULT_EUR:10,TOPUP_POLL_MS:4e3,TOPUP_POLL_TRIES:30,bankAmount:bankAmount,parseTopupAmount:parseTopupAmount,classifyTopupError:classifyTopupError,startTopup:startTopup,createCreditWatch:createCreditWatch};