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/renderer/fingerprint.js
CHANGED
|
@@ -1,722 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* fingerprint.js — the φ(α) panel: authorship key, sealed records, the vault.
|
|
10
|
-
*
|
|
11
|
-
* The IPC surface has existed since 5072779 (`fingerprintStatus` …
|
|
12
|
-
* `fingerprintVaultAudit`, desktop/preload.js:171-183, main.js:750-765) and
|
|
13
|
-
* until now nothing in desktop/renderer/ called any of it —
|
|
14
|
-
* `grep -rn fingerprint desktop/renderer/` returned nothing, so a GUI user had
|
|
15
|
-
* no button for a feature the terminal has had since `aegiscode fingerprint`.
|
|
16
|
-
* This file is that button.
|
|
17
|
-
*
|
|
18
|
-
* It obeys the same two rules the backend half already keeps:
|
|
19
|
-
*
|
|
20
|
-
* 1. **The key never reaches the DOM.** No payload from main carries
|
|
21
|
-
* `secret`, and nothing here invents a field that might. What gets painted
|
|
22
|
-
* is `status.keyFingerprint` — the derived 12-hex tag — which is public by
|
|
23
|
-
* construction (client/authorship-key.js prints exactly this).
|
|
24
|
-
* 2. **A refusal is shown, not swallowed.** Every handler resolves
|
|
25
|
-
* `{ ok: false, error }` instead of throwing, so the panel prints the
|
|
26
|
-
* reason ("this key file is readable by other users", "rclone is not
|
|
27
|
-
* installed") rather than an empty state that reads like success.
|
|
28
|
-
*
|
|
29
|
-
* Pure formatting (`keyLine`, `anchorVerdict`, `auditVerdict`, …) is kept
|
|
30
|
-
* separate from painting so test/renderer-fingerprint.test.mjs can assert the
|
|
31
|
-
* strings that carry the three-claim anchor verdict without standing up a DOM —
|
|
32
|
-
* and the functions it asserts are the exact ones that paint.
|
|
33
|
-
*
|
|
34
|
-
* Classic script, no imports: loaded before app.js by index.html and called
|
|
35
|
-
* from init(). It does not auto-mount, so requiring it under test has no side
|
|
36
|
-
* effects.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
(function (root, factory) {
|
|
40
|
-
const api = factory();
|
|
41
|
-
// `module` first: under Node the test requires this file directly, and the
|
|
42
|
-
// renderer's `window` is not there.
|
|
43
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
44
|
-
if (root) root.aegisFingerprint = api;
|
|
45
|
-
})(typeof window !== 'undefined' ? window : null, function () {
|
|
46
|
-
/** Every id this file reads. The test asserts each one exists in index.html —
|
|
47
|
-
* a typo here is otherwise a silent no-op, which is how a panel ships with
|
|
48
|
-
* one dead button and no error. */
|
|
49
|
-
const IDS = {
|
|
50
|
-
cardStatus: 'fp-card-status',
|
|
51
|
-
cardOpen: 'fp-open',
|
|
52
|
-
overlay: 'fp-overlay',
|
|
53
|
-
backdrop: 'fp-backdrop',
|
|
54
|
-
close: 'fp-close',
|
|
55
|
-
keySource: 'fp-key-source',
|
|
56
|
-
keyPath: 'fp-key-path',
|
|
57
|
-
keyTag: 'fp-key-tag',
|
|
58
|
-
keyWarning: 'fp-key-warning',
|
|
59
|
-
vaultTarget: 'fp-vault-target',
|
|
60
|
-
vaultBackendDrive: 'fp-vault-backend-drive',
|
|
61
|
-
vaultBackendCloud: 'fp-vault-backend-aegiscloud',
|
|
62
|
-
vaultBackendOff: 'fp-vault-backend-off',
|
|
63
|
-
label: 'fp-label',
|
|
64
|
-
publishOnMint: 'fp-publish-on-mint',
|
|
65
|
-
autoSeal: 'fp-auto-seal',
|
|
66
|
-
autoSealLabel: 'fp-auto-seal-label',
|
|
67
|
-
autoHint: 'fp-auto-hint',
|
|
68
|
-
sealNow: 'fp-seal-now',
|
|
69
|
-
keygen: 'fp-keygen',
|
|
70
|
-
seal: 'fp-seal',
|
|
71
|
-
sealCard: 'fp-seal-card',
|
|
72
|
-
verify: 'fp-verify',
|
|
73
|
-
publish: 'fp-publish',
|
|
74
|
-
anchor: 'fp-anchor',
|
|
75
|
-
backupTo: 'fp-backup-to',
|
|
76
|
-
backup: 'fp-backup',
|
|
77
|
-
vault: 'fp-vault',
|
|
78
|
-
audit: 'fp-audit',
|
|
79
|
-
vaultList: 'fp-vault-list',
|
|
80
|
-
out: 'fp-out',
|
|
81
|
-
driveStatus: 'fp-drive-status',
|
|
82
|
-
driveSignIn: 'fp-drive-signin',
|
|
83
|
-
driveConnect: 'fp-drive-connect',
|
|
84
|
-
driveSyncNow: 'fp-drive-sync',
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// ------------------------------------------------------------- formatting
|
|
88
|
-
|
|
89
|
-
const dash = (v) => (v === null || v === undefined || v === '' ? '–' : String(v));
|
|
90
|
-
|
|
91
|
-
/** The one-line state that goes in the sidebar card, where there is room for
|
|
92
|
-
* a sentence and not a table. */
|
|
93
|
-
function keyLine(status) {
|
|
94
|
-
if (!status) return 'checking…';
|
|
95
|
-
if (status.ok === false) return status.error || 'unavailable';
|
|
96
|
-
if (!status.configured) return 'no key — mint one to seal records';
|
|
97
|
-
return `key ${dash(status.keyFingerprint)} · ${dash(status.keySource)}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Everything the panel can say about the key without saying the key. */
|
|
101
|
-
function statusRows(status) {
|
|
102
|
-
if (!status || status.ok === false) {
|
|
103
|
-
return [{ k: 'state', v: (status && status.error) || 'unavailable' }];
|
|
104
|
-
}
|
|
105
|
-
const rows = [
|
|
106
|
-
{ k: 'key', v: status.configured ? status.keyFingerprint : 'none' },
|
|
107
|
-
{ k: 'source', v: dash(status.keySource) },
|
|
108
|
-
{ k: 'expected at', v: dash(status.defaultKeyPath) },
|
|
109
|
-
{ k: 'history', v: status.historyPresent ? status.historyPath : 'none yet' },
|
|
110
|
-
];
|
|
111
|
-
// Only when it resolves somewhere else than expected is it worth a row: a
|
|
112
|
-
// key coming from AEGIS_AUTHORSHIP_SECRET or --secret is exactly the
|
|
113
|
-
// situation where "which key am I actually signing with" needs an answer.
|
|
114
|
-
if (status.configured && status.keyPath && status.keyPath !== status.defaultKeyPath) {
|
|
115
|
-
rows.splice(2, 0, { k: 'in use', v: status.keyPath });
|
|
116
|
-
}
|
|
117
|
-
return rows;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** The 0644 refusal is the one failure where the fix is a command the user
|
|
121
|
-
* can run, so it is surfaced as a warning line rather than buried in the
|
|
122
|
-
* result pane. */
|
|
123
|
-
function keyWarning(status) {
|
|
124
|
-
const err = status && status.error ? String(status.error) : '';
|
|
125
|
-
return /chmod/i.test(err) ? err : '';
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** One line per vault entry. Tolerant of the key the shared module chooses,
|
|
129
|
-
* because the panel's job here is to show what came back, not to insist. */
|
|
130
|
-
function vaultRows(list) {
|
|
131
|
-
if (!list || list.ok === false) {
|
|
132
|
-
return [{ name: 'vault unavailable', detail: (list && list.error) || 'unknown error' }];
|
|
133
|
-
}
|
|
134
|
-
const items = Array.isArray(list.records) ? list.records
|
|
135
|
-
: Array.isArray(list.items) ? list.items
|
|
136
|
-
: Array.isArray(list.entries) ? list.entries
|
|
137
|
-
: [];
|
|
138
|
-
if (!items.length) return [{ name: 'nothing published yet', detail: '' }];
|
|
139
|
-
return items.map((it) => {
|
|
140
|
-
const name = it.name || it.file || it.path || it.id || 'record';
|
|
141
|
-
const parts = [it.id, it.publishedAt || it.createdAt, it.url].filter(Boolean);
|
|
142
|
-
return { name: String(name), detail: parts.join(' · ') };
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* The one-line summary of the Google Drive card — `desktop/lib/
|
|
148
|
-
* google-connect.js`'s `status()` shape, `{signedIn, connected, email}`.
|
|
149
|
-
*
|
|
150
|
-
* Three distinct states, because "not connected" means something different
|
|
151
|
-
* depending on whether this machine has signed in with Google at all:
|
|
152
|
-
* connecting Drive before signing in attaches the tokens to a different
|
|
153
|
-
* account than the one this machine polls (see google-connect.js's header
|
|
154
|
-
* comment), so the line has to say WHICH step is next, not just "no".
|
|
155
|
-
*/
|
|
156
|
-
function driveLine(status) {
|
|
157
|
-
if (!status || status.ok === false) return (status && status.error) || 'unavailable';
|
|
158
|
-
if (!status.signedIn) return 'not signed in — sign in with Google first';
|
|
159
|
-
if (!status.connected) return 'signed in · Google Drive not connected yet';
|
|
160
|
-
return `Google Drive connected${status.email ? ` · ${status.email}` : ''}`;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** The anchor answer, kept as three separate claims on purpose: "the file is
|
|
164
|
-
* gone", "the file was replaced" and "the key that sealed it has moved on"
|
|
165
|
-
* are three different situations, and one boolean hides the interesting one
|
|
166
|
-
* (client/record-vault.js checkAnchor). `present` is read as well as
|
|
167
|
-
* `presence` because that spelling was a real bug once. */
|
|
168
|
-
function anchorVerdict(a) {
|
|
169
|
-
if (!a || a.ok === false) {
|
|
170
|
-
return { ok: false, label: (a && a.error) || 'anchor check failed', claims: [] };
|
|
171
|
-
}
|
|
172
|
-
const present = a.present !== undefined ? a.present : a.presence;
|
|
173
|
-
const claims = [
|
|
174
|
-
{ k: 'present', v: present },
|
|
175
|
-
{ k: 'unchanged', v: a.unchanged },
|
|
176
|
-
{ k: 'attested', v: a.attested },
|
|
177
|
-
];
|
|
178
|
-
const ok = present === true && a.unchanged === true && a.attested === true;
|
|
179
|
-
return {
|
|
180
|
-
ok,
|
|
181
|
-
// Ambiguity is not success: an undefined claim is reported as unknown,
|
|
182
|
-
// never folded into a yes.
|
|
183
|
-
label: ok ? 'the published copy is this record' : 'the published copy is not verifiably this record',
|
|
184
|
-
claims,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** What the audit found. A finding is a fact about the vault, not an error
|
|
189
|
-
* from it, so a clean run says so explicitly rather than showing nothing. */
|
|
190
|
-
function auditVerdict(a) {
|
|
191
|
-
if (!a || a.ok === false) return { ok: false, label: (a && a.error) || 'audit failed', findings: [] };
|
|
192
|
-
const findings = Array.isArray(a.findings) ? a.findings
|
|
193
|
-
: Array.isArray(a.leaks) ? a.leaks
|
|
194
|
-
: Array.isArray(a.hits) ? a.hits
|
|
195
|
-
: [];
|
|
196
|
-
if (findings.length) {
|
|
197
|
-
return { ok: false, label: `${findings.length} item(s) in the vault look like key material`, findings };
|
|
198
|
-
}
|
|
199
|
-
return { ok: true, label: 'no key material in the vault', findings: [] };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** A minted record as the panel reports it. The local file is the evidence —
|
|
203
|
-
* a failed publish does not unseal it (desktop/lib/fingerprint.js mint) —
|
|
204
|
-
* so an upload failure is said beside the seal, not instead of it. */
|
|
205
|
-
function sealLine(r) {
|
|
206
|
-
if (!r || r.ok === false) return { ok: false, text: (r && r.error) || 'seal failed' };
|
|
207
|
-
const rec = r.record || {};
|
|
208
|
-
const bits = [`sealed ${dash(rec.id)}`, `key ${dash(rec.keyFingerprint)}`, r.out];
|
|
209
|
-
// Both halves of the session, in the record and in the line: the key that
|
|
210
|
-
// sealed it, and the mind that answered. An un-nominated AI credential reads
|
|
211
|
-
// as "ai unobserved", never as a borrowed key.
|
|
212
|
-
bits.push(
|
|
213
|
-
rec.aiKey
|
|
214
|
-
? `ai ${dash(rec.ai)} on ${dash(rec.aiKey)}`
|
|
215
|
-
: `ai unobserved${rec.ai ? ` (${dash(rec.ai)})` : ''}`,
|
|
216
|
-
);
|
|
217
|
-
if (r.vault && r.vault.url) bits.push(`published ${r.vault.url}`);
|
|
218
|
-
else if (r.vault) bits.push(`published ${dash(r.vault.remote || r.vault.path)}`);
|
|
219
|
-
if (r.vaultError) bits.push(`publish failed: ${r.vaultError}`);
|
|
220
|
-
return { ok: true, text: bits.filter(Boolean).join(' · '), file: r.out || null, record: r.record || null };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ---------------------------------------------------------------- painting
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* One line describing the auto-seal state, in the same voice as the rest of
|
|
227
|
-
* the panel: what is armed, what it has filed, and the last thing that went
|
|
228
|
-
* wrong. Pure, so the test asserts the string the panel paints.
|
|
229
|
-
*/
|
|
230
|
-
function autoMintLine(s) {
|
|
231
|
-
if (!s || typeof s !== 'object') return '–';
|
|
232
|
-
if (s.enabled !== true) return 'off — sealing stays a button';
|
|
233
|
-
const bits = [];
|
|
234
|
-
const secs = Number(s.quietMs);
|
|
235
|
-
bits.push(Number.isFinite(secs) ? `seals ${Math.round(secs / 1000)}s after the last turn` : 'seals when the session goes quiet');
|
|
236
|
-
if (s.publish) bits.push('publishes to the vault');
|
|
237
|
-
if (s.pendingTurns) bits.push(`${s.pendingTurns} turn${s.pendingTurns === 1 ? '' : 's'} pending`);
|
|
238
|
-
if (s.seals) bits.push(`${s.seals} sealed`);
|
|
239
|
-
if (s.failures) bits.push(`${s.failures} failed`);
|
|
240
|
-
if (s.lastError) bits.push(`last error: ${s.lastError}`);
|
|
241
|
-
return bits.join(' · ');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function mount({ api, doc, auto } = {}) {
|
|
245
|
-
const d = doc || (typeof document !== 'undefined' ? document : null);
|
|
246
|
-
if (!d) throw new Error('fingerprint: mount needs a document');
|
|
247
|
-
const bridge = api || (typeof window !== 'undefined' ? window.aegis : null);
|
|
248
|
-
// The automation bridge (main.js registerAutoIpc). Injectable for the same
|
|
249
|
-
// reason `api` is, and optional: a preload predating that surface leaves the
|
|
250
|
-
// auto-seal control inert rather than throwing at mount.
|
|
251
|
-
const autoBridge = auto || (typeof window !== 'undefined' ? window.auto : null);
|
|
252
|
-
|
|
253
|
-
const el = (id) => d.getElementById(id);
|
|
254
|
-
const state = { status: null, last: null, busy: false, mounted: true };
|
|
255
|
-
|
|
256
|
-
function say(text, bad) {
|
|
257
|
-
const out = el(IDS.out);
|
|
258
|
-
if (!out) return;
|
|
259
|
-
out.textContent = text;
|
|
260
|
-
out.className = bad ? 'fp-out fp-bad' : 'fp-out';
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Every bridge call goes through here. A handler that rejects (Electron
|
|
264
|
-
* turns a thrown main-process error into a rejected invoke) must not leave
|
|
265
|
-
* the panel mid-action, so the catch produces the same `{ ok, error }`
|
|
266
|
-
* shape the handlers themselves do. */
|
|
267
|
-
async function call(name, payload) {
|
|
268
|
-
if (!bridge || typeof bridge[name] !== 'function') {
|
|
269
|
-
return { ok: false, error: `this build has no ${name} bridge` };
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
const r = await bridge[name](payload);
|
|
273
|
-
if (!r || typeof r !== 'object') return { ok: false, error: `${name} returned nothing` };
|
|
274
|
-
return r;
|
|
275
|
-
} catch (e) {
|
|
276
|
-
return { ok: false, error: (e && e.message) || String(e) };
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function paintStatus() {
|
|
281
|
-
const s = state.status;
|
|
282
|
-
const card = el(IDS.cardStatus);
|
|
283
|
-
if (card) card.textContent = keyLine(s);
|
|
284
|
-
const src = el(IDS.keySource);
|
|
285
|
-
const pth = el(IDS.keyPath);
|
|
286
|
-
const tag = el(IDS.keyTag);
|
|
287
|
-
const warn = el(IDS.keyWarning);
|
|
288
|
-
const vt = el(IDS.vaultTarget);
|
|
289
|
-
const rows = statusRows(s);
|
|
290
|
-
const pick = (k) => {
|
|
291
|
-
const row = rows.find((r) => r.k === k);
|
|
292
|
-
return row ? row.v : '–';
|
|
293
|
-
};
|
|
294
|
-
if (src) src.textContent = pick('source');
|
|
295
|
-
if (pth) pth.textContent = s && s.keyPath ? s.keyPath : pick('expected at');
|
|
296
|
-
if (tag) tag.textContent = s && s.configured ? dash(s.keyFingerprint) : 'none';
|
|
297
|
-
if (warn) {
|
|
298
|
-
const w = keyWarning(s);
|
|
299
|
-
warn.textContent = w;
|
|
300
|
-
warn.hidden = !w;
|
|
301
|
-
}
|
|
302
|
-
if (vt && s && s.vault) {
|
|
303
|
-
// The default backend is the account (`aegiscloud`), where "gdrive" is
|
|
304
|
-
// not where anything goes — printing `gdrive:AEGIS-AGPL/records` for a
|
|
305
|
-
// cloud publish would name a folder the record never touches. Say the
|
|
306
|
-
// backend for that case, and keep the remembered rclone target for the
|
|
307
|
-
// case where it IS a remote. 'off' names neither — nothing is resolved
|
|
308
|
-
// behind it (desktop/lib/fingerprint.js's status() short-circuits
|
|
309
|
-
// before ever calling vaultTarget()), so the line says so plainly
|
|
310
|
-
// rather than printing the leftover "–:–" two dashes would read as.
|
|
311
|
-
vt.textContent = s.vault.backend === 'off'
|
|
312
|
-
? 'nothing — sync is off'
|
|
313
|
-
: s.vault.backend === 'aegiscloud'
|
|
314
|
-
? `aegiscloud:${dash(s.vault.recordsDir)}`
|
|
315
|
-
: `${dash(s.vault.remote)}:${dash(s.vault.recordsDir || s.vault.root)}`;
|
|
316
|
-
} else if (vt) {
|
|
317
|
-
vt.textContent = '–';
|
|
318
|
-
}
|
|
319
|
-
// A key that is present makes keygen a REPLACE, which orphans every
|
|
320
|
-
// record sealed with the old one. Saying that on the button is the whole
|
|
321
|
-
// warning; the confirm below is the second line of defence.
|
|
322
|
-
const kg = el(IDS.keygen);
|
|
323
|
-
if (kg) kg.textContent = s && s.configured ? 'Replace key…' : 'Mint key';
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* The three backend buttons' pressed state.
|
|
328
|
-
*
|
|
329
|
-
* Deliberately read from `getVaultBackend` (the preference the user set),
|
|
330
|
-
* NOT from `status().vault.backend` (the resolved record-vault.js
|
|
331
|
-
* backend id): the preference vocabulary is 'aegiscloud' | 'drive' | 'off',
|
|
332
|
-
* the resolved one is 'aegiscloud' | 'rclone' | 'off' — a 'drive'
|
|
333
|
-
* preference resolves to 'rclone', so pressing "Google Drive" from status
|
|
334
|
-
* alone would never read back as pressed.
|
|
335
|
-
*/
|
|
336
|
-
function paintVaultBackend(backend) {
|
|
337
|
-
const map = {
|
|
338
|
-
drive: el(IDS.vaultBackendDrive),
|
|
339
|
-
aegiscloud: el(IDS.vaultBackendCloud),
|
|
340
|
-
off: el(IDS.vaultBackendOff),
|
|
341
|
-
};
|
|
342
|
-
for (const [key, btn] of Object.entries(map)) {
|
|
343
|
-
if (btn) btn.setAttribute('aria-pressed', String(key === backend));
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
async function loadVaultBackend() {
|
|
348
|
-
const r = await call('getVaultBackend');
|
|
349
|
-
state.vaultBackend = r && r.backend ? r.backend : 'aegiscloud';
|
|
350
|
-
paintVaultBackend(state.vaultBackend);
|
|
351
|
-
return state.vaultBackend;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const doSetVaultBackend = (backend) =>
|
|
355
|
-
action(`vault: ${backend === 'drive' ? 'Google Drive' : backend === 'off' ? 'no sync' : 'AE'}`, async () => {
|
|
356
|
-
const r = await call('setVaultBackend', backend);
|
|
357
|
-
return r && r.backend ? { ok: true, ...r } : { ok: false, error: 'could not save that' };
|
|
358
|
-
}, {
|
|
359
|
-
// The "publishes to" line names the backend too, so both it and the
|
|
360
|
-
// three buttons must repaint the moment the preference changes — not
|
|
361
|
-
// just on the next open().
|
|
362
|
-
after: (r) => (r && r.ok ? refresh() : null),
|
|
363
|
-
say: (r) => (r.ok ? `vault backend set to ${dash(r.backend)}` : `could not save that: ${r.error}`),
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Google Drive — memory backup (desktop/lib/google-connect.js, exposed
|
|
368
|
-
* over `aegis.driveSignIn`/`driveConnect`/`driveStatus`/`driveSyncMemory`).
|
|
369
|
-
*
|
|
370
|
-
* Distinct from the vault backend buttons above: those choose where a
|
|
371
|
-
* SEALED φ(α) RECORD is published (rclone or the account vault). This is
|
|
372
|
-
* the account's AEGIS CLOUD MEMORY, backed up to the user's own Drive
|
|
373
|
-
* through real Google OAuth — unrelated data, unrelated credential.
|
|
374
|
-
*/
|
|
375
|
-
function paintDrive(status) {
|
|
376
|
-
const line = el(IDS.driveStatus);
|
|
377
|
-
if (line) line.textContent = driveLine(status);
|
|
378
|
-
const connectBtn = el(IDS.driveConnect);
|
|
379
|
-
if (connectBtn) connectBtn.disabled = Boolean(status && !status.signedIn);
|
|
380
|
-
const syncBtn = el(IDS.driveSyncNow);
|
|
381
|
-
if (syncBtn) syncBtn.disabled = Boolean(!status || !status.connected);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
async function loadDriveStatus() {
|
|
385
|
-
const r = await call('driveStatus');
|
|
386
|
-
state.drive = r;
|
|
387
|
-
paintDrive(r);
|
|
388
|
-
return r;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const doDriveSignIn = () =>
|
|
392
|
-
action('sign in with Google', () => call('driveSignIn'), {
|
|
393
|
-
after: (r) => (r && r.ok ? loadDriveStatus() : null),
|
|
394
|
-
say: (r) => (r.ok ? `signed in${r.username ? ` as ${r.username}` : ''}` : `sign-in failed: ${r.error}`),
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const doDriveConnect = () =>
|
|
398
|
-
action('connect Google Drive', () => call('driveConnect'), {
|
|
399
|
-
after: (r) => (r && r.ok ? loadDriveStatus() : null),
|
|
400
|
-
say: (r) => (r.ok ? `Google Drive connected${r.email ? ` · ${r.email}` : ''}` : `could not connect: ${r.error}`),
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const doDriveSyncNow = () =>
|
|
404
|
-
action('back up memory to Drive', () => call('driveSyncMemory'), {
|
|
405
|
-
say: (r) => (r.ok
|
|
406
|
-
? `backed up ${r.count} memory entr${r.count === 1 ? 'y' : 'ies'}${r.webViewLink ? ` → ${r.webViewLink}` : ''}`
|
|
407
|
-
: `backup failed: ${r.error}`),
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
function paintVault(list) {
|
|
411
|
-
const ul = el(IDS.vaultList);
|
|
412
|
-
if (!ul) return;
|
|
413
|
-
ul.textContent = '';
|
|
414
|
-
for (const row of vaultRows(list)) {
|
|
415
|
-
const li = d.createElement('li');
|
|
416
|
-
li.className = 'fp-vault-row';
|
|
417
|
-
const name = d.createElement('span');
|
|
418
|
-
name.className = 'fp-vault-name';
|
|
419
|
-
name.textContent = row.name;
|
|
420
|
-
li.appendChild(name);
|
|
421
|
-
if (row.detail) {
|
|
422
|
-
const det = d.createElement('span');
|
|
423
|
-
det.className = 'fp-vault-detail';
|
|
424
|
-
det.textContent = row.detail;
|
|
425
|
-
li.appendChild(det);
|
|
426
|
-
}
|
|
427
|
-
ul.appendChild(li);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
async function refresh() {
|
|
432
|
-
state.status = await call('fingerprintStatus');
|
|
433
|
-
paintStatus();
|
|
434
|
-
await loadAutoMint();
|
|
435
|
-
await loadVaultBackend();
|
|
436
|
-
await loadDriveStatus();
|
|
437
|
-
return state.status;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* The auto-seal control (settings.js `__autoMint`, through lib/auto.js).
|
|
442
|
-
*
|
|
443
|
-
* Read-only paint from MAIN, for the reason the rest of this panel is: the
|
|
444
|
-
* renderer must never be the place that decides a record gets filed, and a
|
|
445
|
-
* checkbox state this file invented would disagree with the timer the moment
|
|
446
|
-
* anything else (the queue card's window, a restart) changed the setting.
|
|
447
|
-
*/
|
|
448
|
-
function renderAutoMint(s) {
|
|
449
|
-
if (!s || typeof s !== 'object') return;
|
|
450
|
-
const sw = el(IDS.autoSeal);
|
|
451
|
-
if (sw) sw.checked = s.enabled === true;
|
|
452
|
-
const label = el(IDS.autoSealLabel);
|
|
453
|
-
if (label) label.textContent = s.enabled ? 'sealing automatically when quiet' : 'seal automatically when quiet';
|
|
454
|
-
const hint = el(IDS.autoHint);
|
|
455
|
-
if (!hint) return;
|
|
456
|
-
hint.textContent = autoMintLine(s);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function loadAutoMint() {
|
|
460
|
-
if (!autoBridge || typeof autoBridge.status !== 'function') {
|
|
461
|
-
const hint = el(IDS.autoHint);
|
|
462
|
-
// Said plainly rather than left blank: an inert toggle with no
|
|
463
|
-
// explanation is the failure mode this whole panel exists to avoid.
|
|
464
|
-
if (hint) hint.textContent = 'this build has no automation bridge — sealing stays a button';
|
|
465
|
-
return null;
|
|
466
|
-
}
|
|
467
|
-
try {
|
|
468
|
-
const r = await autoBridge.status();
|
|
469
|
-
if (r && r.mint) renderAutoMint(r.mint);
|
|
470
|
-
return r;
|
|
471
|
-
} catch (e) {
|
|
472
|
-
const hint = el(IDS.autoHint);
|
|
473
|
-
if (hint) hint.textContent = `automation status unavailable: ${(e && e.message) || String(e)}`;
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/** Persist the switch through main and paint main's answer. */
|
|
479
|
-
async function setAutoMint(enabled) {
|
|
480
|
-
if (!autoBridge || typeof autoBridge.setMint !== 'function') return null;
|
|
481
|
-
const publish = Boolean(el(IDS.publishOnMint) && el(IDS.publishOnMint).checked);
|
|
482
|
-
try {
|
|
483
|
-
const r = await autoBridge.setMint({ enabled: Boolean(enabled), publish });
|
|
484
|
-
if (r && r.mint) renderAutoMint(r.mint);
|
|
485
|
-
else if (r && r.ok === false) say(r.error || 'could not save that', true);
|
|
486
|
-
return r;
|
|
487
|
-
} catch (e) {
|
|
488
|
-
say(`could not save that: ${(e && e.message) || String(e)}`, true);
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/** Seal right now, through the automation rather than the raw mint: the
|
|
494
|
-
* same call resets the pending-turn counter, so the quiet timer will not
|
|
495
|
-
* file a second identical record for this session. */
|
|
496
|
-
async function doSealNow() {
|
|
497
|
-
const publish = Boolean(el(IDS.publishOnMint) && el(IDS.publishOnMint).checked);
|
|
498
|
-
return action('seal now', async () => {
|
|
499
|
-
if (!autoBridge || typeof autoBridge.sealNow !== 'function') {
|
|
500
|
-
return { ok: false, error: 'this build has no automation bridge' };
|
|
501
|
-
}
|
|
502
|
-
return autoBridge.sealNow(publish);
|
|
503
|
-
}, {
|
|
504
|
-
paint: (r) => { if (r && r.ok && r.out) state.last = { file: r.out, record: r.record || null }; },
|
|
505
|
-
say: (r) => {
|
|
506
|
-
if (!r) return 'seal now: nothing happened';
|
|
507
|
-
if (r.ok === false) return `seal failed: ${r.error || 'unknown'}`;
|
|
508
|
-
if (r.type === 'skip') return `nothing to seal (${r.reason || 'no new turns'})`;
|
|
509
|
-
const line = sealLine(r);
|
|
510
|
-
return line.ok ? `sealed ${dash((r.record || {}).id)} → ${dash(r.out)}` : `seal failed: ${line.text}`;
|
|
511
|
-
},
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
/** Run one action, repaint what it changed, and say what it did. `confirm`
|
|
516
|
-
* is injectable so the harness can answer it deterministically. */
|
|
517
|
-
async function action(label, fn, opts) {
|
|
518
|
-
const o = opts || {};
|
|
519
|
-
if (state.busy) return null;
|
|
520
|
-
state.busy = true;
|
|
521
|
-
say(`${label}…`, false);
|
|
522
|
-
try {
|
|
523
|
-
const r = await fn();
|
|
524
|
-
if (o.paint) o.paint(r);
|
|
525
|
-
if (o.after) await o.after(r);
|
|
526
|
-
say(o.say ? o.say(r) : `${label}: ${r && r.ok === false ? r.error : 'done'}`, Boolean(r && r.ok === false));
|
|
527
|
-
return r;
|
|
528
|
-
} finally {
|
|
529
|
-
state.busy = false;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const doKeygen = () =>
|
|
534
|
-
action('mint key', () => call('fingerprintKeygen', {
|
|
535
|
-
force: Boolean(state.status && state.status.configured),
|
|
536
|
-
}), {
|
|
537
|
-
// Only after a successful mint: status moved from "no key" to a key,
|
|
538
|
-
// and repainting is what turns "Mint key" into "Replace key…".
|
|
539
|
-
after: refresh,
|
|
540
|
-
say: (r) => (r.ok
|
|
541
|
-
? `minted ${dash(r.keyFingerprint)} at ${dash(r.path)}`
|
|
542
|
-
: `mint failed: ${r.error}`),
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
const doSeal = () =>
|
|
546
|
-
action('seal session', () => call('fingerprintMint', {
|
|
547
|
-
label: (el(IDS.label) && el(IDS.label).value.trim()) || null,
|
|
548
|
-
publish: Boolean(el(IDS.publishOnMint) && el(IDS.publishOnMint).checked),
|
|
549
|
-
}), {
|
|
550
|
-
paint: (r) => { if (r.ok) state.last = { file: r.out, record: r.record }; },
|
|
551
|
-
say: (r) => {
|
|
552
|
-
const line = sealLine(r);
|
|
553
|
-
return line.ok ? `sealed ${dash((r.record || {}).id)} → ${dash(r.out)}` : `seal failed: ${line.text}`;
|
|
554
|
-
},
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
/** Verify/publish/anchor act on the record this panel sealed. A path is
|
|
558
|
-
* preferred over the in-memory record because the sha256 in the anchor is
|
|
559
|
-
* then about the bytes on disk rather than a re-serialisation of them
|
|
560
|
-
* (desktop/lib/fingerprint.js publish makes the same argument) — so `file`
|
|
561
|
-
* wins when there is one, and the record is the fallback for a host where
|
|
562
|
-
* the write failed but the seal did not. */
|
|
563
|
-
const doVerify = () =>
|
|
564
|
-
action('verify', async () => {
|
|
565
|
-
if (state.last && state.last.file) return call('fingerprintVerify', { file: state.last.file });
|
|
566
|
-
if (state.last && state.last.record) return call('fingerprintVerify', { record: state.last.record });
|
|
567
|
-
return { ok: false, error: 'nothing sealed in this window yet — seal a session first' };
|
|
568
|
-
}, {
|
|
569
|
-
say: (r) => (r.ok ? `verified ${dash(r.id)} · key ${dash(r.keySource)}` : `not verified: ${r.error || r.reason}`),
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
const doPublish = () =>
|
|
573
|
-
action('publish', () => {
|
|
574
|
-
if (state.last && state.last.file) return call('fingerprintPublish', { file: state.last.file });
|
|
575
|
-
if (state.last && state.last.record) return call('fingerprintPublish', { record: state.last.record });
|
|
576
|
-
return { ok: false, error: 'nothing sealed in this window yet — seal a session first' };
|
|
577
|
-
}, {
|
|
578
|
-
say: (r) => (r.ok
|
|
579
|
-
? `published ${dash(r.url || r.path)}${r.sha256 ? ` · sha256 ${String(r.sha256).slice(0, 12)}` : ''}`
|
|
580
|
-
: `publish failed: ${r.error}`),
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
const doAnchor = () =>
|
|
584
|
-
action('check anchor', () => {
|
|
585
|
-
if (state.last && state.last.file) return call('fingerprintAnchor', { file: state.last.file });
|
|
586
|
-
return call('fingerprintAnchor', {});
|
|
587
|
-
}, {
|
|
588
|
-
say: (r) => {
|
|
589
|
-
const v = anchorVerdict(r);
|
|
590
|
-
const claims = v.claims.map((c) => `${c.k} ${c.v === true ? 'yes' : c.v === false ? 'no' : 'unknown'}`).join(' · ');
|
|
591
|
-
return `${v.label}${claims ? ` — ${claims}` : ''}`;
|
|
592
|
-
},
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
const doBackup = () =>
|
|
596
|
-
action('back up key', () => {
|
|
597
|
-
const to = el(IDS.backupTo) && el(IDS.backupTo).value.trim();
|
|
598
|
-
if (!to) return { ok: false, error: 'backup needs a destination directory' };
|
|
599
|
-
return call('fingerprintBackup', { to });
|
|
600
|
-
}, {
|
|
601
|
-
say: (r) => (r.ok ? `key copied to ${dash(r.to || r.path)}` : `backup refused: ${r.error}`),
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
const doVault = () =>
|
|
605
|
-
action('list vault', () => call('fingerprintVault'), {
|
|
606
|
-
paint: paintVault,
|
|
607
|
-
say: (r) => (r.ok ? `vault listed — ${vaultRows(r).length} entry(ies)` : `vault unavailable: ${r.error}`),
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
const doAudit = () =>
|
|
611
|
-
action('audit vault', () => call('fingerprintVaultAudit'), {
|
|
612
|
-
say: (r) => {
|
|
613
|
-
const v = auditVerdict(r);
|
|
614
|
-
const names = v.findings.map((f) => (f && (f.path || f.name)) || String(f)).slice(0, 3);
|
|
615
|
-
return `${v.label}${names.length ? ` — ${names.join(', ')}` : ''}`;
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
const isOpen = () => {
|
|
620
|
-
const o = el(IDS.overlay);
|
|
621
|
-
return Boolean(o) && !o.hidden;
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
async function open() {
|
|
625
|
-
const o = el(IDS.overlay);
|
|
626
|
-
if (!o) return;
|
|
627
|
-
o.hidden = false;
|
|
628
|
-
// Body class so the page behind cannot scroll under the sheet, matching
|
|
629
|
-
// the memory inspector's `.memory-open` rule.
|
|
630
|
-
if (d.body && d.body.classList) d.body.classList.add('fp-open');
|
|
631
|
-
await refresh();
|
|
632
|
-
await doVault();
|
|
633
|
-
const f = el(IDS.close);
|
|
634
|
-
if (f) f.focus();
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function close() {
|
|
638
|
-
const o = el(IDS.overlay);
|
|
639
|
-
if (!o) return;
|
|
640
|
-
o.hidden = true;
|
|
641
|
-
if (d.body && d.body.classList) d.body.classList.remove('fp-open');
|
|
642
|
-
const btn = el(IDS.cardOpen);
|
|
643
|
-
if (btn) btn.focus();
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// ------------------------------------------------------------- listeners
|
|
647
|
-
// Bound here rather than in app.js so the panel owns its own markup: every
|
|
648
|
-
// id in IDS is read by this file only, and the test asserts they all exist.
|
|
649
|
-
const bind = (id, type, fn) => {
|
|
650
|
-
const node = el(id);
|
|
651
|
-
if (node) node.addEventListener(type, fn);
|
|
652
|
-
};
|
|
653
|
-
|
|
654
|
-
bind(IDS.cardOpen, 'click', open);
|
|
655
|
-
bind(IDS.close, 'click', close);
|
|
656
|
-
bind(IDS.backdrop, 'click', close);
|
|
657
|
-
bind(IDS.keygen, 'click', doKeygen);
|
|
658
|
-
bind(IDS.seal, 'click', doSeal);
|
|
659
|
-
// The sidebar's one-click seal runs the same action; on success it also
|
|
660
|
-
// opens the panel, because a record filed silently behind a closed sheet is
|
|
661
|
-
// indistinguishable from a button that did nothing.
|
|
662
|
-
bind(IDS.sealCard, 'click', async () => {
|
|
663
|
-
const r = await doSeal();
|
|
664
|
-
if (r && r.ok) await open();
|
|
665
|
-
});
|
|
666
|
-
bind(IDS.verify, 'click', doVerify);
|
|
667
|
-
bind(IDS.publish, 'click', doPublish);
|
|
668
|
-
bind(IDS.anchor, 'click', doAnchor);
|
|
669
|
-
bind(IDS.backup, 'click', doBackup);
|
|
670
|
-
bind(IDS.vault, 'click', doVault);
|
|
671
|
-
bind(IDS.audit, 'click', doAudit);
|
|
672
|
-
bind(IDS.vaultBackendDrive, 'click', () => doSetVaultBackend('drive'));
|
|
673
|
-
bind(IDS.vaultBackendCloud, 'click', () => doSetVaultBackend('aegiscloud'));
|
|
674
|
-
bind(IDS.vaultBackendOff, 'click', () => doSetVaultBackend('off'));
|
|
675
|
-
bind(IDS.driveSignIn, 'click', doDriveSignIn);
|
|
676
|
-
bind(IDS.driveConnect, 'click', doDriveConnect);
|
|
677
|
-
bind(IDS.driveSyncNow, 'click', doDriveSyncNow);
|
|
678
|
-
// The auto-seal switch is the ONE control on this panel that can file a
|
|
679
|
-
// record with nobody watching, so it is the one that must repaint from
|
|
680
|
-
// main's answer rather than from the checkbox the user just clicked.
|
|
681
|
-
if (el(IDS.autoSeal)) {
|
|
682
|
-
bind(IDS.autoSeal, 'change', () => {
|
|
683
|
-
const box = el(IDS.autoSeal);
|
|
684
|
-
return setAutoMint(Boolean(box && box.checked));
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
if (el(IDS.sealNow)) bind(IDS.sealNow, 'click', doSealNow);
|
|
688
|
-
|
|
689
|
-
return {
|
|
690
|
-
open,
|
|
691
|
-
close,
|
|
692
|
-
isOpen,
|
|
693
|
-
refresh,
|
|
694
|
-
loadAutoMint,
|
|
695
|
-
state,
|
|
696
|
-
// Exposed for the test and for app.js's on-demand repaint. Every one is
|
|
697
|
-
// idempotent, so calling them from a console is safe.
|
|
698
|
-
actions: {
|
|
699
|
-
doKeygen, doSeal, doSealNow, doVerify, doPublish, doAnchor, doBackup, doVault, doAudit,
|
|
700
|
-
doSetVaultBackend, doDriveSignIn, doDriveConnect, doDriveSyncNow,
|
|
701
|
-
},
|
|
702
|
-
loadVaultBackend,
|
|
703
|
-
loadDriveStatus,
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
return {
|
|
708
|
-
IDS,
|
|
709
|
-
mount,
|
|
710
|
-
// Pure, and asserted as such by the test.
|
|
711
|
-
keyLine,
|
|
712
|
-
statusRows,
|
|
713
|
-
keyWarning,
|
|
714
|
-
vaultRows,
|
|
715
|
-
anchorVerdict,
|
|
716
|
-
auditVerdict,
|
|
717
|
-
sealLine,
|
|
718
|
-
autoMintLine,
|
|
719
|
-
driveLine,
|
|
720
|
-
dash,
|
|
721
|
-
};
|
|
722
|
-
});
|
|
3
|
+
"use strict";!function(e,t){const n=function(){const e={cardStatus:"fp-card-status",cardOpen:"fp-open",overlay:"fp-overlay",backdrop:"fp-backdrop",close:"fp-close",keySource:"fp-key-source",keyPath:"fp-key-path",keyTag:"fp-key-tag",keyWarning:"fp-key-warning",vaultTarget:"fp-vault-target",vaultBackendDrive:"fp-vault-backend-drive",vaultBackendCloud:"fp-vault-backend-aegiscloud",vaultBackendOff:"fp-vault-backend-off",label:"fp-label",publishOnMint:"fp-publish-on-mint",autoSeal:"fp-auto-seal",autoSealLabel:"fp-auto-seal-label",autoHint:"fp-auto-hint",sealNow:"fp-seal-now",keygen:"fp-keygen",seal:"fp-seal",sealCard:"fp-seal-card",verify:"fp-verify",publish:"fp-publish",anchor:"fp-anchor",backupTo:"fp-backup-to",backup:"fp-backup",vault:"fp-vault",audit:"fp-audit",vaultList:"fp-vault-list",out:"fp-out",driveStatus:"fp-drive-status",driveSignIn:"fp-drive-signin",driveConnect:"fp-drive-connect",driveSyncNow:"fp-drive-sync"},t=e=>null==e||""===e?"–":String(e);function n(e){return e?!1===e.ok?e.error||"unavailable":e.configured?`key ${t(e.keyFingerprint)} · ${t(e.keySource)}`:"no key — mint one to seal records":"checking…"}function a(e){if(!e||!1===e.ok)return[{k:"state",v:e&&e.error||"unavailable"}];const n=[{k:"key",v:e.configured?e.keyFingerprint:"none"},{k:"source",v:t(e.keySource)},{k:"expected at",v:t(e.defaultKeyPath)},{k:"history",v:e.historyPresent?e.historyPath:"none yet"}];return e.configured&&e.keyPath&&e.keyPath!==e.defaultKeyPath&&n.splice(2,0,{k:"in use",v:e.keyPath}),n}function o(e){const t=e&&e.error?String(e.error):"";return/chmod/i.test(t)?t:""}function i(e){if(!e||!1===e.ok)return[{name:"vault unavailable",detail:e&&e.error||"unknown error"}];const t=Array.isArray(e.records)?e.records:Array.isArray(e.items)?e.items:Array.isArray(e.entries)?e.entries:[];return t.length?t.map((e=>{const t=e.name||e.file||e.path||e.id||"record",n=[e.id,e.publishedAt||e.createdAt,e.url].filter(Boolean);return{name:String(t),detail:n.join(" · ")}})):[{name:"nothing published yet",detail:""}]}function r(e){return e&&!1!==e.ok?e.signedIn?e.connected?"Google Drive connected"+(e.email?` · ${e.email}`:""):"signed in · Google Drive not connected yet":"not signed in — sign in with Google first":e&&e.error||"unavailable"}function s(e){if(!e||!1===e.ok)return{ok:!1,label:e&&e.error||"anchor check failed",claims:[]};const t=void 0!==e.present?e.present:e.presence,n=[{k:"present",v:t},{k:"unchanged",v:e.unchanged},{k:"attested",v:e.attested}],a=!0===t&&!0===e.unchanged&&!0===e.attested;return{ok:a,label:a?"the published copy is this record":"the published copy is not verifiably this record",claims:n}}function l(e){if(!e||!1===e.ok)return{ok:!1,label:e&&e.error||"audit failed",findings:[]};const t=Array.isArray(e.findings)?e.findings:Array.isArray(e.leaks)?e.leaks:Array.isArray(e.hits)?e.hits:[];return t.length?{ok:!1,label:`${t.length} item(s) in the vault look like key material`,findings:t}:{ok:!0,label:"no key material in the vault",findings:[]}}function u(e){if(!e||!1===e.ok)return{ok:!1,text:e&&e.error||"seal failed"};const n=e.record||{},a=[`sealed ${t(n.id)}`,`key ${t(n.keyFingerprint)}`,e.out];return a.push(n.aiKey?`ai ${t(n.ai)} on ${t(n.aiKey)}`:"ai unobserved"+(n.ai?` (${t(n.ai)})`:"")),e.vault&&e.vault.url?a.push(`published ${e.vault.url}`):e.vault&&a.push(`published ${t(e.vault.remote||e.vault.path)}`),e.vaultError&&a.push(`publish failed: ${e.vaultError}`),{ok:!0,text:a.filter(Boolean).join(" · "),file:e.out||null,record:e.record||null}}function c(e){if(!e||"object"!=typeof e)return"–";if(!0!==e.enabled)return"off — sealing stays a button";const t=[],n=Number(e.quietMs);return t.push(Number.isFinite(n)?`seals ${Math.round(n/1e3)}s after the last turn`:"seals when the session goes quiet"),e.publish&&t.push("publishes to the vault"),e.pendingTurns&&t.push(`${e.pendingTurns} turn${1===e.pendingTurns?"":"s"} pending`),e.seals&&t.push(`${e.seals} sealed`),e.failures&&t.push(`${e.failures} failed`),e.lastError&&t.push(`last error: ${e.lastError}`),t.join(" · ")}return{IDS:e,mount:function({api:d,doc:f,auto:p}={}){const k=f||("undefined"!=typeof document?document:null);if(!k)throw new Error("fingerprint: mount needs a document");const y=d||("undefined"!=typeof window?window.aegis:null),h=p||("undefined"!=typeof window?window.auto:null),v=e=>k.getElementById(e),g={status:null,last:null,busy:!1,mounted:!0};function b(t,n){const a=v(e.out);a&&(a.textContent=t,a.className=n?"fp-out fp-bad":"fp-out")}async function m(e,t){if(!y||"function"!=typeof y[e])return{ok:!1,error:`this build has no ${e} bridge`};try{const n=await y[e](t);return n&&"object"==typeof n?n:{ok:!1,error:`${e} returned nothing`}}catch(e){return{ok:!1,error:e&&e.message||String(e)}}}async function $(){const t=await m("getVaultBackend");return g.vaultBackend=t&&t.backend?t.backend:"aegiscloud",function(t){const n={drive:v(e.vaultBackendDrive),aegiscloud:v(e.vaultBackendCloud),off:v(e.vaultBackendOff)};for(const[e,a]of Object.entries(n))a&&a.setAttribute("aria-pressed",String(e===t))}(g.vaultBackend),g.vaultBackend}const w=e=>L("vault: "+("drive"===e?"Google Drive":"off"===e?"no sync":"AE"),(async()=>{const t=await m("setVaultBackend",e);return t&&t.backend?{ok:!0,...t}:{ok:!1,error:"could not save that"}}),{after:e=>e&&e.ok?N():null,say:e=>e.ok?`vault backend set to ${t(e.backend)}`:`could not save that: ${e.error}`});async function S(){const t=await m("driveStatus");return g.drive=t,function(t){const n=v(e.driveStatus);n&&(n.textContent=r(t));const a=v(e.driveConnect);a&&(a.disabled=Boolean(t&&!t.signedIn));const o=v(e.driveSyncNow);o&&(o.disabled=Boolean(!t||!t.connected))}(t),t}const B=()=>L("sign in with Google",(()=>m("driveSignIn")),{after:e=>e&&e.ok?S():null,say:e=>e.ok?"signed in"+(e.username?` as ${e.username}`:""):`sign-in failed: ${e.error}`}),C=()=>L("connect Google Drive",(()=>m("driveConnect")),{after:e=>e&&e.ok?S():null,say:e=>e.ok?"Google Drive connected"+(e.email?` · ${e.email}`:""):`could not connect: ${e.error}`}),x=()=>L("back up memory to Drive",(()=>m("driveSyncMemory")),{say:e=>e.ok?`backed up ${e.count} memory entr${1===e.count?"y":"ies"}${e.webViewLink?` → ${e.webViewLink}`:""}`:`backup failed: ${e.error}`});function A(t){const n=v(e.vaultList);if(n){n.textContent="";for(const e of i(t)){const t=k.createElement("li");t.className="fp-vault-row";const a=k.createElement("span");if(a.className="fp-vault-name",a.textContent=e.name,t.appendChild(a),e.detail){const n=k.createElement("span");n.className="fp-vault-detail",n.textContent=e.detail,t.appendChild(n)}n.appendChild(t)}}}async function N(){return g.status=await m("fingerprintStatus"),function(){const i=g.status,r=v(e.cardStatus);r&&(r.textContent=n(i));const s=v(e.keySource),l=v(e.keyPath),u=v(e.keyTag),c=v(e.keyWarning),d=v(e.vaultTarget),f=a(i),p=e=>{const t=f.find((t=>t.k===e));return t?t.v:"–"};if(s&&(s.textContent=p("source")),l&&(l.textContent=i&&i.keyPath?i.keyPath:p("expected at")),u&&(u.textContent=i&&i.configured?t(i.keyFingerprint):"none"),c){const e=o(i);c.textContent=e,c.hidden=!e}d&&i&&i.vault?
|
|
4
|
+
// not where anything goes — printing `gdrive:AEGIS-AGPL/records` for a
|
|
5
|
+
d.textContent="off"===i.vault.backend?"nothing — sync is off":"aegiscloud"===i.vault.backend?`aegiscloud:${t(i.vault.recordsDir)}`:`${t(i.vault.remote)}:${t(i.vault.recordsDir||i.vault.root)}`:d&&(d.textContent="–");const k=v(e.keygen);k&&(k.textContent=i&&i.configured?"Replace key…":"Mint key")}(),await M(),await $(),await S(),g.status}function D(t){if(!t||"object"!=typeof t)return;const n=v(e.autoSeal);n&&(n.checked=!0===t.enabled);const a=v(e.autoSealLabel);a&&(a.textContent=t.enabled?"sealing automatically when quiet":"seal automatically when quiet");const o=v(e.autoHint);o&&(o.textContent=c(t))}async function M(){if(!h||"function"!=typeof h.status){const t=v(e.autoHint);return t&&(t.textContent="this build has no automation bridge — sealing stays a button"),null}try{const e=await h.status();return e&&e.mint&&D(e.mint),e}catch(t){const n=v(e.autoHint);return n&&(n.textContent=`automation status unavailable: ${t&&t.message||String(t)}`),null}}async function V(){const n=Boolean(v(e.publishOnMint)&&v(e.publishOnMint).checked);return L("seal now",(async()=>h&&"function"==typeof h.sealNow?h.sealNow(n):{ok:!1,error:"this build has no automation bridge"}),{paint:e=>{e&&e.ok&&e.out&&(g.last={file:e.out,record:e.record||null})},say:e=>{if(!e)return"seal now: nothing happened";if(!1===e.ok)return`seal failed: ${e.error||"unknown"}`;if("skip"===e.type)return`nothing to seal (${e.reason||"no new turns"})`;const n=u(e);return n.ok?`sealed ${t((e.record||{}).id)} → ${t(e.out)}`:`seal failed: ${n.text}`}})}async function L(e,t,n){const a=n||{};if(g.busy)return null;g.busy=!0,b(`${e}…`,!1);try{const n=await t();return a.paint&&a.paint(n),a.after&&await a.after(n),b(a.say?a.say(n):`${e}: ${n&&!1===n.ok?n.error:"done"}`,Boolean(n&&!1===n.ok)),n}finally{g.busy=!1}}const O=()=>L("mint key",(()=>m("fingerprintKeygen",{force:Boolean(g.status&&g.status.configured)})),{after:N,say:e=>e.ok?`minted ${t(e.keyFingerprint)} at ${t(e.path)}`:`mint failed: ${e.error}`}),P=()=>L("seal session",(()=>m("fingerprintMint",{label:v(e.label)&&v(e.label).value.trim()||null,publish:Boolean(v(e.publishOnMint)&&v(e.publishOnMint).checked)})),{paint:e=>{e.ok&&(g.last={file:e.out,record:e.record})},say:e=>{const n=u(e);return n.ok?`sealed ${t((e.record||{}).id)} → ${t(e.out)}`:`seal failed: ${n.text}`}}),E=()=>L("verify",(async()=>g.last&&g.last.file?m("fingerprintVerify",{file:g.last.file}):g.last&&g.last.record?m("fingerprintVerify",{record:g.last.record}):{ok:!1,error:"nothing sealed in this window yet — seal a session first"}),{say:e=>e.ok?`verified ${t(e.id)} · key ${t(e.keySource)}`:`not verified: ${e.error||e.reason}`}),I=()=>L("publish",(()=>g.last&&g.last.file?m("fingerprintPublish",{file:g.last.file}):g.last&&g.last.record?m("fingerprintPublish",{record:g.last.record}):{ok:!1,error:"nothing sealed in this window yet — seal a session first"}),{say:e=>e.ok?`published ${t(e.url||e.path)}${e.sha256?` · sha256 ${String(e.sha256).slice(0,12)}`:""}`:`publish failed: ${e.error}`}),T=()=>L("check anchor",(()=>g.last&&g.last.file?m("fingerprintAnchor",{file:g.last.file}):m("fingerprintAnchor",{})),{say:e=>{const t=s(e),n=t.claims.map((e=>`${e.k} ${!0===e.v?"yes":!1===e.v?"no":"unknown"}`)).join(" · ");return`${t.label}${n?` — ${n}`:""}`}}),j=()=>L("back up key",(()=>{const t=v(e.backupTo)&&v(e.backupTo).value.trim();return t?m("fingerprintBackup",{to:t}):{ok:!1,error:"backup needs a destination directory"}}),{say:e=>e.ok?`key copied to ${t(e.to||e.path)}`:`backup refused: ${e.error}`}),F=()=>L("list vault",(()=>m("fingerprintVault")),{paint:A,say:e=>e.ok?`vault listed — ${i(e).length} entry(ies)`:`vault unavailable: ${e.error}`}),G=()=>L("audit vault",(()=>m("fingerprintVaultAudit")),{say:e=>{const t=l(e),n=t.findings.map((e=>e&&(e.path||e.name)||String(e))).slice(0,3);return`${t.label}${n.length?` — ${n.join(", ")}`:""}`}});async function K(t){const n=v(e.overlay);if(!n)return;if(n.hidden=!1,k.body&&k.body.classList&&k.body.classList.add("fp-open"),await N(),await F(),t&&"drive"===t.focus){const t=v(e.driveStatus);t&&t.scrollIntoView&&t.scrollIntoView({block:"center"});const n=[e.driveSignIn,e.driveConnect,e.driveSyncNow].map(v).find((e=>e&&!e.disabled));return void(n&&n.focus())}const a=v(e.close);a&&a.focus()}function q(){const t=v(e.overlay);if(!t)return;t.hidden=!0,k.body&&k.body.classList&&k.body.classList.remove("fp-open");const n=v(e.cardOpen);n&&n.focus()}const H=(e,t,n)=>{const a=v(e);a&&a.addEventListener(t,n)};return H(e.cardOpen,"click",K),H(e.close,"click",q),H(e.backdrop,"click",q),H(e.keygen,"click",O),H(e.seal,"click",P),H(e.sealCard,"click",(async()=>{const e=await P();e&&e.ok&&await K()})),H(e.verify,"click",E),H(e.publish,"click",I),H(e.anchor,"click",T),H(e.backup,"click",j),H(e.vault,"click",F),H(e.audit,"click",G),H(e.vaultBackendDrive,"click",(()=>w("drive"))),H(e.vaultBackendCloud,"click",(()=>w("aegiscloud"))),H(e.vaultBackendOff,"click",(()=>w("off"))),H(e.driveSignIn,"click",B),H(e.driveConnect,"click",C),H(e.driveSyncNow,"click",x),v(e.autoSeal)&&H(e.autoSeal,"change",(()=>{const t=v(e.autoSeal);return async function(t){if(!h||"function"!=typeof h.setMint)return null;const n=Boolean(v(e.publishOnMint)&&v(e.publishOnMint).checked);try{const e=await h.setMint({enabled:Boolean(t),publish:n});return e&&e.mint?D(e.mint):e&&!1===e.ok&&b(e.error||"could not save that",!0),e}catch(e){return b(`could not save that: ${e&&e.message||String(e)}`,!0),null}}(Boolean(t&&t.checked))})),v(e.sealNow)&&H(e.sealNow,"click",V),{open:K,close:q,isOpen:()=>{const t=v(e.overlay);return Boolean(t)&&!t.hidden},refresh:N,loadAutoMint:M,state:g,actions:{doKeygen:O,doSeal:P,doSealNow:V,doVerify:E,doPublish:I,doAnchor:T,doBackup:j,doVault:F,doAudit:G,doSetVaultBackend:w,doDriveSignIn:B,doDriveConnect:C,doDriveSyncNow:x},loadVaultBackend:$,loadDriveStatus:S}},keyLine:n,statusRows:a,keyWarning:o,vaultRows:i,anchorVerdict:s,auditVerdict:l,sealLine:u,autoMintLine:c,driveLine:r,dash:t}}();"undefined"!=typeof module&&module.exports&&(module.exports=n),e&&(e.aegisFingerprint=n)}("undefined"!=typeof window?window:null);
|