aegis-desktop 1.0.12 → 1.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/google-connect.js +171 -0
- package/lib/local/engine.js +64 -1
- package/lib/local/prompt.js +67 -37
- package/lib/local/tools.js +49 -13
- package/main.js +191 -12
- package/package.json +2 -1
- package/preload.js +14 -0
- package/renderer/app.js +15 -5
- package/renderer/fingerprint.js +75 -1
- package/renderer/index.html +17 -0
- package/vendor/aegis.js +169 -3
- package/vendor/drive-connect.js +87 -0
- package/vendor/drive-upload.js +176 -0
- package/vendor/update.js +100 -8
package/main.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
const path = require('node:path');
|
|
28
28
|
const fs = require('node:fs');
|
|
29
|
+
const { execFile } = require('node:child_process');
|
|
29
30
|
const { pathToFileURL } = require('node:url');
|
|
30
31
|
|
|
31
32
|
// Dev / CI / smoke-test layout: desktop/ lives inside the repo, so the shared
|
|
@@ -88,6 +89,30 @@ try {
|
|
|
88
89
|
sharedTopup = require('./vendor/topup.js');
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// Google identity + Drive (client/google-signin.js, client/drive-connect.js,
|
|
93
|
+
// client/drive-upload.js). google-signin.js already existed for the CLI's
|
|
94
|
+
// `connect --google`; this is the first time desktop wires any of it. See
|
|
95
|
+
// lib/google-connect.js for how the three are sequenced.
|
|
96
|
+
let googleSignIn;
|
|
97
|
+
try {
|
|
98
|
+
googleSignIn = require('../client/google-signin.js');
|
|
99
|
+
} catch {
|
|
100
|
+
googleSignIn = require('./vendor/google-signin.js');
|
|
101
|
+
}
|
|
102
|
+
let driveConnectLib;
|
|
103
|
+
try {
|
|
104
|
+
driveConnectLib = require('../client/drive-connect.js');
|
|
105
|
+
} catch {
|
|
106
|
+
driveConnectLib = require('./vendor/drive-connect.js');
|
|
107
|
+
}
|
|
108
|
+
let driveUploadLib;
|
|
109
|
+
try {
|
|
110
|
+
driveUploadLib = require('../client/drive-upload.js');
|
|
111
|
+
} catch {
|
|
112
|
+
driveUploadLib = require('./vendor/drive-upload.js');
|
|
113
|
+
}
|
|
114
|
+
const { createGoogleConnect } = require('./lib/google-connect.js');
|
|
115
|
+
|
|
91
116
|
// The device fingerprint (client/device-fingerprint.js) — the SAME identity the
|
|
92
117
|
// auto-provision path registers a device-key against, now also carried in every
|
|
93
118
|
// presence beat so one laptop is one machine in the admin panel rather than one
|
|
@@ -2771,6 +2796,10 @@ function createNpmUpdateManager({
|
|
|
2771
2796
|
fetchImpl,
|
|
2772
2797
|
setTimeoutImpl = setTimeout,
|
|
2773
2798
|
clearTimeoutImpl = clearTimeout,
|
|
2799
|
+
// Both injectable so the install path is testable without running npm or
|
|
2800
|
+
// killing the test runner's own process.
|
|
2801
|
+
runUpgrade,
|
|
2802
|
+
relaunch,
|
|
2774
2803
|
} = {}) {
|
|
2775
2804
|
const PKG = 'aegis-desktop';
|
|
2776
2805
|
let current = 'unknown';
|
|
@@ -2792,6 +2821,11 @@ function createNpmUpdateManager({
|
|
|
2792
2821
|
attempts: 0,
|
|
2793
2822
|
retryInMs: 0,
|
|
2794
2823
|
command: null,
|
|
2824
|
+
// `plan` is the shared verdict the banner was rendered from. `download()`
|
|
2825
|
+
// installs from THIS object rather than re-deriving a command, so the
|
|
2826
|
+
// button can only ever run what the banner promised.
|
|
2827
|
+
plan: null,
|
|
2828
|
+
method: null,
|
|
2795
2829
|
canDownload: false,
|
|
2796
2830
|
};
|
|
2797
2831
|
const set = (patch) => {
|
|
@@ -2818,6 +2852,34 @@ function createNpmUpdateManager({
|
|
|
2818
2852
|
let inFlight = null;
|
|
2819
2853
|
let interval = null;
|
|
2820
2854
|
|
|
2855
|
+
/**
|
|
2856
|
+
* How THIS desktop copy was installed: 'npm' | 'pip' | 'npx' | 'source'.
|
|
2857
|
+
*
|
|
2858
|
+
* Memoised — it cannot change while the process runs. `entry` is the app
|
|
2859
|
+
* directory: an unpackaged run is either a global npm install (a path with
|
|
2860
|
+
* `node_modules` in it), a checkout (`desktop/` beside `.git`), or a pip
|
|
2861
|
+
* launcher, which declares itself through AEGIS_INSTALL_METHOD because
|
|
2862
|
+
* nothing in the filesystem would otherwise say so.
|
|
2863
|
+
*/
|
|
2864
|
+
let methodCache = null;
|
|
2865
|
+
function detectMethod() {
|
|
2866
|
+
if (methodCache) return methodCache;
|
|
2867
|
+
const detect = updater && updater.detectInstallMethod;
|
|
2868
|
+
if (typeof detect !== 'function') { methodCache = 'npm'; return methodCache; }
|
|
2869
|
+
try {
|
|
2870
|
+
methodCache = detect({
|
|
2871
|
+
env: process.env,
|
|
2872
|
+
argv: process.argv,
|
|
2873
|
+
pkg: PKG,
|
|
2874
|
+
entry: __dirname,
|
|
2875
|
+
fileExists: (rel) => fs.existsSync(path.resolve(__dirname, '..', rel)),
|
|
2876
|
+
});
|
|
2877
|
+
} catch {
|
|
2878
|
+
methodCache = 'npm';
|
|
2879
|
+
}
|
|
2880
|
+
return methodCache;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2821
2883
|
/** 1m, 5m, 15m, 60m, 60m… — capped, never unbounded. */
|
|
2822
2884
|
const delayFor = () =>
|
|
2823
2885
|
UPDATE_RETRY_BACKOFF_MS[Math.min(Math.max(attempts - 1, 0), UPDATE_RETRY_BACKOFF_MS.length - 1)];
|
|
@@ -2917,8 +2979,14 @@ function createNpmUpdateManager({
|
|
|
2917
2979
|
// The npm applier's instruction comes from the ONE shared verdict
|
|
2918
2980
|
// (`updatePlan`), the same call `aegiscode`'s notice and `aegis doctor`
|
|
2919
2981
|
// render — so the banner cannot offer a command the terminal would not.
|
|
2982
|
+
//
|
|
2983
|
+
// `method` is this copy's install channel, detected the same way the
|
|
2984
|
+
// CLI does. It matters because `npm i -g` against a pip-installed
|
|
2985
|
+
// aegis-desktop writes a second copy beside the one the user actually
|
|
2986
|
+
// runs: the button would appear to work and change nothing.
|
|
2987
|
+
const method = detectMethod();
|
|
2920
2988
|
const plan = typeof updater.updatePlan === 'function'
|
|
2921
|
-
? updater.updatePlan({ current, latest: res.latest, behind: true, channel: 'npm', pkg: PKG })
|
|
2989
|
+
? updater.updatePlan({ current, latest: res.latest, behind: true, channel: 'npm', pkg: PKG, method })
|
|
2922
2990
|
: null;
|
|
2923
2991
|
return set({
|
|
2924
2992
|
status: 'available',
|
|
@@ -2928,11 +2996,13 @@ function createNpmUpdateManager({
|
|
|
2928
2996
|
transient: false,
|
|
2929
2997
|
attempts: 0,
|
|
2930
2998
|
retryInMs: 0,
|
|
2999
|
+
plan,
|
|
3000
|
+
method,
|
|
2931
3001
|
command: (plan && plan.instruction) || `npm i -g ${PKG}@latest`,
|
|
2932
|
-
//
|
|
2933
|
-
//
|
|
2934
|
-
//
|
|
2935
|
-
canDownload:
|
|
3002
|
+
// A channel with an `apply` argv has something this process can run;
|
|
3003
|
+
// `npx` and a source checkout do not, and offering a button that
|
|
3004
|
+
// silently does nothing is worse than the command alone.
|
|
3005
|
+
canDownload: Boolean(plan && plan.apply),
|
|
2936
3006
|
});
|
|
2937
3007
|
}
|
|
2938
3008
|
|
|
@@ -2974,16 +3044,97 @@ function createNpmUpdateManager({
|
|
|
2974
3044
|
}
|
|
2975
3045
|
}
|
|
2976
3046
|
|
|
3047
|
+
/**
|
|
3048
|
+
* Run one installer argv and resolve to a status patch. Never rejects.
|
|
3049
|
+
*
|
|
3050
|
+
* `npm i -g` against a global prefix needs no elevation the way a system
|
|
3051
|
+
* package does, but it still writes outside the app dir and it can fail on a
|
|
3052
|
+
* permissions wall — so a failure is reported with npm's own last lines
|
|
3053
|
+
* rather than a blanket "update failed".
|
|
3054
|
+
*/
|
|
3055
|
+
function runInstall(target) {
|
|
3056
|
+
const exec = runUpgrade || ((cmd, args) => new Promise((resolve) => {
|
|
3057
|
+
execFile(cmd, args, { timeout: 180_000, windowsHide: true }, (err, stdout, stderr) => {
|
|
3058
|
+
if (err) return resolve({ ok: false, output: String(stderr || stdout || err.message || err) });
|
|
3059
|
+
resolve({ ok: true, output: String(stdout || '') });
|
|
3060
|
+
});
|
|
3061
|
+
}));
|
|
3062
|
+
return Promise.resolve(exec(target.command, target.args)).then(
|
|
3063
|
+
(res) => ({ ok: Boolean(res && res.ok), output: String((res && res.output) || '') }),
|
|
3064
|
+
(err) => ({ ok: false, output: errorText(err) }),
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
|
|
2977
3068
|
return {
|
|
2978
3069
|
status: () => state,
|
|
2979
3070
|
check,
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3071
|
+
/**
|
|
3072
|
+
* The banner's Download button. It INSTALLS.
|
|
3073
|
+
*
|
|
3074
|
+
* This used to be a no-op that re-set `command` and `canDownload:false`:
|
|
3075
|
+
* the button was hidden for every npm install, so the affordance and the
|
|
3076
|
+
* action were both missing, and the only way to update the window was to
|
|
3077
|
+
* read a shell command off a banner and type it into a different app.
|
|
3078
|
+
*
|
|
3079
|
+
* It still runs only on an explicit click (nothing here installs on a
|
|
3080
|
+
* timer), and it still does not hot-swap the running process: after a
|
|
3081
|
+
* successful install the honest state is 'downloaded' — "restart to install"
|
|
3082
|
+
* — because the new files are on disk and this process is still the old
|
|
3083
|
+
* ones. That is the same promise the packaged electron-updater path makes,
|
|
3084
|
+
* so the banner needs no branch.
|
|
3085
|
+
*/
|
|
3086
|
+
download: async () => {
|
|
3087
|
+
const target = state.plan && state.plan.apply;
|
|
3088
|
+
if (!target) {
|
|
3089
|
+
// Nothing to install on this channel (npx fetches fresh every run; a
|
|
3090
|
+
// checkout belongs to its git remote). Say who owns it instead of
|
|
3091
|
+
// pretending to act.
|
|
3092
|
+
return set({
|
|
3093
|
+
status: state.status === 'available' ? 'available' : state.status,
|
|
3094
|
+
command: (state.plan && state.plan.instruction) || state.command,
|
|
3095
|
+
canDownload: false,
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
set({ status: 'downloading', error: null, progress: 0 });
|
|
3099
|
+
const res = await runInstall(target);
|
|
3100
|
+
if (!res.ok) {
|
|
3101
|
+
// `transient: false` deliberately: an installer that refused is not a
|
|
3102
|
+
// network blip, and the retry ladder must not quietly re-run an npm
|
|
3103
|
+
// install every minute.
|
|
3104
|
+
return set({
|
|
3105
|
+
status: 'error',
|
|
3106
|
+
transient: false,
|
|
3107
|
+
error: res.output.trim().split('\n').slice(-3).join(' ').trim() || 'the installer failed',
|
|
3108
|
+
command: `${target.command} ${target.args.join(' ')}`,
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
return set({
|
|
3112
|
+
status: 'downloaded',
|
|
3113
|
+
version: state.version,
|
|
3114
|
+
error: null,
|
|
3115
|
+
progress: 100,
|
|
3116
|
+
command: `npm i -g ${PKG}@latest`,
|
|
3117
|
+
canDownload: false,
|
|
3118
|
+
});
|
|
3119
|
+
},
|
|
3120
|
+
/**
|
|
3121
|
+
* "Restart to install". Relaunches the app so the freshly installed files
|
|
3122
|
+
* are what runs — `app.relaunch()` before `app.exit()`, the standard pair,
|
|
3123
|
+
* because `app.quit()` alone would not start a new process.
|
|
3124
|
+
*/
|
|
3125
|
+
quitAndInstall: () => {
|
|
3126
|
+
if (state.status !== 'downloaded') return;
|
|
3127
|
+
const again = relaunch || (() => {
|
|
3128
|
+
try {
|
|
3129
|
+
const { app } = require('electron');
|
|
3130
|
+
app.relaunch();
|
|
3131
|
+
app.exit(0);
|
|
3132
|
+
} catch {
|
|
3133
|
+
/* not under Electron (a test or the CLI): nothing to relaunch */
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
again();
|
|
3137
|
+
},
|
|
2987
3138
|
start: () => {
|
|
2988
3139
|
check().catch(() => {});
|
|
2989
3140
|
interval = setInterval(() => check().catch(() => {}), UPDATE_CHECK_INTERVAL_MS);
|
|
@@ -3396,6 +3547,26 @@ function bootstrap() {
|
|
|
3396
3547
|
describeFailure: autoProvision.provisionFailureText,
|
|
3397
3548
|
onChange: (state) => broadcastAccountStatus(state),
|
|
3398
3549
|
});
|
|
3550
|
+
|
|
3551
|
+
// Google sign-in + Drive connect (lib/google-connect.js). Same key store as
|
|
3552
|
+
// accountKey above (settings.setAegisKey) — one saved key, whichever door
|
|
3553
|
+
// last wrote it — and the same banner: a successful Google sign-in clears
|
|
3554
|
+
// whatever "no key" state accountKey was showing, same as a successful
|
|
3555
|
+
// register-device retry does.
|
|
3556
|
+
const googleConnect = createGoogleConnect({
|
|
3557
|
+
aegis,
|
|
3558
|
+
credentials,
|
|
3559
|
+
persistKey: (key) => settings.setAegisKey(key),
|
|
3560
|
+
openUrl: (url) => {
|
|
3561
|
+
if (!isSafeExternalUrl(url)) return false;
|
|
3562
|
+
shell.openExternal(url);
|
|
3563
|
+
return true;
|
|
3564
|
+
},
|
|
3565
|
+
googleSignIn,
|
|
3566
|
+
driveConnect: driveConnectLib,
|
|
3567
|
+
driveUpload: driveUploadLib,
|
|
3568
|
+
onAccountChange: () => broadcastAccountStatus(accountKey.status()),
|
|
3569
|
+
});
|
|
3399
3570
|
const keyPresentAtStart = Boolean(aegis.apiKey);
|
|
3400
3571
|
const earlyKeyProvision = autoProvision.autoProvisionApiKey(aegis, credentials, { app: 'desktop' }).then((result) => {
|
|
3401
3572
|
accountKey.recordStartup(result);
|
|
@@ -3873,6 +4044,14 @@ function bootstrap() {
|
|
|
3873
4044
|
registerUpdateIpc(ipcMain, updateManager);
|
|
3874
4045
|
ipcMain.handle(`${IPC_PREFIX}accountStatus`, () => accountKey.status());
|
|
3875
4046
|
ipcMain.handle(`${IPC_PREFIX}registerAccount`, () => accountKey.registerAccount());
|
|
4047
|
+
// Google sign-in + Drive connect/sync (lib/google-connect.js). Every call
|
|
4048
|
+
// resolves `{ ok, … }` / `{ ok: false, error }` rather than throwing, same
|
|
4049
|
+
// contract as the fingerprint* handlers below — the renderer shows the
|
|
4050
|
+
// actual reason instead of Electron's opaque "Error invoking remote method".
|
|
4051
|
+
ipcMain.handle(`${IPC_PREFIX}driveSignIn`, () => googleConnect.signInWithGoogle());
|
|
4052
|
+
ipcMain.handle(`${IPC_PREFIX}driveConnect`, () => googleConnect.connectDrive());
|
|
4053
|
+
ipcMain.handle(`${IPC_PREFIX}driveStatus`, () => googleConnect.status());
|
|
4054
|
+
ipcMain.handle(`${IPC_PREFIX}driveSyncMemory`, () => googleConnect.syncMemoryNow());
|
|
3876
4055
|
registerExportIpc(
|
|
3877
4056
|
ipcMain,
|
|
3878
4057
|
sessionStore,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.13",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"start": "electron .",
|
|
28
28
|
"predist": "node scripts/predist.mjs",
|
|
29
|
+
"prepack": "node scripts/predist.mjs --quiet",
|
|
29
30
|
"prepublishOnly": "npm run predist",
|
|
30
31
|
"icon": "node scripts/generate-icon.mjs",
|
|
31
32
|
"shots": "node scripts/capture-marketing-shots.mjs",
|
package/preload.js
CHANGED
|
@@ -285,6 +285,20 @@ const api = {
|
|
|
285
285
|
ipcRenderer.on(ACCOUNT_STATUS_CHANNEL, listener);
|
|
286
286
|
return () => ipcRenderer.removeListener(ACCOUNT_STATUS_CHANNEL, listener);
|
|
287
287
|
},
|
|
288
|
+
// Google sign-in + Drive connect/sync (main.js lib/google-connect.js).
|
|
289
|
+
// `driveSignIn` opens a browser for aegis1's `/auth/google` round-trip and
|
|
290
|
+
// replaces this machine's saved key with the Google-linked one — the
|
|
291
|
+
// desktop counterpart of the CLI's `aegiscode connect`. `driveConnect`
|
|
292
|
+
// opens `/connect/drive` and polls until this (now Google-linked) account
|
|
293
|
+
// shows a Drive token on file; it is only coherent once `driveSignIn` has
|
|
294
|
+
// actually run. `driveStatus` is a one-shot, no-browser read for painting
|
|
295
|
+
// the card on load. `driveSyncMemory` pulls this account's AEGIS cloud
|
|
296
|
+
// memory and backs it up to a dedicated "AEGIS Memory" Drive folder. Every
|
|
297
|
+
// one resolves `{ ok, … }` / `{ ok: false, error }`, never throws.
|
|
298
|
+
driveSignIn: () => invoke('driveSignIn'),
|
|
299
|
+
driveConnect: () => invoke('driveConnect'),
|
|
300
|
+
driveStatus: () => invoke('driveStatus'),
|
|
301
|
+
driveSyncMemory: () => invoke('driveSyncMemory'),
|
|
288
302
|
// Native menu accelerators with no built-in Electron role (Cmd/Ctrl+N,
|
|
289
303
|
// Cmd/Ctrl+K — see main.js buildAppMenu): main.js just pings the channel,
|
|
290
304
|
// the renderer owns what "new chat" / "search" actually do.
|
package/renderer/app.js
CHANGED
|
@@ -1183,12 +1183,15 @@ function renderUpdateBanner(state) {
|
|
|
1183
1183
|
let showRetry = false;
|
|
1184
1184
|
const version = state.version ? `v${state.version} ` : '';
|
|
1185
1185
|
if (status === 'available') {
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1186
|
+
// A channel the main process can install for (npm, pip) gets a real
|
|
1187
|
+
// Download button — `canDownload` is true only when it has an argv to run.
|
|
1188
|
+
// The rest (npx, a source checkout) have nothing to install, so the banner
|
|
1189
|
+
// names the command instead of offering a button that would do nothing.
|
|
1188
1190
|
text = state.command
|
|
1189
1191
|
? `Update ${version}available — run: ${state.command}`
|
|
1190
1192
|
: `Update ${version}available.`;
|
|
1191
1193
|
showDownload = !!state.canDownload;
|
|
1194
|
+
if (showDownload) text = `Update ${version}available.`;
|
|
1192
1195
|
} else if (status === 'downloading') {
|
|
1193
1196
|
const pct = typeof state.progress === 'number' ? ` (${Math.round(state.progress)}%)` : '';
|
|
1194
1197
|
text = `Downloading update${pct}…`;
|
|
@@ -6467,9 +6470,16 @@ async function init() {
|
|
|
6467
6470
|
|
|
6468
6471
|
els.updateDownloadBtn.addEventListener('click', () => {
|
|
6469
6472
|
els.updateDownloadBtn.disabled = true;
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
+
// The resolved value is the post-install state: render it as well as
|
|
6474
|
+
// waiting for the push, so a channel whose installer finishes inside the
|
|
6475
|
+
// invoke still repaints (and so a failure surfaces even if the push
|
|
6476
|
+
// channel is not wired yet).
|
|
6477
|
+
Promise.resolve(aegis.downloadUpdate())
|
|
6478
|
+
.then((state) => { if (state && state.status) renderUpdateBanner(state); })
|
|
6479
|
+
.catch(() => {})
|
|
6480
|
+
.finally(() => {
|
|
6481
|
+
els.updateDownloadBtn.disabled = false;
|
|
6482
|
+
});
|
|
6473
6483
|
});
|
|
6474
6484
|
|
|
6475
6485
|
// The only place quitAndInstallUpdate is ever called — an explicit user
|
package/renderer/fingerprint.js
CHANGED
|
@@ -78,6 +78,10 @@
|
|
|
78
78
|
audit: 'fp-audit',
|
|
79
79
|
vaultList: 'fp-vault-list',
|
|
80
80
|
out: 'fp-out',
|
|
81
|
+
driveStatus: 'fp-drive-status',
|
|
82
|
+
driveSignIn: 'fp-drive-signin',
|
|
83
|
+
driveConnect: 'fp-drive-connect',
|
|
84
|
+
driveSyncNow: 'fp-drive-sync',
|
|
81
85
|
};
|
|
82
86
|
|
|
83
87
|
// ------------------------------------------------------------- formatting
|
|
@@ -139,6 +143,23 @@
|
|
|
139
143
|
});
|
|
140
144
|
}
|
|
141
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
|
+
|
|
142
163
|
/** The anchor answer, kept as three separate claims on purpose: "the file is
|
|
143
164
|
* gone", "the file was replaced" and "the key that sealed it has moved on"
|
|
144
165
|
* are three different situations, and one boolean hides the interesting one
|
|
@@ -342,6 +363,50 @@
|
|
|
342
363
|
say: (r) => (r.ok ? `vault backend set to ${dash(r.backend)}` : `could not save that: ${r.error}`),
|
|
343
364
|
});
|
|
344
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
|
+
|
|
345
410
|
function paintVault(list) {
|
|
346
411
|
const ul = el(IDS.vaultList);
|
|
347
412
|
if (!ul) return;
|
|
@@ -368,6 +433,7 @@
|
|
|
368
433
|
paintStatus();
|
|
369
434
|
await loadAutoMint();
|
|
370
435
|
await loadVaultBackend();
|
|
436
|
+
await loadDriveStatus();
|
|
371
437
|
return state.status;
|
|
372
438
|
}
|
|
373
439
|
|
|
@@ -606,6 +672,9 @@
|
|
|
606
672
|
bind(IDS.vaultBackendDrive, 'click', () => doSetVaultBackend('drive'));
|
|
607
673
|
bind(IDS.vaultBackendCloud, 'click', () => doSetVaultBackend('aegiscloud'));
|
|
608
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);
|
|
609
678
|
// The auto-seal switch is the ONE control on this panel that can file a
|
|
610
679
|
// record with nobody watching, so it is the one that must repaint from
|
|
611
680
|
// main's answer rather than from the checkbox the user just clicked.
|
|
@@ -626,8 +695,12 @@
|
|
|
626
695
|
state,
|
|
627
696
|
// Exposed for the test and for app.js's on-demand repaint. Every one is
|
|
628
697
|
// idempotent, so calling them from a console is safe.
|
|
629
|
-
actions: {
|
|
698
|
+
actions: {
|
|
699
|
+
doKeygen, doSeal, doSealNow, doVerify, doPublish, doAnchor, doBackup, doVault, doAudit,
|
|
700
|
+
doSetVaultBackend, doDriveSignIn, doDriveConnect, doDriveSyncNow,
|
|
701
|
+
},
|
|
630
702
|
loadVaultBackend,
|
|
703
|
+
loadDriveStatus,
|
|
631
704
|
};
|
|
632
705
|
}
|
|
633
706
|
|
|
@@ -643,6 +716,7 @@
|
|
|
643
716
|
auditVerdict,
|
|
644
717
|
sealLine,
|
|
645
718
|
autoMintLine,
|
|
719
|
+
driveLine,
|
|
646
720
|
dash,
|
|
647
721
|
};
|
|
648
722
|
});
|
package/renderer/index.html
CHANGED
|
@@ -927,6 +927,23 @@
|
|
|
927
927
|
|
|
928
928
|
<ul class="fp-vault-list" id="fp-vault-list"></ul>
|
|
929
929
|
|
|
930
|
+
<!-- Google Drive — memory backup. Unrelated to the vault above: that
|
|
931
|
+
publishes a sealed φ(α) RECORD; this backs up the account's AEGIS
|
|
932
|
+
CLOUD MEMORY to the user's own Drive through real Google OAuth
|
|
933
|
+
(desktop/lib/google-connect.js). Two buttons because connecting
|
|
934
|
+
Drive is only coherent once this machine has signed in with the
|
|
935
|
+
SAME Google identity — see that file's header comment — so
|
|
936
|
+
"Connect Drive" stays disabled (fingerprint.js paintDrive()) until
|
|
937
|
+
sign-in has actually landed. -->
|
|
938
|
+
<div class="fp-controls">
|
|
939
|
+
<div class="fp-row"><span class="fp-k">Google Drive</span><span class="fp-v" id="fp-drive-status">–</span></div>
|
|
940
|
+
<div class="fp-actions">
|
|
941
|
+
<button type="button" id="fp-drive-signin" class="ghost-btn">Sign in with Google</button>
|
|
942
|
+
<button type="button" id="fp-drive-connect" class="ghost-btn" disabled>Connect Drive</button>
|
|
943
|
+
<button type="button" id="fp-drive-sync" class="ghost-btn" disabled>Back up memory now</button>
|
|
944
|
+
</div>
|
|
945
|
+
</div>
|
|
946
|
+
|
|
930
947
|
<div class="fp-out" id="fp-out">Ready. The key is never displayed.</div>
|
|
931
948
|
</div>
|
|
932
949
|
</div>
|