@membox-cloud/membox 0.1.1 → 0.1.5
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 +15 -0
- package/dist/src/api/client.d.ts +3 -1
- package/dist/src/api/client.js +37 -9
- package/dist/src/api/device-flow.js +17 -2
- package/dist/src/cli/bootstrap.js +13 -0
- package/dist/src/cli/setup.js +44 -20
- package/dist/src/constants.d.ts +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/debug-logger.d.ts +7 -2
- package/dist/src/debug-logger.js +22 -4
- package/dist/src/store/pending-setup-secrets.js +3 -0
- package/dist/src/store/pending-setup.js +6 -1
- package/dist/src/sync/downloader.js +4 -0
- package/dist/src/sync/uploader.js +5 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,6 +146,21 @@ The plugin's `serverUrl` is environment-driven in local development. Source `scr
|
|
|
146
146
|
|
|
147
147
|
For local verification, all plugin debug logs can be redirected into the repository `./logs/` directory by exporting `MEMBOX_LOG_FILE` before starting OpenClaw.
|
|
148
148
|
|
|
149
|
+
For end-to-end diagnostics across plugin + API:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
export MEMBOX_LOG_FILE="$PWD/logs/plugin-debug.log"
|
|
153
|
+
export MEMBOX_SERVER_LOG_FILE="$PWD/logs/api-debug.log"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
When a repro is complete, collect a safe debug bundle with:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
bash scripts/collect-membox-debug.sh
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The bundle includes safe state snapshots, plugin/API logs, and OpenClaw command output. It intentionally excludes raw refresh tokens, device private keys, and vault secret bundles.
|
|
163
|
+
|
|
149
164
|
For a full manual validation flow, see `./MANUAL-DUAL-DEVICE-CHECKLIST.md`.
|
|
150
165
|
|
|
151
166
|
For an agent-first install + pairing flow, see `./AGENT-WORKFLOW.md` and run `bash scripts/install-membox-agent-stack.sh` from the repository root.
|
package/dist/src/api/client.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare class ApiError extends Error {
|
|
2
2
|
status: number;
|
|
3
3
|
code: string;
|
|
4
|
-
|
|
4
|
+
requestId?: string | null | undefined;
|
|
5
|
+
bodySnippet?: string | null | undefined;
|
|
6
|
+
constructor(status: number, code: string, message: string, requestId?: string | null | undefined, bodySnippet?: string | null | undefined);
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Typed HTTP client for the Membox API.
|
package/dist/src/api/client.js
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { debugLog } from "../debug-logger.js";
|
|
2
3
|
export class ApiError extends Error {
|
|
3
4
|
status;
|
|
4
5
|
code;
|
|
5
|
-
|
|
6
|
+
requestId;
|
|
7
|
+
bodySnippet;
|
|
8
|
+
constructor(status, code, message, requestId, bodySnippet) {
|
|
6
9
|
super(message);
|
|
7
10
|
this.status = status;
|
|
8
11
|
this.code = code;
|
|
12
|
+
this.requestId = requestId;
|
|
13
|
+
this.bodySnippet = bodySnippet;
|
|
9
14
|
this.name = "ApiError";
|
|
10
15
|
}
|
|
11
16
|
}
|
|
17
|
+
function parseErrorBody(bodyText) {
|
|
18
|
+
if (!bodyText)
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(bodyText);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
12
27
|
/**
|
|
13
28
|
* Typed HTTP client for the Membox API.
|
|
14
29
|
* Handles auth headers and automatic token refresh on 401.
|
|
@@ -26,14 +41,16 @@ export class MemboxApiClient {
|
|
|
26
41
|
}
|
|
27
42
|
async request(method, path, body, retry = true) {
|
|
28
43
|
const url = `${this.baseUrl}${path}`;
|
|
44
|
+
const requestId = randomUUID();
|
|
29
45
|
const headers = {
|
|
30
46
|
"Content-Type": "application/json",
|
|
47
|
+
"X-Request-Id": requestId,
|
|
31
48
|
};
|
|
32
49
|
const token = await this.getAccessToken();
|
|
33
50
|
if (token) {
|
|
34
51
|
headers["Authorization"] = `Bearer ${token}`;
|
|
35
52
|
}
|
|
36
|
-
debugLog.
|
|
53
|
+
debugLog.apiRequestWithId(method, url, requestId);
|
|
37
54
|
const t0 = Date.now();
|
|
38
55
|
let res;
|
|
39
56
|
try {
|
|
@@ -47,7 +64,8 @@ export class MemboxApiClient {
|
|
|
47
64
|
debugLog.apiError(method, url, err);
|
|
48
65
|
throw err;
|
|
49
66
|
}
|
|
50
|
-
|
|
67
|
+
const responseRequestId = res.headers.get("x-request-id") ?? requestId;
|
|
68
|
+
debugLog.apiResponse(method, url, res.status, Date.now() - t0, responseRequestId);
|
|
51
69
|
if (res.status === 401 && retry) {
|
|
52
70
|
debugLog.info("api-client", "Got 401, attempting token refresh...");
|
|
53
71
|
const refreshed = await this.tryRefresh();
|
|
@@ -56,9 +74,15 @@ export class MemboxApiClient {
|
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
76
|
if (!res.ok) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
77
|
+
const bodyText = await res.text().catch(() => "");
|
|
78
|
+
const err = parseErrorBody(bodyText);
|
|
79
|
+
const apiRequestId = err?.error?.request_id ?? responseRequestId;
|
|
80
|
+
const message = err?.error?.message ?? (bodyText || res.statusText);
|
|
81
|
+
const apiErr = new ApiError(res.status, err?.error?.code ?? "unknown", message, apiRequestId, bodyText || null);
|
|
82
|
+
debugLog.apiError(method, url, apiErr, {
|
|
83
|
+
requestId: apiRequestId,
|
|
84
|
+
bodySnippet: bodyText || null,
|
|
85
|
+
});
|
|
62
86
|
throw apiErr;
|
|
63
87
|
}
|
|
64
88
|
// Handle 204 No Content
|
|
@@ -74,15 +98,19 @@ export class MemboxApiClient {
|
|
|
74
98
|
return false;
|
|
75
99
|
}
|
|
76
100
|
const url = `${this.baseUrl}/auth/token/refresh`;
|
|
101
|
+
const requestId = randomUUID();
|
|
77
102
|
try {
|
|
78
|
-
debugLog.
|
|
103
|
+
debugLog.apiRequestWithId("POST", url, requestId);
|
|
79
104
|
const t0 = Date.now();
|
|
80
105
|
const res = await fetch(url, {
|
|
81
106
|
method: "POST",
|
|
82
|
-
headers: {
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/json",
|
|
109
|
+
"X-Request-Id": requestId,
|
|
110
|
+
},
|
|
83
111
|
body: JSON.stringify({ refresh_token: rt }),
|
|
84
112
|
});
|
|
85
|
-
debugLog.apiResponse("POST", url, res.status, Date.now() - t0);
|
|
113
|
+
debugLog.apiResponse("POST", url, res.status, Date.now() - t0, res.headers.get("x-request-id") ?? requestId);
|
|
86
114
|
if (!res.ok)
|
|
87
115
|
return false;
|
|
88
116
|
const data = (await res.json());
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { debugLog } from "../debug-logger.js";
|
|
2
|
+
function shortCode(value) {
|
|
3
|
+
return value.length <= 16 ? value : `${value.slice(0, 12)}...`;
|
|
4
|
+
}
|
|
1
5
|
export async function startDeviceFlow(client, params) {
|
|
2
|
-
|
|
6
|
+
const result = await client.post("/device/start", params);
|
|
7
|
+
debugLog.state("setup", `device-start user_code=${result.user_code} device_code=${shortCode(result.device_code)} expires_in=${result.expires_in}s`);
|
|
8
|
+
return result;
|
|
3
9
|
}
|
|
4
10
|
export async function pollDeviceFlowOnce(client, deviceCode) {
|
|
5
|
-
|
|
11
|
+
const result = await client.get(`/device/poll/${deviceCode}`);
|
|
12
|
+
debugLog.state("setup", `device-poll device_code=${shortCode(deviceCode)} status=${result.status}`);
|
|
13
|
+
return result;
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Poll the device code endpoint until authorization is granted, denied, or expired.
|
|
@@ -22,6 +30,7 @@ export async function pollDeviceFlow(client, deviceCode, interval, expiresIn) {
|
|
|
22
30
|
}
|
|
23
31
|
catch (err) {
|
|
24
32
|
transientErrors++;
|
|
33
|
+
debugLog.warn("setup", `device-poll-error device_code=${shortCode(deviceCode)} consecutive_errors=${transientErrors} error=${err instanceof Error ? err.message : String(err)}`);
|
|
25
34
|
if (transientErrors >= MAX_TRANSIENT_ERRORS) {
|
|
26
35
|
throw new Error(`Device code polling failed after ${MAX_TRANSIENT_ERRORS} consecutive errors: ${err instanceof Error ? err.message : String(err)}`);
|
|
27
36
|
}
|
|
@@ -30,6 +39,7 @@ export async function pollDeviceFlow(client, deviceCode, interval, expiresIn) {
|
|
|
30
39
|
}
|
|
31
40
|
switch (res.status) {
|
|
32
41
|
case "access_granted":
|
|
42
|
+
debugLog.info("setup", `device-poll-complete device_code=${shortCode(deviceCode)} access_granted`);
|
|
33
43
|
return {
|
|
34
44
|
access_token: res.access_token,
|
|
35
45
|
refresh_token: res.refresh_token,
|
|
@@ -38,16 +48,21 @@ export async function pollDeviceFlow(client, deviceCode, interval, expiresIn) {
|
|
|
38
48
|
device_id: res.device_id,
|
|
39
49
|
};
|
|
40
50
|
case "slow_down":
|
|
51
|
+
debugLog.info("setup", `device-poll-slow-down device_code=${shortCode(deviceCode)} next_wait_ms=${Math.min(waitMs + 1000, 10000)}`);
|
|
41
52
|
waitMs = Math.min(waitMs + 1000, 10000);
|
|
42
53
|
break;
|
|
43
54
|
case "expired_token":
|
|
55
|
+
debugLog.warn("setup", `device-poll-expired device_code=${shortCode(deviceCode)}`);
|
|
44
56
|
throw new Error("Device code expired. Please try again.");
|
|
45
57
|
case "denied":
|
|
58
|
+
debugLog.warn("setup", `device-poll-denied device_code=${shortCode(deviceCode)}`);
|
|
46
59
|
throw new Error("Device authorization was denied.");
|
|
47
60
|
case "authorization_pending":
|
|
61
|
+
debugLog.info("setup", `device-poll-pending device_code=${shortCode(deviceCode)} wait_ms=${waitMs}`);
|
|
48
62
|
break;
|
|
49
63
|
}
|
|
50
64
|
}
|
|
65
|
+
debugLog.error("setup", `device-poll-timeout device_code=${shortCode(deviceCode)}`);
|
|
51
66
|
throw new Error("Device code flow timed out.");
|
|
52
67
|
}
|
|
53
68
|
function sleep(ms) {
|
|
@@ -5,6 +5,7 @@ import { generateDeviceKeyPair, } from "../crypto/device-keys.js";
|
|
|
5
5
|
import { getAccountMe } from "../api/account.js";
|
|
6
6
|
import { MemboxApiClient } from "../api/client.js";
|
|
7
7
|
import { pollDeviceFlow, pollDeviceFlowOnce, startDeviceFlow, } from "../api/device-flow.js";
|
|
8
|
+
import { debugLog } from "../debug-logger.js";
|
|
8
9
|
import { clearPendingSetupSecrets, } from "../store/keychain.js";
|
|
9
10
|
import { clearPendingSetupSecretsFile, getPendingSetupRefreshToken, loadPendingSetupDeviceKeys as loadPendingSetupDeviceKeysFile, storePendingSetupDeviceKeys as storePendingSetupDeviceKeysFile, storePendingSetupRefreshToken, } from "../store/pending-setup-secrets.js";
|
|
10
11
|
import { deletePendingSetup, isPendingSetupExpired, readPendingSetup, writePendingSetup, } from "../store/pending-setup.js";
|
|
@@ -78,12 +79,15 @@ export async function clearPendingSetupArtifacts() {
|
|
|
78
79
|
async function loadActivePendingSetup(cfg) {
|
|
79
80
|
const pendingSetup = await readPendingSetup();
|
|
80
81
|
if (!pendingSetup) {
|
|
82
|
+
debugLog.state("setup", "pending-setup-none");
|
|
81
83
|
return null;
|
|
82
84
|
}
|
|
83
85
|
if (pendingSetup.server_url !== cfg.serverUrl || isPendingSetupExpired(pendingSetup)) {
|
|
86
|
+
debugLog.warn("setup", `pending-setup-stale server_url=${pendingSetup.server_url} expected=${cfg.serverUrl} expired=${isPendingSetupExpired(pendingSetup)}`);
|
|
84
87
|
await clearPendingSetupArtifacts();
|
|
85
88
|
return null;
|
|
86
89
|
}
|
|
90
|
+
debugLog.info("setup", `pending-setup-active status=${pendingSetup.status} user_code=${pendingSetup.user_code}`);
|
|
87
91
|
return pendingSetup;
|
|
88
92
|
}
|
|
89
93
|
async function createPendingSetup(cfg) {
|
|
@@ -124,6 +128,7 @@ async function createPendingSetup(cfg) {
|
|
|
124
128
|
catch (error) {
|
|
125
129
|
throw new Error(`Failed to persist pending setup state on disk: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
130
|
}
|
|
131
|
+
debugLog.info("setup", `pending-setup-created user_code=${pendingSetup.user_code} device_name=${deviceName} platform=${platform}`);
|
|
127
132
|
return pendingSetup;
|
|
128
133
|
}
|
|
129
134
|
export async function ensurePendingDeviceAuthorization(cfg) {
|
|
@@ -203,6 +208,7 @@ export async function pollPendingDeviceAuthorizationOnce(cfg) {
|
|
|
203
208
|
};
|
|
204
209
|
}
|
|
205
210
|
await clearPendingSetupArtifacts();
|
|
211
|
+
debugLog.warn("setup", `pending-setup-terminated status=${result.status} device_code=${pendingSetup.device_code.slice(0, 12)}...`);
|
|
206
212
|
return {
|
|
207
213
|
status: result.status === "denied" ? "denied" : "expired",
|
|
208
214
|
pendingSetup: null,
|
|
@@ -247,6 +253,7 @@ async function buildAuthorizedContextFromPending(cfg, pendingSetup, options) {
|
|
|
247
253
|
function printAuthorizationPrompt(pendingSetup) {
|
|
248
254
|
console.log(`\n Your code: ${pendingSetup.user_code}`);
|
|
249
255
|
console.log(` Open: ${pendingSetup.verification_uri_complete}\n`);
|
|
256
|
+
debugLog.info("setup", `authorization-prompt user_code=${pendingSetup.user_code} url=${pendingSetup.verification_uri_complete}`);
|
|
250
257
|
}
|
|
251
258
|
async function waitForPendingAuthorization(cfg, pendingSetup) {
|
|
252
259
|
const client = createAnonymousClient(pendingSetupBaseUrl(cfg));
|
|
@@ -257,6 +264,7 @@ async function waitForPendingAuthorization(cfg, pendingSetup) {
|
|
|
257
264
|
const finalized = await finalizePendingAuthorization(cfg, pendingSetup, tokens);
|
|
258
265
|
console.log("Authorized!");
|
|
259
266
|
console.log(`Logged in as: ${describeAccount(finalized.account)}`);
|
|
267
|
+
debugLog.info("setup", `authorization-complete user_code=${finalized.pendingSetup.user_code} account=${describeAccount(finalized.account)}`);
|
|
260
268
|
return buildAuthorizedContextFromPending(cfg, finalized.pendingSetup, {
|
|
261
269
|
accessToken: tokens.access_token,
|
|
262
270
|
refreshToken: tokens.refresh_token,
|
|
@@ -267,20 +275,25 @@ export async function authorizeDevice(cfg) {
|
|
|
267
275
|
const { pendingSetup, created } = await ensurePendingDeviceAuthorization(cfg);
|
|
268
276
|
if (pendingSetup.status === "authorized") {
|
|
269
277
|
console.log("Reusing previously completed browser authorization.");
|
|
278
|
+
debugLog.info("setup", `authorization-reuse user_code=${pendingSetup.user_code} device_id=${pendingSetup.device_id ?? "unknown"}`);
|
|
270
279
|
return buildAuthorizedContextFromPending(cfg, pendingSetup);
|
|
271
280
|
}
|
|
272
281
|
if (created) {
|
|
273
282
|
console.log("Starting device authorization...");
|
|
283
|
+
debugLog.info("setup", "authorization-start");
|
|
274
284
|
}
|
|
275
285
|
else {
|
|
276
286
|
console.log("Resuming pending device authorization...");
|
|
287
|
+
debugLog.info("setup", `authorization-resume user_code=${pendingSetup.user_code}`);
|
|
277
288
|
}
|
|
278
289
|
printAuthorizationPrompt(pendingSetup);
|
|
279
290
|
const browserOpened = tryOpenUrl(pendingSetup.verification_uri_complete);
|
|
280
291
|
if (!browserOpened) {
|
|
281
292
|
console.log(" No local browser was opened. If this machine is headless, open that URL in any trusted browser and sign in there.\n");
|
|
293
|
+
debugLog.warn("setup", "authorization-browser-open-skipped");
|
|
282
294
|
}
|
|
283
295
|
console.log("Waiting for authorization...");
|
|
296
|
+
debugLog.info("setup", "authorization-waiting");
|
|
284
297
|
return waitForPendingAuthorization(cfg, pendingSetup);
|
|
285
298
|
}
|
|
286
299
|
export async function persistVaultSecrets(params) {
|
package/dist/src/cli/setup.js
CHANGED
|
@@ -17,6 +17,7 @@ import { pullAction } from "./pull.js";
|
|
|
17
17
|
import { authorizeDevice, clearPendingSetupArtifacts, fromB64, persistVaultSecrets, toB64, } from "./bootstrap.js";
|
|
18
18
|
import { runWithProvisioningRollback } from "./provisioning.js";
|
|
19
19
|
import { loadVaultSecretsWithPassphrase } from "../store/vault-secrets.js";
|
|
20
|
+
import { debugLog } from "../debug-logger.js";
|
|
20
21
|
export function selectGrantCapableDevices(devices, currentDeviceId) {
|
|
21
22
|
return devices.filter((device) => device.device_id !== currentDeviceId &&
|
|
22
23
|
device.status === "active" &&
|
|
@@ -31,6 +32,7 @@ async function receiveApprovedGrant(params) {
|
|
|
31
32
|
console.log("Trusted device approval required.");
|
|
32
33
|
console.log("On an existing unlocked device, run `openclaw membox grants approve-pending`.");
|
|
33
34
|
console.log("Waiting for device grant approval...");
|
|
35
|
+
debugLog.info("setup", `grant-wait-start target_device_id=${params.deviceId}`);
|
|
34
36
|
while (Date.now() < deadline) {
|
|
35
37
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
36
38
|
const grant = await getGrant(params.client, grantRequest.grant_id);
|
|
@@ -38,9 +40,11 @@ async function receiveApprovedGrant(params) {
|
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
42
|
if (grant.status === "rejected") {
|
|
43
|
+
debugLog.warn("setup", `grant-wait-rejected grant_id=${grantRequest.grant_id}`);
|
|
41
44
|
throw new Error("Trusted-device approval was rejected.");
|
|
42
45
|
}
|
|
43
46
|
if (grant.status === "expired") {
|
|
47
|
+
debugLog.warn("setup", `grant-wait-expired grant_id=${grantRequest.grant_id}`);
|
|
44
48
|
throw new Error("Trusted-device approval expired before it was approved.");
|
|
45
49
|
}
|
|
46
50
|
if (grant.status !== "approved") {
|
|
@@ -70,6 +74,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
70
74
|
const otherGrantCapableDevices = selectGrantCapableDevices(devices, auth.tokens.device_id);
|
|
71
75
|
let amk = null;
|
|
72
76
|
let initialMode = "upload";
|
|
77
|
+
debugLog.info("setup", `finish-authorized-setup-start device_id=${auth.tokens.device_id} grant_capable_devices=${otherGrantCapableDevices.length} recovery_bundle=${Boolean(recoveryStatus?.has_recovery_bundle)} sync_object_count=${syncStatus?.object_count ?? 0}`);
|
|
73
78
|
try {
|
|
74
79
|
if (otherGrantCapableDevices.length > 0) {
|
|
75
80
|
amk = await receiveApprovedGrant({
|
|
@@ -78,6 +83,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
78
83
|
deviceKeys: auth.deviceKeys,
|
|
79
84
|
});
|
|
80
85
|
initialMode = "pull";
|
|
86
|
+
debugLog.info("setup", "finish-authorized-setup-mode pull");
|
|
81
87
|
}
|
|
82
88
|
else if (recoveryStatus?.has_recovery_bundle) {
|
|
83
89
|
console.log("Recovery materials already exist for this account, but no active trusted device is available.");
|
|
@@ -92,6 +98,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
92
98
|
else {
|
|
93
99
|
console.log("No existing vault detected. Creating a new account master key.");
|
|
94
100
|
amk = generateAMK();
|
|
101
|
+
debugLog.info("setup", "finish-authorized-setup-mode upload");
|
|
95
102
|
}
|
|
96
103
|
await persistVaultSecrets({
|
|
97
104
|
deviceKeys: auth.deviceKeys,
|
|
@@ -117,6 +124,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
117
124
|
});
|
|
118
125
|
await params.onRecoveryCode(recoveryCode);
|
|
119
126
|
recoveryCodeGenerated = true;
|
|
127
|
+
debugLog.info("setup", "recovery-material-generated");
|
|
120
128
|
}
|
|
121
129
|
const state = createInitialState({
|
|
122
130
|
serverUrl: cfg.serverUrl,
|
|
@@ -130,6 +138,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
130
138
|
await writeState(state);
|
|
131
139
|
if (params.enableManagedUnlock) {
|
|
132
140
|
await enableManagedUnlock(params.passphrase);
|
|
141
|
+
debugLog.info("setup", "managed-unlock-enabled");
|
|
133
142
|
}
|
|
134
143
|
await clearPendingSetupArtifacts();
|
|
135
144
|
const loadedSecrets = await loadVaultSecretsWithPassphrase(params.passphrase);
|
|
@@ -148,6 +157,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
148
157
|
const pullResult = await pullAction({ onConflict: "conflict-copy" });
|
|
149
158
|
const downloadedFiles = pullResult.status === "ok" ? pullResult.downloaded : 0;
|
|
150
159
|
console.log("Setup complete! Existing vault material is now available.");
|
|
160
|
+
debugLog.info("setup", `finish-authorized-setup-complete mode=pull downloaded=${downloadedFiles}`);
|
|
151
161
|
return {
|
|
152
162
|
initialMode,
|
|
153
163
|
uploadedFiles: [],
|
|
@@ -178,6 +188,7 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
178
188
|
uploadedFiles.push(path);
|
|
179
189
|
}
|
|
180
190
|
console.log("Setup complete! Vault is ready.");
|
|
191
|
+
debugLog.info("setup", `finish-authorized-setup-complete mode=upload uploaded=${uploadedFiles.length}`);
|
|
181
192
|
return {
|
|
182
193
|
initialMode,
|
|
183
194
|
uploadedFiles,
|
|
@@ -191,30 +202,43 @@ export async function finishAuthorizedSetup(cfg, auth, params) {
|
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
export async function setupAction(cfg) {
|
|
205
|
+
const startedAt = Date.now();
|
|
206
|
+
debugLog.cliCommand("membox setup");
|
|
194
207
|
const existing = await readState();
|
|
195
208
|
if (existing?.setup_complete) {
|
|
196
209
|
console.log("Vault already set up. Use `openclaw membox status` to check.");
|
|
210
|
+
debugLog.cliCommandEnd("membox setup", true, Date.now() - startedAt);
|
|
197
211
|
return;
|
|
198
212
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
passphrase
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
try {
|
|
214
|
+
const auth = await authorizeDevice(cfg);
|
|
215
|
+
await runWithProvisioningRollback(auth, async (markProvisioned) => {
|
|
216
|
+
console.log("\nSet your vault passphrase (this encrypts your memory locally):");
|
|
217
|
+
console.log("Warning: This passphrase is NEVER sent to the server. If lost, use recovery materials.");
|
|
218
|
+
debugLog.info("setup", "passphrase-prompt-start");
|
|
219
|
+
const passphrase = await readPassphraseWithConfirm("Vault passphrase: ");
|
|
220
|
+
debugLog.info("setup", "passphrase-prompt-complete");
|
|
221
|
+
const result = await finishAuthorizedSetup(cfg, auth, {
|
|
222
|
+
passphrase,
|
|
223
|
+
onRecoveryCode: async (recoveryCode) => {
|
|
224
|
+
console.log("\n========================================");
|
|
225
|
+
console.log(" SAVE YOUR RECOVERY CODE NOW!");
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log(` ${recoveryCode}`);
|
|
228
|
+
console.log("");
|
|
229
|
+
console.log(" Store it in a password manager or print.");
|
|
230
|
+
console.log(" Without it + passphrase, data is lost.");
|
|
231
|
+
console.log("========================================\n");
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
markProvisioned();
|
|
235
|
+
return result;
|
|
216
236
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
debugLog.cliCommandEnd("membox setup", true, Date.now() - startedAt);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
debugLog.error("setup", `setup-action-failed error=${error instanceof Error ? error.message : String(error)}`);
|
|
241
|
+
debugLog.cliCommandEnd("membox setup", false, Date.now() - startedAt);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
220
244
|
}
|
package/dist/src/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const PLUGIN_ID = "membox";
|
|
2
|
-
export declare const PLUGIN_VERSION = "0.1.
|
|
2
|
+
export declare const PLUGIN_VERSION = "0.1.5";
|
|
3
3
|
export declare const DEFAULT_SERVER_URL = "https://membox.cloud";
|
|
4
4
|
export declare const API_PREFIX = "/api/v1";
|
|
5
5
|
export declare const KEYCHAIN_SERVICE: string;
|
package/dist/src/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
export const PLUGIN_ID = "membox";
|
|
4
|
-
export const PLUGIN_VERSION = "0.1.
|
|
4
|
+
export const PLUGIN_VERSION = "0.1.5";
|
|
5
5
|
export const DEFAULT_SERVER_URL = "https://membox.cloud";
|
|
6
6
|
export const API_PREFIX = "/api/v1";
|
|
7
7
|
export const KEYCHAIN_SERVICE = process.env.MEMBOX_KEYCHAIN_SERVICE || "membox.cloud";
|
|
@@ -4,10 +4,14 @@ export declare const debugLog: {
|
|
|
4
4
|
error(component: string, msg: string): void;
|
|
5
5
|
/** Log an API request (never logs tokens or ciphertext). */
|
|
6
6
|
apiRequest(method: string, url: string): void;
|
|
7
|
+
apiRequestWithId(method: string, url: string, requestId: string): void;
|
|
7
8
|
/** Log an API response (status + timing). */
|
|
8
|
-
apiResponse(method: string, url: string, status: number, ms: number): void;
|
|
9
|
+
apiResponse(method: string, url: string, status: number, ms: number, requestId?: string | null): void;
|
|
9
10
|
/** Log an API error. */
|
|
10
|
-
apiError(method: string, url: string, error: unknown
|
|
11
|
+
apiError(method: string, url: string, error: unknown, params?: {
|
|
12
|
+
requestId?: string | null;
|
|
13
|
+
bodySnippet?: string | null;
|
|
14
|
+
}): void;
|
|
11
15
|
/** Log a crypto operation (never log key material). */
|
|
12
16
|
crypto(op: string, detail?: string): void;
|
|
13
17
|
/** Log a sync event. */
|
|
@@ -29,4 +33,5 @@ export declare const debugLog: {
|
|
|
29
33
|
/** Log CLI command execution. */
|
|
30
34
|
cliCommand(command: string): void;
|
|
31
35
|
cliCommandEnd(command: string, success: boolean, ms: number): void;
|
|
36
|
+
state(component: string, msg: string): void;
|
|
32
37
|
};
|
package/dist/src/debug-logger.js
CHANGED
|
@@ -16,6 +16,12 @@ function ensureDir() {
|
|
|
16
16
|
function ts() {
|
|
17
17
|
return new Date().toISOString();
|
|
18
18
|
}
|
|
19
|
+
function safeSnippet(value, max = 240) {
|
|
20
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
21
|
+
if (normalized.length <= max)
|
|
22
|
+
return normalized;
|
|
23
|
+
return `${normalized.slice(0, max - 1)}...`;
|
|
24
|
+
}
|
|
19
25
|
// Synchronous write is acceptable for debug/联调 use — keeps log ordering
|
|
20
26
|
// deterministic and avoids async complexity. Not intended for production.
|
|
21
27
|
function write(level, component, msg) {
|
|
@@ -46,16 +52,25 @@ export const debugLog = {
|
|
|
46
52
|
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
47
53
|
write("INFO", "api-client", `→ ${method} ${safeUrl}`);
|
|
48
54
|
},
|
|
55
|
+
apiRequestWithId(method, url, requestId) {
|
|
56
|
+
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
57
|
+
write("INFO", "api-client", `→ ${method} ${safeUrl} request_id=${requestId}`);
|
|
58
|
+
},
|
|
49
59
|
/** Log an API response (status + timing). */
|
|
50
|
-
apiResponse(method, url, status, ms) {
|
|
60
|
+
apiResponse(method, url, status, ms, requestId) {
|
|
51
61
|
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
52
|
-
|
|
62
|
+
const suffix = requestId ? ` request_id=${requestId}` : "";
|
|
63
|
+
write("INFO", "api-client", `← ${status} ${method} ${safeUrl} (${ms}ms)${suffix}`);
|
|
53
64
|
},
|
|
54
65
|
/** Log an API error. */
|
|
55
|
-
apiError(method, url, error) {
|
|
66
|
+
apiError(method, url, error, params) {
|
|
56
67
|
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
57
68
|
const msg = error instanceof Error ? error.message : String(error);
|
|
58
|
-
|
|
69
|
+
const requestId = params?.requestId ? ` request_id=${params.requestId}` : "";
|
|
70
|
+
const snippet = params?.bodySnippet
|
|
71
|
+
? ` body="${safeSnippet(params.bodySnippet)}"`
|
|
72
|
+
: "";
|
|
73
|
+
write("ERROR", "api-client", `✗ ${method} ${safeUrl}${requestId} — ${msg}${snippet}`);
|
|
59
74
|
},
|
|
60
75
|
/** Log a crypto operation (never log key material). */
|
|
61
76
|
crypto(op, detail) {
|
|
@@ -105,4 +120,7 @@ export const debugLog = {
|
|
|
105
120
|
const status = success ? "ok" : "failed";
|
|
106
121
|
write("INFO", "cli", `command-end: ${command} [${status}] (${ms}ms)`);
|
|
107
122
|
},
|
|
123
|
+
state(component, msg) {
|
|
124
|
+
write("INFO", component, msg);
|
|
125
|
+
},
|
|
108
126
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmod, mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { PENDING_SETUP_SECRETS_FILE, resolveStateRoot } from "../constants.js";
|
|
4
|
+
import { debugLog } from "../debug-logger.js";
|
|
4
5
|
function toB64(data) {
|
|
5
6
|
return Buffer.from(data).toString("base64");
|
|
6
7
|
}
|
|
@@ -26,6 +27,7 @@ async function writeFileState(state) {
|
|
|
26
27
|
await writeFile(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
27
28
|
await rename(tmp, path);
|
|
28
29
|
await chmod(path, 0o600).catch(() => { });
|
|
30
|
+
debugLog.state("state", `pending-setup-secrets-write refresh_token_present=${Boolean(state.refresh_token)}`);
|
|
29
31
|
}
|
|
30
32
|
export async function storePendingSetupDeviceKeys(deviceKeys) {
|
|
31
33
|
const existing = await readFileState();
|
|
@@ -70,4 +72,5 @@ export async function getPendingSetupRefreshToken() {
|
|
|
70
72
|
}
|
|
71
73
|
export async function clearPendingSetupSecretsFile() {
|
|
72
74
|
await unlink(secretsPath()).catch(() => { });
|
|
75
|
+
debugLog.state("state", "pending-setup-secrets-delete");
|
|
73
76
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { PENDING_SETUP_FILE, resolveStateRoot } from "../constants.js";
|
|
4
|
+
import { debugLog } from "../debug-logger.js";
|
|
4
5
|
function pendingSetupPath() {
|
|
5
6
|
return join(resolveStateRoot(), PENDING_SETUP_FILE);
|
|
6
7
|
}
|
|
7
8
|
export async function readPendingSetup() {
|
|
8
9
|
try {
|
|
9
10
|
const raw = await readFile(pendingSetupPath(), "utf-8");
|
|
10
|
-
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
debugLog.state("state", `pending-setup-read status=${parsed.status} user_code=${parsed.user_code} expires_at=${parsed.expires_at}`);
|
|
13
|
+
return parsed;
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
13
16
|
return null;
|
|
@@ -19,9 +22,11 @@ export async function writePendingSetup(state) {
|
|
|
19
22
|
const tmp = `${path}.tmp.${Date.now()}`;
|
|
20
23
|
await writeFile(tmp, JSON.stringify(state, null, 2));
|
|
21
24
|
await rename(tmp, path);
|
|
25
|
+
debugLog.state("state", `pending-setup-write status=${state.status} user_code=${state.user_code} expires_at=${state.expires_at}`);
|
|
22
26
|
}
|
|
23
27
|
export async function deletePendingSetup() {
|
|
24
28
|
await unlink(pendingSetupPath()).catch(() => { });
|
|
29
|
+
debugLog.state("state", "pending-setup-delete");
|
|
25
30
|
}
|
|
26
31
|
export function isPendingSetupExpired(state) {
|
|
27
32
|
if (state.status !== "pending") {
|
|
@@ -2,6 +2,7 @@ import { unwrapDEK } from "../crypto/keys.js";
|
|
|
2
2
|
import { decrypt } from "../crypto/aes-gcm.js";
|
|
3
3
|
import { computeSha256Hex, serializeAad } from "../crypto/manifest.js";
|
|
4
4
|
import { getManifest, downloadBlob } from "../api/sync.js";
|
|
5
|
+
import { debugLog } from "../debug-logger.js";
|
|
5
6
|
function fromB64(s) {
|
|
6
7
|
return new Uint8Array(Buffer.from(s, "base64"));
|
|
7
8
|
}
|
|
@@ -10,6 +11,7 @@ function fromB64(s) {
|
|
|
10
11
|
* Fetch manifest → download blobs → unwrap DEK → decrypt content → validate hash.
|
|
11
12
|
*/
|
|
12
13
|
export async function downloadAndDecrypt(params) {
|
|
14
|
+
debugLog.syncDownloadStart(params.objectId);
|
|
13
15
|
// Fetch manifest
|
|
14
16
|
const { manifest } = await getManifest(params.client, params.objectId, params.version);
|
|
15
17
|
const aadBytes = serializeAad(manifest.aad);
|
|
@@ -60,6 +62,7 @@ export async function downloadAndDecrypt(params) {
|
|
|
60
62
|
throw new Error(`Content hash mismatch: expected ${metadata.content_sha256}, got ${actualSha256}`);
|
|
61
63
|
}
|
|
62
64
|
}
|
|
65
|
+
debugLog.syncDownloadEnd(params.objectId, metadata.logical_path, manifest.object_version);
|
|
63
66
|
return {
|
|
64
67
|
logicalPath: metadata.logical_path,
|
|
65
68
|
content,
|
|
@@ -68,6 +71,7 @@ export async function downloadAndDecrypt(params) {
|
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
73
|
finally {
|
|
74
|
+
debugLog.cryptoDecryptDone(params.objectId);
|
|
71
75
|
dek.fill(0);
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -3,6 +3,7 @@ import { generateDEK, wrapDEK } from "../crypto/keys.js";
|
|
|
3
3
|
import { encrypt } from "../crypto/aes-gcm.js";
|
|
4
4
|
import { buildManifest, computeSha256Hex, serializeAad } from "../crypto/manifest.js";
|
|
5
5
|
import { uploadBlob, commitObject } from "../api/sync.js";
|
|
6
|
+
import { debugLog } from "../debug-logger.js";
|
|
6
7
|
function toB64(d) {
|
|
7
8
|
return Buffer.from(d).toString("base64");
|
|
8
9
|
}
|
|
@@ -14,6 +15,8 @@ export async function uploadFile(params) {
|
|
|
14
15
|
const objectId = params.objectId ?? randomUUID();
|
|
15
16
|
const version = (params.objectVersion ?? 0) + 1;
|
|
16
17
|
const dek = generateDEK();
|
|
18
|
+
debugLog.syncUploadStart(params.logicalPath, objectId);
|
|
19
|
+
debugLog.crypto("encrypt-start", `${params.logicalPath} v${version}`);
|
|
17
20
|
try {
|
|
18
21
|
const aad = {
|
|
19
22
|
user_id: params.userId,
|
|
@@ -78,6 +81,8 @@ export async function uploadFile(params) {
|
|
|
78
81
|
});
|
|
79
82
|
// Commit manifest
|
|
80
83
|
const commitResult = await commitObject(params.client, { manifest });
|
|
84
|
+
debugLog.cryptoEncryptDone(objectId);
|
|
85
|
+
debugLog.syncUploadEnd(params.logicalPath, objectId, commitResult.version);
|
|
81
86
|
return { objectId: commitResult.object_id, version: commitResult.version, seq: commitResult.seq };
|
|
82
87
|
}
|
|
83
88
|
finally {
|
package/openclaw.plugin.json
CHANGED