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.
- package/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- 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
|
-
|
|
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};
|