@membox-cloud/membox 0.1.0
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/README.md +169 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +159 -0
- package/dist/src/api/account.d.ts +3 -0
- package/dist/src/api/account.js +3 -0
- package/dist/src/api/client.d.ts +21 -0
- package/dist/src/api/client.js +107 -0
- package/dist/src/api/device-flow.d.ts +9 -0
- package/dist/src/api/device-flow.js +55 -0
- package/dist/src/api/devices.d.ts +10 -0
- package/dist/src/api/devices.js +24 -0
- package/dist/src/api/recovery.d.ts +5 -0
- package/dist/src/api/recovery.js +9 -0
- package/dist/src/api/sync.d.ts +9 -0
- package/dist/src/api/sync.js +22 -0
- package/dist/src/cli/bootstrap.d.ts +37 -0
- package/dist/src/cli/bootstrap.js +326 -0
- package/dist/src/cli/grants.d.ts +8 -0
- package/dist/src/cli/grants.js +76 -0
- package/dist/src/cli/helpers.d.ts +6 -0
- package/dist/src/cli/helpers.js +13 -0
- package/dist/src/cli/passphrase-input.d.ts +11 -0
- package/dist/src/cli/passphrase-input.js +94 -0
- package/dist/src/cli/pause.d.ts +2 -0
- package/dist/src/cli/pause.js +29 -0
- package/dist/src/cli/provisioning.d.ts +3 -0
- package/dist/src/cli/provisioning.js +30 -0
- package/dist/src/cli/pull.d.ts +31 -0
- package/dist/src/cli/pull.js +142 -0
- package/dist/src/cli/restore.d.ts +12 -0
- package/dist/src/cli/restore.js +90 -0
- package/dist/src/cli/setup.d.ts +17 -0
- package/dist/src/cli/setup.js +209 -0
- package/dist/src/cli/status.d.ts +1 -0
- package/dist/src/cli/status.js +31 -0
- package/dist/src/cli/sync.d.ts +4 -0
- package/dist/src/cli/sync.js +124 -0
- package/dist/src/cli/unlock.d.ts +19 -0
- package/dist/src/cli/unlock.js +153 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +12 -0
- package/dist/src/constants.d.ts +23 -0
- package/dist/src/constants.js +27 -0
- package/dist/src/contract-types.d.ts +301 -0
- package/dist/src/contract-types.js +52 -0
- package/dist/src/crypto/aes-gcm.d.ts +29 -0
- package/dist/src/crypto/aes-gcm.js +44 -0
- package/dist/src/crypto/device-keys.d.ts +18 -0
- package/dist/src/crypto/device-keys.js +25 -0
- package/dist/src/crypto/grant.d.ts +29 -0
- package/dist/src/crypto/grant.js +87 -0
- package/dist/src/crypto/kdf.d.ts +16 -0
- package/dist/src/crypto/kdf.js +24 -0
- package/dist/src/crypto/keys.d.ts +14 -0
- package/dist/src/crypto/keys.js +35 -0
- package/dist/src/crypto/manifest.d.ts +25 -0
- package/dist/src/crypto/manifest.js +41 -0
- package/dist/src/crypto/recovery.d.ts +16 -0
- package/dist/src/crypto/recovery.js +94 -0
- package/dist/src/crypto/types.d.ts +34 -0
- package/dist/src/crypto/types.js +1 -0
- package/dist/src/debug-logger.d.ts +32 -0
- package/dist/src/debug-logger.js +108 -0
- package/dist/src/hooks/gateway-lifecycle.d.ts +6 -0
- package/dist/src/hooks/gateway-lifecycle.js +40 -0
- package/dist/src/hooks/prompt-inject.d.ts +7 -0
- package/dist/src/hooks/prompt-inject.js +18 -0
- package/dist/src/store/keychain.d.ts +26 -0
- package/dist/src/store/keychain.js +151 -0
- package/dist/src/store/local-state.d.ts +27 -0
- package/dist/src/store/local-state.js +47 -0
- package/dist/src/store/managed-unlock.d.ts +8 -0
- package/dist/src/store/managed-unlock.js +46 -0
- package/dist/src/store/pending-setup.d.ts +23 -0
- package/dist/src/store/pending-setup.js +32 -0
- package/dist/src/store/session.d.ts +13 -0
- package/dist/src/store/session.js +28 -0
- package/dist/src/sync/auto-sync.d.ts +12 -0
- package/dist/src/sync/auto-sync.js +82 -0
- package/dist/src/sync/conflict.d.ts +24 -0
- package/dist/src/sync/conflict.js +92 -0
- package/dist/src/sync/diff.d.ts +25 -0
- package/dist/src/sync/diff.js +75 -0
- package/dist/src/sync/downloader.d.ts +16 -0
- package/dist/src/sync/downloader.js +73 -0
- package/dist/src/sync/scanner.d.ts +12 -0
- package/dist/src/sync/scanner.js +52 -0
- package/dist/src/sync/state.d.ts +4 -0
- package/dist/src/sync/state.js +22 -0
- package/dist/src/sync/uploader.d.ts +20 -0
- package/dist/src/sync/uploader.js +86 -0
- package/dist/src/tools/grants-approve-pending.d.ts +17 -0
- package/dist/src/tools/grants-approve-pending.js +50 -0
- package/dist/src/tools/pull.d.ts +31 -0
- package/dist/src/tools/pull.js +71 -0
- package/dist/src/tools/restore.d.ts +31 -0
- package/dist/src/tools/restore.js +96 -0
- package/dist/src/tools/result.d.ts +7 -0
- package/dist/src/tools/result.js +6 -0
- package/dist/src/tools/secret-file.d.ts +10 -0
- package/dist/src/tools/secret-file.js +37 -0
- package/dist/src/tools/setup-finish.d.ts +36 -0
- package/dist/src/tools/setup-finish.js +108 -0
- package/dist/src/tools/setup-poll.d.ts +27 -0
- package/dist/src/tools/setup-poll.js +83 -0
- package/dist/src/tools/setup-start.d.ts +18 -0
- package/dist/src/tools/setup-start.js +49 -0
- package/dist/src/tools/status.d.ts +17 -0
- package/dist/src/tools/status.js +40 -0
- package/dist/src/tools/sync.d.ts +17 -0
- package/dist/src/tools/sync.js +49 -0
- package/dist/src/tools/unlock-secret.d.ts +42 -0
- package/dist/src/tools/unlock-secret.js +87 -0
- package/dist/src/tools/unlock.d.ts +25 -0
- package/dist/src/tools/unlock.js +72 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +35 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { API_PREFIX, PLUGIN_VERSION } from "../constants.js";
|
|
4
|
+
import { deriveURK } from "../crypto/kdf.js";
|
|
5
|
+
import { wrapAMK } from "../crypto/keys.js";
|
|
6
|
+
import { generateDeviceKeyPair, } from "../crypto/device-keys.js";
|
|
7
|
+
import { getAccountMe } from "../api/account.js";
|
|
8
|
+
import { MemboxApiClient } from "../api/client.js";
|
|
9
|
+
import { pollDeviceFlow, pollDeviceFlowOnce, startDeviceFlow, } from "../api/device-flow.js";
|
|
10
|
+
import { ACCOUNTS, getBytes, getString, storeBytes, storeString, clearPendingSetupSecrets, } from "../store/keychain.js";
|
|
11
|
+
import { deletePendingSetup, isPendingSetupExpired, readPendingSetup, writePendingSetup, } from "../store/pending-setup.js";
|
|
12
|
+
export function toB64(data) {
|
|
13
|
+
return Buffer.from(data).toString("base64");
|
|
14
|
+
}
|
|
15
|
+
export function fromB64(value) {
|
|
16
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
17
|
+
}
|
|
18
|
+
export function detectPlatform() {
|
|
19
|
+
switch (process.platform) {
|
|
20
|
+
case "darwin":
|
|
21
|
+
return "macos";
|
|
22
|
+
case "win32":
|
|
23
|
+
return "windows";
|
|
24
|
+
case "linux":
|
|
25
|
+
return "linux";
|
|
26
|
+
default:
|
|
27
|
+
return "unknown";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function tryOpenUrl(url) {
|
|
31
|
+
if (process.platform === "linux" &&
|
|
32
|
+
!process.env.DISPLAY &&
|
|
33
|
+
!process.env.WAYLAND_DISPLAY) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const cmd = process.platform === "darwin"
|
|
38
|
+
? "open"
|
|
39
|
+
: process.platform === "win32"
|
|
40
|
+
? "start"
|
|
41
|
+
: "xdg-open";
|
|
42
|
+
execFile(cmd, [url], { shell: process.platform === "win32" }, () => { });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function createAnonymousClient(baseUrl) {
|
|
50
|
+
return new MemboxApiClient(baseUrl, async () => null, async () => null, async () => { });
|
|
51
|
+
}
|
|
52
|
+
function createAuthenticatedClient(params) {
|
|
53
|
+
let accessToken = params.accessToken ?? null;
|
|
54
|
+
let refreshToken = params.refreshToken;
|
|
55
|
+
return new MemboxApiClient(params.baseUrl, async () => accessToken, async () => refreshToken, async (access, refresh) => {
|
|
56
|
+
accessToken = access;
|
|
57
|
+
refreshToken = refresh;
|
|
58
|
+
await storeString(params.refreshTokenAccount, refresh);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function pendingSetupBaseUrl(cfg) {
|
|
62
|
+
return cfg.serverUrl + API_PREFIX;
|
|
63
|
+
}
|
|
64
|
+
function describeAccount(account) {
|
|
65
|
+
return account.display_name ?? account.primary_email ?? account.user_id;
|
|
66
|
+
}
|
|
67
|
+
async function storePendingSetupDeviceKeys(deviceKeys) {
|
|
68
|
+
await storeBytes(ACCOUNTS.PENDING_SETUP_ED25519_PRIVATE, deviceKeys.ed25519.privateKey);
|
|
69
|
+
await storeBytes(ACCOUNTS.PENDING_SETUP_ED25519_PUBLIC, deviceKeys.ed25519.publicKey);
|
|
70
|
+
await storeBytes(ACCOUNTS.PENDING_SETUP_X25519_PRIVATE, deviceKeys.x25519.privateKey);
|
|
71
|
+
await storeBytes(ACCOUNTS.PENDING_SETUP_X25519_PUBLIC, deviceKeys.x25519.publicKey);
|
|
72
|
+
}
|
|
73
|
+
async function loadPendingSetupDeviceKeys() {
|
|
74
|
+
const [ed25519Private, ed25519Public, x25519Private, x25519Public,] = await Promise.all([
|
|
75
|
+
getBytes(ACCOUNTS.PENDING_SETUP_ED25519_PRIVATE),
|
|
76
|
+
getBytes(ACCOUNTS.PENDING_SETUP_ED25519_PUBLIC),
|
|
77
|
+
getBytes(ACCOUNTS.PENDING_SETUP_X25519_PRIVATE),
|
|
78
|
+
getBytes(ACCOUNTS.PENDING_SETUP_X25519_PUBLIC),
|
|
79
|
+
]);
|
|
80
|
+
if (!ed25519Private ||
|
|
81
|
+
!ed25519Public ||
|
|
82
|
+
!x25519Private ||
|
|
83
|
+
!x25519Public) {
|
|
84
|
+
throw new Error("Pending setup secrets are incomplete. Start setup again to regenerate device authorization.");
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ed25519: {
|
|
88
|
+
privateKey: ed25519Private,
|
|
89
|
+
publicKey: ed25519Public,
|
|
90
|
+
},
|
|
91
|
+
x25519: {
|
|
92
|
+
privateKey: x25519Private,
|
|
93
|
+
publicKey: x25519Public,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function clearPendingSetupArtifacts() {
|
|
98
|
+
await clearPendingSetupSecrets();
|
|
99
|
+
await deletePendingSetup();
|
|
100
|
+
}
|
|
101
|
+
async function loadActivePendingSetup(cfg) {
|
|
102
|
+
const pendingSetup = await readPendingSetup();
|
|
103
|
+
if (!pendingSetup) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (pendingSetup.server_url !== cfg.serverUrl || isPendingSetupExpired(pendingSetup)) {
|
|
107
|
+
await clearPendingSetupArtifacts();
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return pendingSetup;
|
|
111
|
+
}
|
|
112
|
+
async function createPendingSetup(cfg) {
|
|
113
|
+
const deviceName = hostname();
|
|
114
|
+
const platform = detectPlatform();
|
|
115
|
+
const deviceKeys = generateDeviceKeyPair();
|
|
116
|
+
const baseUrl = pendingSetupBaseUrl(cfg);
|
|
117
|
+
const tempClient = createAnonymousClient(baseUrl);
|
|
118
|
+
const deviceStart = await startDeviceFlow(tempClient, {
|
|
119
|
+
device_name: deviceName,
|
|
120
|
+
platform,
|
|
121
|
+
plugin_version: PLUGIN_VERSION,
|
|
122
|
+
sign_public_key_b64: toB64(deviceKeys.ed25519.publicKey),
|
|
123
|
+
kex_public_key_b64: toB64(deviceKeys.x25519.publicKey),
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
await storePendingSetupDeviceKeys(deviceKeys);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
throw new Error(`Failed to persist pending setup device keys in the local keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
130
|
+
}
|
|
131
|
+
const pendingSetup = {
|
|
132
|
+
status: "pending",
|
|
133
|
+
server_url: cfg.serverUrl,
|
|
134
|
+
device_name: deviceName,
|
|
135
|
+
platform,
|
|
136
|
+
device_code: deviceStart.device_code,
|
|
137
|
+
user_code: deviceStart.user_code,
|
|
138
|
+
verification_uri: deviceStart.verification_uri,
|
|
139
|
+
verification_uri_complete: deviceStart.verification_uri_complete,
|
|
140
|
+
interval_seconds: deviceStart.interval,
|
|
141
|
+
expires_at: new Date(Date.now() + deviceStart.expires_in * 1000).toISOString(),
|
|
142
|
+
started_at: new Date().toISOString(),
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
await writePendingSetup(pendingSetup);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
throw new Error(`Failed to persist pending setup state on disk: ${error instanceof Error ? error.message : String(error)}`);
|
|
149
|
+
}
|
|
150
|
+
return pendingSetup;
|
|
151
|
+
}
|
|
152
|
+
export async function ensurePendingDeviceAuthorization(cfg) {
|
|
153
|
+
const existing = await loadActivePendingSetup(cfg);
|
|
154
|
+
if (existing) {
|
|
155
|
+
return { pendingSetup: existing, created: false };
|
|
156
|
+
}
|
|
157
|
+
const created = await createPendingSetup(cfg);
|
|
158
|
+
return { pendingSetup: created, created: true };
|
|
159
|
+
}
|
|
160
|
+
async function finalizePendingAuthorization(cfg, pendingSetup, tokens) {
|
|
161
|
+
await storeString(ACCOUNTS.PENDING_SETUP_REFRESH_TOKEN, tokens.refresh_token);
|
|
162
|
+
const client = createAuthenticatedClient({
|
|
163
|
+
baseUrl: pendingSetupBaseUrl(cfg),
|
|
164
|
+
accessToken: tokens.access_token,
|
|
165
|
+
refreshToken: tokens.refresh_token,
|
|
166
|
+
refreshTokenAccount: ACCOUNTS.PENDING_SETUP_REFRESH_TOKEN,
|
|
167
|
+
});
|
|
168
|
+
const account = await getAccountMe(client);
|
|
169
|
+
const authorizedSetup = {
|
|
170
|
+
...pendingSetup,
|
|
171
|
+
status: "authorized",
|
|
172
|
+
device_id: tokens.device_id,
|
|
173
|
+
user_id: account.user_id,
|
|
174
|
+
account_label: describeAccount(account),
|
|
175
|
+
authorized_at: new Date().toISOString(),
|
|
176
|
+
};
|
|
177
|
+
await writePendingSetup(authorizedSetup);
|
|
178
|
+
return { pendingSetup: authorizedSetup, account };
|
|
179
|
+
}
|
|
180
|
+
export async function pollPendingDeviceAuthorizationOnce(cfg) {
|
|
181
|
+
const pendingSetup = await loadActivePendingSetup(cfg);
|
|
182
|
+
if (!pendingSetup) {
|
|
183
|
+
return {
|
|
184
|
+
status: "not_started",
|
|
185
|
+
pendingSetup: null,
|
|
186
|
+
message: "No pending setup authorization is active.",
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (pendingSetup.status === "authorized") {
|
|
190
|
+
return {
|
|
191
|
+
status: "authorized",
|
|
192
|
+
pendingSetup,
|
|
193
|
+
message: "Browser authorization is already complete.",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const client = createAnonymousClient(pendingSetupBaseUrl(cfg));
|
|
197
|
+
const result = await pollDeviceFlowOnce(client, pendingSetup.device_code);
|
|
198
|
+
if (result.status === "access_granted") {
|
|
199
|
+
const finalized = await finalizePendingAuthorization(cfg, pendingSetup, {
|
|
200
|
+
access_token: result.access_token,
|
|
201
|
+
refresh_token: result.refresh_token,
|
|
202
|
+
token_type: result.token_type ?? "Bearer",
|
|
203
|
+
expires_in: result.expires_in,
|
|
204
|
+
device_id: result.device_id,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
status: "authorized",
|
|
208
|
+
pendingSetup: finalized.pendingSetup,
|
|
209
|
+
message: `Authorized as ${describeAccount(finalized.account)}.`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (result.status === "authorization_pending") {
|
|
213
|
+
return {
|
|
214
|
+
status: "authorization_pending",
|
|
215
|
+
pendingSetup,
|
|
216
|
+
retryAfterSeconds: pendingSetup.interval_seconds,
|
|
217
|
+
message: "Waiting for the user to finish browser authorization.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (result.status === "slow_down") {
|
|
221
|
+
return {
|
|
222
|
+
status: "slow_down",
|
|
223
|
+
pendingSetup,
|
|
224
|
+
retryAfterSeconds: pendingSetup.interval_seconds + 1,
|
|
225
|
+
message: "The authorization server asked this device to poll more slowly.",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
await clearPendingSetupArtifacts();
|
|
229
|
+
return {
|
|
230
|
+
status: result.status === "denied" ? "denied" : "expired",
|
|
231
|
+
pendingSetup: null,
|
|
232
|
+
message: result.status === "denied"
|
|
233
|
+
? "The browser authorization was denied."
|
|
234
|
+
: "The browser authorization expired. Start setup again.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function buildAuthorizedContextFromPending(cfg, pendingSetup, options) {
|
|
238
|
+
if (pendingSetup.status !== "authorized" || !pendingSetup.device_id) {
|
|
239
|
+
throw new Error("Device authorization has not completed yet. Finish browser authorization first.");
|
|
240
|
+
}
|
|
241
|
+
const refreshToken = options?.refreshToken ??
|
|
242
|
+
(await getString(ACCOUNTS.PENDING_SETUP_REFRESH_TOKEN));
|
|
243
|
+
if (!refreshToken) {
|
|
244
|
+
throw new Error("Pending setup is missing its refresh token. Start setup again.");
|
|
245
|
+
}
|
|
246
|
+
const deviceKeys = await loadPendingSetupDeviceKeys();
|
|
247
|
+
const client = createAuthenticatedClient({
|
|
248
|
+
baseUrl: pendingSetupBaseUrl(cfg),
|
|
249
|
+
accessToken: options?.accessToken,
|
|
250
|
+
refreshToken,
|
|
251
|
+
refreshTokenAccount: ACCOUNTS.PENDING_SETUP_REFRESH_TOKEN,
|
|
252
|
+
});
|
|
253
|
+
const account = options?.account ?? (await getAccountMe(client));
|
|
254
|
+
return {
|
|
255
|
+
baseUrl: pendingSetupBaseUrl(cfg),
|
|
256
|
+
deviceName: pendingSetup.device_name,
|
|
257
|
+
platform: pendingSetup.platform,
|
|
258
|
+
deviceKeys,
|
|
259
|
+
tokens: {
|
|
260
|
+
access_token: options?.accessToken ?? "",
|
|
261
|
+
refresh_token: refreshToken,
|
|
262
|
+
token_type: "Bearer",
|
|
263
|
+
expires_in: 0,
|
|
264
|
+
device_id: pendingSetup.device_id,
|
|
265
|
+
},
|
|
266
|
+
client,
|
|
267
|
+
account,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function printAuthorizationPrompt(pendingSetup) {
|
|
271
|
+
console.log(`\n Your code: ${pendingSetup.user_code}`);
|
|
272
|
+
console.log(` Open: ${pendingSetup.verification_uri_complete}\n`);
|
|
273
|
+
}
|
|
274
|
+
async function waitForPendingAuthorization(cfg, pendingSetup) {
|
|
275
|
+
const client = createAnonymousClient(pendingSetupBaseUrl(cfg));
|
|
276
|
+
const expiresAt = Date.parse(pendingSetup.expires_at);
|
|
277
|
+
const tokens = await pollDeviceFlow(client, pendingSetup.device_code, pendingSetup.interval_seconds, Number.isFinite(expiresAt)
|
|
278
|
+
? Math.max(1, Math.ceil((expiresAt - Date.now()) / 1000))
|
|
279
|
+
: 15 * 60);
|
|
280
|
+
const finalized = await finalizePendingAuthorization(cfg, pendingSetup, tokens);
|
|
281
|
+
console.log("Authorized!");
|
|
282
|
+
console.log(`Logged in as: ${describeAccount(finalized.account)}`);
|
|
283
|
+
return buildAuthorizedContextFromPending(cfg, finalized.pendingSetup, {
|
|
284
|
+
accessToken: tokens.access_token,
|
|
285
|
+
refreshToken: tokens.refresh_token,
|
|
286
|
+
account: finalized.account,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
export async function authorizeDevice(cfg) {
|
|
290
|
+
const { pendingSetup, created } = await ensurePendingDeviceAuthorization(cfg);
|
|
291
|
+
if (pendingSetup.status === "authorized") {
|
|
292
|
+
console.log("Reusing previously completed browser authorization.");
|
|
293
|
+
return buildAuthorizedContextFromPending(cfg, pendingSetup);
|
|
294
|
+
}
|
|
295
|
+
if (created) {
|
|
296
|
+
console.log("Starting device authorization...");
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
console.log("Resuming pending device authorization...");
|
|
300
|
+
}
|
|
301
|
+
printAuthorizationPrompt(pendingSetup);
|
|
302
|
+
const browserOpened = tryOpenUrl(pendingSetup.verification_uri_complete);
|
|
303
|
+
if (!browserOpened) {
|
|
304
|
+
console.log(" No local browser was opened. If this machine is headless, open that URL in any trusted browser and sign in there.\n");
|
|
305
|
+
}
|
|
306
|
+
console.log("Waiting for authorization...");
|
|
307
|
+
return waitForPendingAuthorization(cfg, pendingSetup);
|
|
308
|
+
}
|
|
309
|
+
export async function persistVaultSecrets(params) {
|
|
310
|
+
const { urk, salt } = await deriveURK(params.passphrase);
|
|
311
|
+
try {
|
|
312
|
+
const wrappedAmk = wrapAMK(urk, params.amk);
|
|
313
|
+
await storeBytes(ACCOUNTS.ED25519_PRIVATE, params.deviceKeys.ed25519.privateKey);
|
|
314
|
+
await storeBytes(ACCOUNTS.ED25519_PUBLIC, params.deviceKeys.ed25519.publicKey);
|
|
315
|
+
await storeBytes(ACCOUNTS.X25519_PRIVATE, params.deviceKeys.x25519.privateKey);
|
|
316
|
+
await storeBytes(ACCOUNTS.X25519_PUBLIC, params.deviceKeys.x25519.publicKey);
|
|
317
|
+
await storeBytes(ACCOUNTS.WRAPPED_AMK, wrappedAmk.encrypted_amk);
|
|
318
|
+
await storeBytes(ACCOUNTS.WRAPPED_AMK_SALT, salt);
|
|
319
|
+
await storeBytes(ACCOUNTS.WRAPPED_AMK_IV, wrappedAmk.iv);
|
|
320
|
+
await storeBytes(ACCOUNTS.WRAPPED_AMK_TAG, wrappedAmk.tag);
|
|
321
|
+
await storeString(ACCOUNTS.REFRESH_TOKEN, params.refreshToken);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
urk.fill(0);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ApprovePendingGrantsResult {
|
|
2
|
+
approved: number;
|
|
3
|
+
skipped: number;
|
|
4
|
+
total: number;
|
|
5
|
+
approvedGrantIds: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function approvePendingGrants(): Promise<ApprovePendingGrantsResult>;
|
|
8
|
+
export declare function approvePendingGrantsAction(): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createAuthenticatedClient } from "./helpers.js";
|
|
2
|
+
import { vaultSession } from "../store/session.js";
|
|
3
|
+
import { getState } from "../sync/state.js";
|
|
4
|
+
import { ACCOUNTS, getBytes, } from "../store/keychain.js";
|
|
5
|
+
import { createDeviceGrant } from "../crypto/grant.js";
|
|
6
|
+
import { computeSha256Hex } from "../crypto/manifest.js";
|
|
7
|
+
import { approveGrant, getPendingGrants, } from "../api/devices.js";
|
|
8
|
+
import { fromB64, toB64 } from "./bootstrap.js";
|
|
9
|
+
import { ensureVaultUnlocked } from "./unlock.js";
|
|
10
|
+
export async function approvePendingGrants() {
|
|
11
|
+
if (!(await ensureVaultUnlocked({ announceSuccess: true }))) {
|
|
12
|
+
return {
|
|
13
|
+
approved: 0,
|
|
14
|
+
skipped: 0,
|
|
15
|
+
total: 0,
|
|
16
|
+
approvedGrantIds: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const state = await getState();
|
|
20
|
+
const client = await createAuthenticatedClient(state);
|
|
21
|
+
const pending = await getPendingGrants(client);
|
|
22
|
+
if (pending.length === 0) {
|
|
23
|
+
console.log("No pending trusted-device approvals.");
|
|
24
|
+
return {
|
|
25
|
+
approved: 0,
|
|
26
|
+
skipped: 0,
|
|
27
|
+
total: 0,
|
|
28
|
+
approvedGrantIds: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const sourceEdPrivateKey = await getBytes(ACCOUNTS.ED25519_PRIVATE);
|
|
32
|
+
const sourceXPrivateKey = await getBytes(ACCOUNTS.X25519_PRIVATE);
|
|
33
|
+
if (!sourceEdPrivateKey || !sourceXPrivateKey) {
|
|
34
|
+
throw new Error("Device keypair missing from local keychain. Re-run setup on this device.");
|
|
35
|
+
}
|
|
36
|
+
const amk = vaultSession.getAMK();
|
|
37
|
+
let approved = 0;
|
|
38
|
+
let skipped = 0;
|
|
39
|
+
const approvedGrantIds = [];
|
|
40
|
+
for (const grant of pending) {
|
|
41
|
+
if (!grant.target_device_kex_public_key_b64) {
|
|
42
|
+
console.log(`Skipping grant ${grant.grant_id}: target device public key is missing from the API response.`);
|
|
43
|
+
skipped += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const { payloadBytes, signature } = createDeviceGrant({
|
|
47
|
+
sourceEdPrivateKey,
|
|
48
|
+
sourceXPrivateKey,
|
|
49
|
+
sourceDeviceId: state.device_id,
|
|
50
|
+
targetDeviceId: grant.target_device_id,
|
|
51
|
+
targetKexPublicKey: fromB64(grant.target_device_kex_public_key_b64),
|
|
52
|
+
amk,
|
|
53
|
+
});
|
|
54
|
+
await approveGrant(client, grant.grant_id, {
|
|
55
|
+
encrypted_grant_payload_b64: toB64(payloadBytes),
|
|
56
|
+
signature: {
|
|
57
|
+
signature_algorithm: "ed25519",
|
|
58
|
+
signature_b64: toB64(signature),
|
|
59
|
+
},
|
|
60
|
+
payload_checksum_sha256: computeSha256Hex(payloadBytes),
|
|
61
|
+
});
|
|
62
|
+
console.log(`Approved grant ${grant.grant_id} for ${grant.target_device_name ?? grant.target_device_id}.`);
|
|
63
|
+
approved += 1;
|
|
64
|
+
approvedGrantIds.push(grant.grant_id);
|
|
65
|
+
}
|
|
66
|
+
console.log(`Grant approval complete. approved=${approved} skipped=${skipped}`);
|
|
67
|
+
return {
|
|
68
|
+
approved,
|
|
69
|
+
skipped,
|
|
70
|
+
total: pending.length,
|
|
71
|
+
approvedGrantIds,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function approvePendingGrantsAction() {
|
|
75
|
+
await approvePendingGrants();
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { MemboxApiClient } from "../api/client.js";
|
|
2
|
+
import type { LocalState } from "../store/local-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* Create an authenticated API client using stored tokens.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createAuthenticatedClient(state: LocalState): Promise<MemboxApiClient>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MemboxApiClient } from "../api/client.js";
|
|
2
|
+
import { getString, storeString, ACCOUNTS } from "../store/keychain.js";
|
|
3
|
+
import { API_PREFIX } from "../constants.js";
|
|
4
|
+
let cachedAccessToken = null;
|
|
5
|
+
/**
|
|
6
|
+
* Create an authenticated API client using stored tokens.
|
|
7
|
+
*/
|
|
8
|
+
export async function createAuthenticatedClient(state) {
|
|
9
|
+
return new MemboxApiClient(state.server_url + API_PREFIX, async () => cachedAccessToken, async () => getString(ACCOUNTS.REFRESH_TOKEN), async (access, refresh) => {
|
|
10
|
+
cachedAccessToken = access;
|
|
11
|
+
await storeString(ACCOUNTS.REFRESH_TOKEN, refresh);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function readPassphraseFromNonTty(prompt: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<string>;
|
|
2
|
+
/**
|
|
3
|
+
* Read a passphrase from stdin with echo disabled.
|
|
4
|
+
* Uses raw mode to suppress character display.
|
|
5
|
+
* NEVER reads from command arguments or environment variables.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readPassphrase(prompt: string): Promise<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Read passphrase with confirmation (must enter twice).
|
|
10
|
+
*/
|
|
11
|
+
export declare function readPassphraseWithConfirm(prompt: string): Promise<string>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
const nonTtyReaders = new WeakMap();
|
|
3
|
+
function getNonTtyReader(input) {
|
|
4
|
+
const existing = nonTtyReaders.get(input);
|
|
5
|
+
if (existing) {
|
|
6
|
+
return existing;
|
|
7
|
+
}
|
|
8
|
+
const rl = createInterface({
|
|
9
|
+
input,
|
|
10
|
+
terminal: false,
|
|
11
|
+
crlfDelay: Infinity,
|
|
12
|
+
});
|
|
13
|
+
const state = {
|
|
14
|
+
rl,
|
|
15
|
+
iterator: rl[Symbol.asyncIterator](),
|
|
16
|
+
};
|
|
17
|
+
nonTtyReaders.set(input, state);
|
|
18
|
+
return state;
|
|
19
|
+
}
|
|
20
|
+
export async function readPassphraseFromNonTty(prompt, input = process.stdin, output = process.stderr) {
|
|
21
|
+
output.write(prompt);
|
|
22
|
+
const { rl, iterator } = getNonTtyReader(input);
|
|
23
|
+
const { value, done } = await iterator.next();
|
|
24
|
+
if (done || value == null) {
|
|
25
|
+
rl.close();
|
|
26
|
+
nonTtyReaders.delete(input);
|
|
27
|
+
throw new Error("Cancelled");
|
|
28
|
+
}
|
|
29
|
+
output.write("\n");
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read a passphrase from stdin with echo disabled.
|
|
34
|
+
* Uses raw mode to suppress character display.
|
|
35
|
+
* NEVER reads from command arguments or environment variables.
|
|
36
|
+
*/
|
|
37
|
+
export function readPassphrase(prompt) {
|
|
38
|
+
if (!process.stdin.isTTY) {
|
|
39
|
+
return readPassphraseFromNonTty(prompt);
|
|
40
|
+
}
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const rl = createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stderr,
|
|
45
|
+
terminal: true,
|
|
46
|
+
});
|
|
47
|
+
process.stderr.write(prompt);
|
|
48
|
+
const stdin = process.stdin;
|
|
49
|
+
const wasRaw = stdin.isRaw;
|
|
50
|
+
if (stdin.isTTY) {
|
|
51
|
+
stdin.setRawMode(true);
|
|
52
|
+
}
|
|
53
|
+
let input = "";
|
|
54
|
+
const onData = (ch) => {
|
|
55
|
+
const c = ch.toString();
|
|
56
|
+
if (c === "\n" || c === "\r") {
|
|
57
|
+
if (stdin.isTTY)
|
|
58
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
59
|
+
stdin.removeListener("data", onData);
|
|
60
|
+
process.stderr.write("\n");
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(input);
|
|
63
|
+
}
|
|
64
|
+
else if (c === "\u0003") {
|
|
65
|
+
// Ctrl+C
|
|
66
|
+
if (stdin.isTTY)
|
|
67
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
68
|
+
stdin.removeListener("data", onData);
|
|
69
|
+
rl.close();
|
|
70
|
+
reject(new Error("Cancelled"));
|
|
71
|
+
}
|
|
72
|
+
else if (c === "\u007f" || c === "\b") {
|
|
73
|
+
// Backspace
|
|
74
|
+
input = input.slice(0, -1);
|
|
75
|
+
}
|
|
76
|
+
else if (c.charCodeAt(0) >= 32) {
|
|
77
|
+
// Printable character
|
|
78
|
+
input += c;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
stdin.on("data", onData);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read passphrase with confirmation (must enter twice).
|
|
86
|
+
*/
|
|
87
|
+
export async function readPassphraseWithConfirm(prompt) {
|
|
88
|
+
const p1 = await readPassphrase(prompt);
|
|
89
|
+
const p2 = await readPassphrase("Confirm passphrase: ");
|
|
90
|
+
if (p1 !== p2) {
|
|
91
|
+
throw new Error("Passphrases do not match.");
|
|
92
|
+
}
|
|
93
|
+
return p1;
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readState, writeState } from "../store/local-state.js";
|
|
2
|
+
export async function pauseAction() {
|
|
3
|
+
const state = await readState();
|
|
4
|
+
if (!state?.setup_complete) {
|
|
5
|
+
console.log("Vault not set up. Run `openclaw membox setup` first.");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (state.sync_paused) {
|
|
9
|
+
console.log("Sync is already paused.");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
state.sync_paused = true;
|
|
13
|
+
await writeState(state);
|
|
14
|
+
console.log("Auto-sync paused. Run `openclaw membox resume` to resume.");
|
|
15
|
+
}
|
|
16
|
+
export async function resumeAction() {
|
|
17
|
+
const state = await readState();
|
|
18
|
+
if (!state?.setup_complete) {
|
|
19
|
+
console.log("Vault not set up. Run `openclaw membox setup` first.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!state.sync_paused) {
|
|
23
|
+
console.log("Sync is not paused.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
state.sync_paused = false;
|
|
27
|
+
await writeState(state);
|
|
28
|
+
console.log("Auto-sync resumed.");
|
|
29
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type AuthorizedDeviceContext } from "./bootstrap.js";
|
|
2
|
+
export declare function rollbackIncompleteProvisioning(auth: Pick<AuthorizedDeviceContext, "client">): Promise<void>;
|
|
3
|
+
export declare function runWithProvisioningRollback<T>(auth: Pick<AuthorizedDeviceContext, "client">, fn: (markProvisioned: () => void) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { revokeCurrentDevice } from "../api/devices.js";
|
|
2
|
+
import { clearPendingSetupArtifacts, } from "./bootstrap.js";
|
|
3
|
+
import { clearStoredSecrets } from "../store/keychain.js";
|
|
4
|
+
import { deleteState } from "../store/local-state.js";
|
|
5
|
+
import { vaultSession } from "../store/session.js";
|
|
6
|
+
export async function rollbackIncompleteProvisioning(auth) {
|
|
7
|
+
vaultSession.lock();
|
|
8
|
+
try {
|
|
9
|
+
await revokeCurrentDevice(auth.client);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
console.warn("[membox] Failed to revoke incomplete device authorization:", error);
|
|
13
|
+
}
|
|
14
|
+
await clearStoredSecrets();
|
|
15
|
+
await clearPendingSetupArtifacts();
|
|
16
|
+
await deleteState();
|
|
17
|
+
}
|
|
18
|
+
export async function runWithProvisioningRollback(auth, fn) {
|
|
19
|
+
let provisioned = false;
|
|
20
|
+
try {
|
|
21
|
+
return await fn(() => {
|
|
22
|
+
provisioned = true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (!provisioned) {
|
|
27
|
+
await rollbackIncompleteProvisioning(auth);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ConflictStrategy } from "../sync/conflict.js";
|
|
2
|
+
export type PullActionResult = {
|
|
3
|
+
status: "locked";
|
|
4
|
+
message: string;
|
|
5
|
+
} | {
|
|
6
|
+
status: "up_to_date";
|
|
7
|
+
cursor: number;
|
|
8
|
+
} | {
|
|
9
|
+
status: "preview";
|
|
10
|
+
cursor: number;
|
|
11
|
+
strategy: ConflictStrategy;
|
|
12
|
+
toDownload: Array<{
|
|
13
|
+
logicalPath: string;
|
|
14
|
+
objectId: string;
|
|
15
|
+
version: number;
|
|
16
|
+
changeType: "upsert" | "delete";
|
|
17
|
+
}>;
|
|
18
|
+
conflicts: Array<{
|
|
19
|
+
logicalPath: string;
|
|
20
|
+
objectId: string;
|
|
21
|
+
}>;
|
|
22
|
+
} | {
|
|
23
|
+
status: "ok";
|
|
24
|
+
cursor: number;
|
|
25
|
+
downloaded: number;
|
|
26
|
+
conflictsResolved: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function pullAction(options?: {
|
|
29
|
+
preview?: boolean;
|
|
30
|
+
onConflict?: ConflictStrategy;
|
|
31
|
+
}): Promise<PullActionResult>;
|