@membox-cloud/membox 0.1.4 → 0.1.6

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 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.
@@ -1,7 +1,9 @@
1
1
  export declare class ApiError extends Error {
2
2
  status: number;
3
3
  code: string;
4
- constructor(status: number, code: string, message: string);
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.
@@ -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
- constructor(status, code, message) {
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.apiRequest(method, url);
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
- debugLog.apiResponse(method, url, res.status, Date.now() - t0);
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 err = (await res.json().catch(() => null));
60
- const apiErr = new ApiError(res.status, err?.error?.code ?? "unknown", err?.error?.message ?? res.statusText);
61
- debugLog.apiError(method, url, apiErr);
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.apiRequest("POST", url);
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: { "Content-Type": "application/json" },
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
- return client.post("/device/start", params);
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
- return client.get(`/device/poll/${deviceCode}`);
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) {
@@ -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
- const auth = await authorizeDevice(cfg);
200
- await runWithProvisioningRollback(auth, async (markProvisioned) => {
201
- console.log("\nSet your vault passphrase (this encrypts your memory locally):");
202
- console.log("Warning: This passphrase is NEVER sent to the server. If lost, use recovery materials.");
203
- const passphrase = await readPassphraseWithConfirm("Vault passphrase: ");
204
- const result = await finishAuthorizedSetup(cfg, auth, {
205
- passphrase,
206
- onRecoveryCode: async (recoveryCode) => {
207
- console.log("\n========================================");
208
- console.log(" SAVE YOUR RECOVERY CODE NOW!");
209
- console.log("");
210
- console.log(` ${recoveryCode}`);
211
- console.log("");
212
- console.log(" Store it in a password manager or print.");
213
- console.log(" Without it + passphrase, data is lost.");
214
- console.log("========================================\n");
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
- markProvisioned();
218
- return result;
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
  }
@@ -1,5 +1,5 @@
1
1
  export declare const PLUGIN_ID = "membox";
2
- export declare const PLUGIN_VERSION = "0.1.0";
2
+ export declare const PLUGIN_VERSION = "0.1.6";
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;
@@ -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.0";
4
+ export const PLUGIN_VERSION = "0.1.6";
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): void;
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
  };
@@ -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
- write("INFO", "api-client", `← ${status} ${method} ${safeUrl} (${ms}ms)`);
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
- write("ERROR", "api-client", `✗ ${method} ${safeUrl} ${msg}`);
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
- return JSON.parse(raw);
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 {
@@ -2,7 +2,7 @@
2
2
  "id": "membox",
3
3
  "name": "Membox Vault",
4
4
  "description": "Zero-knowledge encrypted memory sync for OpenClaw",
5
- "version": "0.1.4",
5
+ "version": "0.1.6",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@membox-cloud/membox",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",