@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.
Files changed (117) hide show
  1. package/README.md +169 -0
  2. package/dist/index.d.ts +8 -0
  3. package/dist/index.js +159 -0
  4. package/dist/src/api/account.d.ts +3 -0
  5. package/dist/src/api/account.js +3 -0
  6. package/dist/src/api/client.d.ts +21 -0
  7. package/dist/src/api/client.js +107 -0
  8. package/dist/src/api/device-flow.d.ts +9 -0
  9. package/dist/src/api/device-flow.js +55 -0
  10. package/dist/src/api/devices.d.ts +10 -0
  11. package/dist/src/api/devices.js +24 -0
  12. package/dist/src/api/recovery.d.ts +5 -0
  13. package/dist/src/api/recovery.js +9 -0
  14. package/dist/src/api/sync.d.ts +9 -0
  15. package/dist/src/api/sync.js +22 -0
  16. package/dist/src/cli/bootstrap.d.ts +37 -0
  17. package/dist/src/cli/bootstrap.js +326 -0
  18. package/dist/src/cli/grants.d.ts +8 -0
  19. package/dist/src/cli/grants.js +76 -0
  20. package/dist/src/cli/helpers.d.ts +6 -0
  21. package/dist/src/cli/helpers.js +13 -0
  22. package/dist/src/cli/passphrase-input.d.ts +11 -0
  23. package/dist/src/cli/passphrase-input.js +94 -0
  24. package/dist/src/cli/pause.d.ts +2 -0
  25. package/dist/src/cli/pause.js +29 -0
  26. package/dist/src/cli/provisioning.d.ts +3 -0
  27. package/dist/src/cli/provisioning.js +30 -0
  28. package/dist/src/cli/pull.d.ts +31 -0
  29. package/dist/src/cli/pull.js +142 -0
  30. package/dist/src/cli/restore.d.ts +12 -0
  31. package/dist/src/cli/restore.js +90 -0
  32. package/dist/src/cli/setup.d.ts +17 -0
  33. package/dist/src/cli/setup.js +209 -0
  34. package/dist/src/cli/status.d.ts +1 -0
  35. package/dist/src/cli/status.js +31 -0
  36. package/dist/src/cli/sync.d.ts +4 -0
  37. package/dist/src/cli/sync.js +124 -0
  38. package/dist/src/cli/unlock.d.ts +19 -0
  39. package/dist/src/cli/unlock.js +153 -0
  40. package/dist/src/config.d.ts +4 -0
  41. package/dist/src/config.js +12 -0
  42. package/dist/src/constants.d.ts +23 -0
  43. package/dist/src/constants.js +27 -0
  44. package/dist/src/contract-types.d.ts +301 -0
  45. package/dist/src/contract-types.js +52 -0
  46. package/dist/src/crypto/aes-gcm.d.ts +29 -0
  47. package/dist/src/crypto/aes-gcm.js +44 -0
  48. package/dist/src/crypto/device-keys.d.ts +18 -0
  49. package/dist/src/crypto/device-keys.js +25 -0
  50. package/dist/src/crypto/grant.d.ts +29 -0
  51. package/dist/src/crypto/grant.js +87 -0
  52. package/dist/src/crypto/kdf.d.ts +16 -0
  53. package/dist/src/crypto/kdf.js +24 -0
  54. package/dist/src/crypto/keys.d.ts +14 -0
  55. package/dist/src/crypto/keys.js +35 -0
  56. package/dist/src/crypto/manifest.d.ts +25 -0
  57. package/dist/src/crypto/manifest.js +41 -0
  58. package/dist/src/crypto/recovery.d.ts +16 -0
  59. package/dist/src/crypto/recovery.js +94 -0
  60. package/dist/src/crypto/types.d.ts +34 -0
  61. package/dist/src/crypto/types.js +1 -0
  62. package/dist/src/debug-logger.d.ts +32 -0
  63. package/dist/src/debug-logger.js +108 -0
  64. package/dist/src/hooks/gateway-lifecycle.d.ts +6 -0
  65. package/dist/src/hooks/gateway-lifecycle.js +40 -0
  66. package/dist/src/hooks/prompt-inject.d.ts +7 -0
  67. package/dist/src/hooks/prompt-inject.js +18 -0
  68. package/dist/src/store/keychain.d.ts +26 -0
  69. package/dist/src/store/keychain.js +151 -0
  70. package/dist/src/store/local-state.d.ts +27 -0
  71. package/dist/src/store/local-state.js +47 -0
  72. package/dist/src/store/managed-unlock.d.ts +8 -0
  73. package/dist/src/store/managed-unlock.js +46 -0
  74. package/dist/src/store/pending-setup.d.ts +23 -0
  75. package/dist/src/store/pending-setup.js +32 -0
  76. package/dist/src/store/session.d.ts +13 -0
  77. package/dist/src/store/session.js +28 -0
  78. package/dist/src/sync/auto-sync.d.ts +12 -0
  79. package/dist/src/sync/auto-sync.js +82 -0
  80. package/dist/src/sync/conflict.d.ts +24 -0
  81. package/dist/src/sync/conflict.js +92 -0
  82. package/dist/src/sync/diff.d.ts +25 -0
  83. package/dist/src/sync/diff.js +75 -0
  84. package/dist/src/sync/downloader.d.ts +16 -0
  85. package/dist/src/sync/downloader.js +73 -0
  86. package/dist/src/sync/scanner.d.ts +12 -0
  87. package/dist/src/sync/scanner.js +52 -0
  88. package/dist/src/sync/state.d.ts +4 -0
  89. package/dist/src/sync/state.js +22 -0
  90. package/dist/src/sync/uploader.d.ts +20 -0
  91. package/dist/src/sync/uploader.js +86 -0
  92. package/dist/src/tools/grants-approve-pending.d.ts +17 -0
  93. package/dist/src/tools/grants-approve-pending.js +50 -0
  94. package/dist/src/tools/pull.d.ts +31 -0
  95. package/dist/src/tools/pull.js +71 -0
  96. package/dist/src/tools/restore.d.ts +31 -0
  97. package/dist/src/tools/restore.js +96 -0
  98. package/dist/src/tools/result.d.ts +7 -0
  99. package/dist/src/tools/result.js +6 -0
  100. package/dist/src/tools/secret-file.d.ts +10 -0
  101. package/dist/src/tools/secret-file.js +37 -0
  102. package/dist/src/tools/setup-finish.d.ts +36 -0
  103. package/dist/src/tools/setup-finish.js +108 -0
  104. package/dist/src/tools/setup-poll.d.ts +27 -0
  105. package/dist/src/tools/setup-poll.js +83 -0
  106. package/dist/src/tools/setup-start.d.ts +18 -0
  107. package/dist/src/tools/setup-start.js +49 -0
  108. package/dist/src/tools/status.d.ts +17 -0
  109. package/dist/src/tools/status.js +40 -0
  110. package/dist/src/tools/sync.d.ts +17 -0
  111. package/dist/src/tools/sync.js +49 -0
  112. package/dist/src/tools/unlock-secret.d.ts +42 -0
  113. package/dist/src/tools/unlock-secret.js +87 -0
  114. package/dist/src/tools/unlock.d.ts +25 -0
  115. package/dist/src/tools/unlock.js +72 -0
  116. package/openclaw.plugin.json +16 -0
  117. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # @membox-cloud/membox
2
+
3
+ Zero-knowledge encrypted memory sync for [OpenClaw](https://openclaw.ai).
4
+
5
+ Keep your local Markdown memory (`MEMORY.md`, `memory/*.md`) as the source of truth, with an encrypted cloud backup for sync, recovery, and multi-device support.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ openclaw plugins install @membox-cloud/membox
11
+ ```
12
+
13
+ ## Prerequisites
14
+
15
+ - OpenClaw >= 2026.3.7
16
+ - A [membox.cloud](https://membox.cloud) account (GitHub, Google, or email login)
17
+
18
+ ## Quick Start
19
+
20
+ ### 1. Setup
21
+
22
+ ```bash
23
+ openclaw membox setup
24
+ ```
25
+
26
+ This will:
27
+ - Start device authorization via `membox.cloud`
28
+ - Prompt for a vault passphrase (never sent to server)
29
+ - Create a new account master key for first-device setup, or request trusted-device approval for an existing vault
30
+ - Generate recovery materials on first-device setup
31
+ - Run initial upload or pull depending on whether the vault already exists
32
+
33
+ **Save the recovery code** displayed at the end. Without it and your passphrase, encrypted data cannot be recovered.
34
+
35
+ ### 2. Sync
36
+
37
+ ```bash
38
+ openclaw membox sync
39
+ ```
40
+
41
+ Upload local changes to the encrypted cloud replica.
42
+
43
+ ### 3. Pull
44
+
45
+ ```bash
46
+ openclaw membox pull
47
+ openclaw membox pull --preview
48
+ ```
49
+
50
+ Download remote changes. Use `--preview` to inspect before applying.
51
+
52
+ ### 4. Status
53
+
54
+ ```bash
55
+ openclaw membox status
56
+ ```
57
+
58
+ Show vault state, sync cursor, and device info.
59
+
60
+ ### 5. Restore
61
+
62
+ ```bash
63
+ openclaw membox restore
64
+ ```
65
+
66
+ Restore access on a new device with your recovery bundle and recovery code.
67
+
68
+ ### 6. Trusted-device approval
69
+
70
+ On an existing unlocked device:
71
+
72
+ ```bash
73
+ openclaw membox grants approve-pending
74
+ ```
75
+
76
+ Approve pending new-device requests and transfer the encrypted account master key.
77
+
78
+ ## CLI Commands
79
+
80
+ | Command | Description |
81
+ |---------|-------------|
82
+ | `membox setup` | Initial setup and device authorization |
83
+ | `membox unlock` | Unlock vault for the current session |
84
+ | `membox restore` | Restore vault access from the recovery bundle |
85
+ | `membox sync` | Upload local changes to cloud |
86
+ | `membox pull` | Download remote changes |
87
+ | `membox status` | Show vault status |
88
+ | `membox grants approve-pending` | Approve pending trusted-device requests |
89
+ | `membox pause` | Pause automatic sync |
90
+ | `membox resume` | Resume automatic sync |
91
+
92
+ ## Agent Tools
93
+
94
+ The plugin also registers LLM-callable tools:
95
+
96
+ - `membox_status` - Check vault status
97
+ - `membox_sync` - Trigger sync
98
+ - `membox_setup_start` - Begin setup flow
99
+ - `membox_setup_poll` - Poll pending browser authorization
100
+ - `membox_setup_finish` - Finish setup from local passphrase and recovery-code files
101
+ - `membox_unlock` - Unlock the vault from a local passphrase file
102
+ - `membox_unlock_secret_enable` - Explicitly opt this machine into managed local auto-unlock
103
+ - `membox_unlock_secret_disable` - Disable the managed local auto-unlock secret
104
+ - `membox_grants_approve_pending` - Approve trusted-device requests
105
+ - `membox_pull` - Preview or apply remote changes
106
+ - `membox_restore` - Restore with local recovery-code and passphrase files
107
+
108
+ Security note:
109
+
110
+ - Secret-bearing tools read from local files so the model does not need the vault passphrase or recovery code inline.
111
+ - First-device tool-only setup should always provide a local `recovery_code_output_file` so the recovery code never needs to travel through the model.
112
+ - Managed unlock is explicit opt-in. When enabled, the passphrase is kept in the local keychain for future auto-unlock on that machine.
113
+ - On Unix-like systems, passphrase and recovery-code files should be private, for example `chmod 600 /path/to/file`.
114
+
115
+ ## Configuration
116
+
117
+ In your OpenClaw plugin config:
118
+
119
+ ```json
120
+ {
121
+ "serverUrl": "https://membox.cloud"
122
+ }
123
+ ```
124
+
125
+ The default server is `https://membox.cloud`. Override with `serverUrl` for self-hosted deployments.
126
+
127
+ ## Local Development
128
+
129
+ Run the following commands from the repository root:
130
+
131
+ ```bash
132
+ source ./scripts/dev-env.sh
133
+ pnpm --filter @membox-cloud/membox build
134
+ openclaw plugins install -l ./apps/openclaw-plugin
135
+ openclaw plugins doctor
136
+ ```
137
+
138
+ The plugin's `serverUrl` is environment-driven in local development. Source `scripts/dev-env.sh` in every terminal where you run `openclaw membox ...`, or it will fall back to the default production URL.
139
+
140
+ For local verification, all plugin debug logs can be redirected into the repository `./logs/` directory by exporting `MEMBOX_LOG_FILE` before starting OpenClaw.
141
+
142
+ For a full manual validation flow, see `./MANUAL-DUAL-DEVICE-CHECKLIST.md`.
143
+
144
+ For an agent-first install + pairing flow, see `./AGENT-WORKFLOW.md` and run `bash scripts/install-membox-agent-stack.sh` from the repository root.
145
+
146
+ For release, production rollout, and final human-owned checks, see `./RELEASE-CHECKLIST.md`.
147
+
148
+ If native `keytar` support is unavailable on your machine, setup/unlock will now fail closed by default instead of silently writing secrets in plaintext. For local sandbox testing only, you can opt into the old file-based fallback with `MEMBOX_ALLOW_INSECURE_FILE_KEYCHAIN=1`.
149
+
150
+ ## Security Model
151
+
152
+ - **Zero-knowledge**: encryption/decryption happens locally. The server never sees plaintext.
153
+ - **Key hierarchy**: `passphrase -> URK -> AMK -> DEK`
154
+ - URK (User Root Key): derived from passphrase via Argon2id
155
+ - AMK (Account Master Key): random, wrapped by URK
156
+ - DEK (Data Encryption Key): per-object, wrapped by AMK
157
+ - **Recovery**: recovery code + passphrase can restore AMK on a new device
158
+ - **Device approval**: existing devices can grant key material to new devices via Ed25519-signed encrypted grants
159
+
160
+ ## Known Limitations (v0.1.0-alpha)
161
+
162
+ - Requires a reachable Membox API endpoint
163
+ - No selective file sync (all memory files are included)
164
+ - Conflict resolution strategies: `local-wins`, `remote-wins`, `conflict-copy`
165
+ - Auto-sync interval is not yet configurable
166
+
167
+ ## License
168
+
169
+ Private - see repository for details.
@@ -0,0 +1,8 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
+ declare const memboxPlugin: {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ register(api: OpenClawPluginApi): void;
7
+ };
8
+ export default memboxPlugin;
package/dist/index.js ADDED
@@ -0,0 +1,159 @@
1
+ import { resolveConfig } from "./src/config.js";
2
+ import { createStatusTool } from "./src/tools/status.js";
3
+ import { createSyncTool } from "./src/tools/sync.js";
4
+ import { createSetupStartTool } from "./src/tools/setup-start.js";
5
+ import { createSetupPollTool } from "./src/tools/setup-poll.js";
6
+ import { createSetupFinishTool } from "./src/tools/setup-finish.js";
7
+ import { createUnlockTool } from "./src/tools/unlock.js";
8
+ import { createUnlockSecretDisableTool, createUnlockSecretEnableTool, } from "./src/tools/unlock-secret.js";
9
+ import { createApprovePendingGrantsTool } from "./src/tools/grants-approve-pending.js";
10
+ import { createPullTool } from "./src/tools/pull.js";
11
+ import { createRestoreTool } from "./src/tools/restore.js";
12
+ import { injectVaultStatus } from "./src/hooks/prompt-inject.js";
13
+ import { onGatewayStart, onGatewayStop } from "./src/hooks/gateway-lifecycle.js";
14
+ const memboxPlugin = {
15
+ id: "membox",
16
+ name: "Membox Vault",
17
+ description: "Encrypted memory sync for Membox",
18
+ register(api) {
19
+ const cfg = resolveConfig(api.pluginConfig);
20
+ api.logger.info(`membox loaded, server: ${cfg.serverUrl}`);
21
+ // CLI commands
22
+ api.registerCli(({ program }) => {
23
+ const vault = program
24
+ .command("membox")
25
+ .description("Encrypted memory sync");
26
+ vault
27
+ .command("setup")
28
+ .description("Set up encrypted sync")
29
+ .action(async () => {
30
+ const { setupAction } = await import("./src/cli/setup.js");
31
+ await setupAction(cfg);
32
+ });
33
+ vault
34
+ .command("unlock")
35
+ .description("Unlock vault for this session")
36
+ .action(async () => {
37
+ const { unlockAction } = await import("./src/cli/unlock.js");
38
+ await unlockAction();
39
+ });
40
+ const unlockSecret = vault
41
+ .command("unlock-secret")
42
+ .description("Manage the local agent-managed unlock secret");
43
+ unlockSecret
44
+ .command("enable")
45
+ .description("Enable managed unlock on this machine")
46
+ .action(async () => {
47
+ const { readPassphrase } = await import("./src/cli/passphrase-input.js");
48
+ const { enableManagedUnlockAction } = await import("./src/cli/unlock.js");
49
+ const passphrase = await readPassphrase("Vault passphrase: ");
50
+ await enableManagedUnlockAction(passphrase);
51
+ });
52
+ unlockSecret
53
+ .command("disable")
54
+ .description("Disable managed unlock on this machine")
55
+ .action(async () => {
56
+ const { disableManagedUnlockAction } = await import("./src/cli/unlock.js");
57
+ await disableManagedUnlockAction();
58
+ });
59
+ vault
60
+ .command("restore")
61
+ .description("Restore vault access from a recovery bundle")
62
+ .action(async () => {
63
+ const { restoreAction } = await import("./src/cli/restore.js");
64
+ await restoreAction(cfg);
65
+ });
66
+ vault
67
+ .command("sync")
68
+ .description("Sync local changes to cloud")
69
+ .option("--on-conflict <strategy>", "conflict strategy: local-wins | remote-wins | conflict-copy", "conflict-copy")
70
+ .action(async (opts) => {
71
+ const { syncAction } = await import("./src/cli/sync.js");
72
+ await syncAction({ onConflict: opts.onConflict });
73
+ });
74
+ vault
75
+ .command("pull")
76
+ .description("Pull remote changes")
77
+ .option("--preview", "show changes without downloading")
78
+ .option("--on-conflict <strategy>", "conflict strategy: local-wins | remote-wins | conflict-copy", "conflict-copy")
79
+ .action(async (opts) => {
80
+ const { pullAction } = await import("./src/cli/pull.js");
81
+ await pullAction({ preview: opts.preview, onConflict: opts.onConflict });
82
+ });
83
+ vault
84
+ .command("status")
85
+ .description("Show vault status")
86
+ .action(async () => {
87
+ const { statusAction } = await import("./src/cli/status.js");
88
+ await statusAction();
89
+ });
90
+ vault
91
+ .command("grants")
92
+ .description("Manage trusted-device approvals")
93
+ .command("approve-pending")
94
+ .description("Approve pending new-device requests using this unlocked device")
95
+ .action(async () => {
96
+ const { approvePendingGrantsAction } = await import("./src/cli/grants.js");
97
+ await approvePendingGrantsAction();
98
+ });
99
+ vault
100
+ .command("pause")
101
+ .description("Pause auto-sync")
102
+ .action(async () => {
103
+ const { pauseAction } = await import("./src/cli/pause.js");
104
+ await pauseAction();
105
+ const { getAutoSync } = await import("./src/hooks/gateway-lifecycle.js");
106
+ getAutoSync()?.stop();
107
+ });
108
+ vault
109
+ .command("resume")
110
+ .description("Resume auto-sync")
111
+ .action(async () => {
112
+ const { resumeAction } = await import("./src/cli/pause.js");
113
+ await resumeAction();
114
+ const { getAutoSync, startAutoSync } = await import("./src/hooks/gateway-lifecycle.js");
115
+ let as = getAutoSync();
116
+ if (!as) {
117
+ as = await startAutoSync();
118
+ }
119
+ if (as) {
120
+ as.start();
121
+ as.triggerNow();
122
+ }
123
+ });
124
+ }, { commands: ["membox"] });
125
+ // Agent tools (LLM-callable)
126
+ api.registerTool(() => [
127
+ createStatusTool(),
128
+ createSyncTool(),
129
+ createSetupStartTool(cfg),
130
+ createSetupPollTool(cfg),
131
+ createSetupFinishTool(cfg),
132
+ createUnlockTool(),
133
+ createUnlockSecretEnableTool(),
134
+ createUnlockSecretDisableTool(),
135
+ createApprovePendingGrantsTool(),
136
+ createPullTool(),
137
+ createRestoreTool(cfg),
138
+ ], {
139
+ names: [
140
+ "membox_status",
141
+ "membox_sync",
142
+ "membox_setup_start",
143
+ "membox_setup_poll",
144
+ "membox_setup_finish",
145
+ "membox_unlock",
146
+ "membox_unlock_secret_enable",
147
+ "membox_unlock_secret_disable",
148
+ "membox_grants_approve_pending",
149
+ "membox_pull",
150
+ "membox_restore",
151
+ ],
152
+ });
153
+ // Hooks
154
+ api.on("before_prompt_build", injectVaultStatus);
155
+ api.on("gateway_start", onGatewayStart);
156
+ api.on("gateway_stop", onGatewayStop);
157
+ },
158
+ };
159
+ export default memboxPlugin;
@@ -0,0 +1,3 @@
1
+ import type { AccountMe } from "../contract-types.js";
2
+ import type { MemboxApiClient } from "./client.js";
3
+ export declare function getAccountMe(c: MemboxApiClient): Promise<AccountMe>;
@@ -0,0 +1,3 @@
1
+ export function getAccountMe(c) {
2
+ return c.get("/account/me");
3
+ }
@@ -0,0 +1,21 @@
1
+ export declare class ApiError extends Error {
2
+ status: number;
3
+ code: string;
4
+ constructor(status: number, code: string, message: string);
5
+ }
6
+ /**
7
+ * Typed HTTP client for the Membox API.
8
+ * Handles auth headers and automatic token refresh on 401.
9
+ */
10
+ export declare class MemboxApiClient {
11
+ private baseUrl;
12
+ private getAccessToken;
13
+ private getRefreshToken;
14
+ private onTokenRefresh;
15
+ constructor(baseUrl: string, getAccessToken: () => Promise<string | null>, getRefreshToken: () => Promise<string | null>, onTokenRefresh: (accessToken: string, refreshToken: string) => Promise<void>);
16
+ request<T>(method: string, path: string, body?: unknown, retry?: boolean): Promise<T>;
17
+ private tryRefresh;
18
+ get<T>(path: string): Promise<T>;
19
+ post<T>(path: string, body?: unknown): Promise<T>;
20
+ del<T>(path: string): Promise<T>;
21
+ }
@@ -0,0 +1,107 @@
1
+ import { debugLog } from "../debug-logger.js";
2
+ export class ApiError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.name = "ApiError";
10
+ }
11
+ }
12
+ /**
13
+ * Typed HTTP client for the Membox API.
14
+ * Handles auth headers and automatic token refresh on 401.
15
+ */
16
+ export class MemboxApiClient {
17
+ baseUrl;
18
+ getAccessToken;
19
+ getRefreshToken;
20
+ onTokenRefresh;
21
+ constructor(baseUrl, getAccessToken, getRefreshToken, onTokenRefresh) {
22
+ this.baseUrl = baseUrl;
23
+ this.getAccessToken = getAccessToken;
24
+ this.getRefreshToken = getRefreshToken;
25
+ this.onTokenRefresh = onTokenRefresh;
26
+ }
27
+ async request(method, path, body, retry = true) {
28
+ const url = `${this.baseUrl}${path}`;
29
+ const headers = {
30
+ "Content-Type": "application/json",
31
+ };
32
+ const token = await this.getAccessToken();
33
+ if (token) {
34
+ headers["Authorization"] = `Bearer ${token}`;
35
+ }
36
+ debugLog.apiRequest(method, url);
37
+ const t0 = Date.now();
38
+ let res;
39
+ try {
40
+ res = await fetch(url, {
41
+ method,
42
+ headers,
43
+ body: body ? JSON.stringify(body) : undefined,
44
+ });
45
+ }
46
+ catch (err) {
47
+ debugLog.apiError(method, url, err);
48
+ throw err;
49
+ }
50
+ debugLog.apiResponse(method, url, res.status, Date.now() - t0);
51
+ if (res.status === 401 && retry) {
52
+ debugLog.info("api-client", "Got 401, attempting token refresh...");
53
+ const refreshed = await this.tryRefresh();
54
+ if (refreshed) {
55
+ return this.request(method, path, body, false);
56
+ }
57
+ }
58
+ 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);
62
+ throw apiErr;
63
+ }
64
+ // Handle 204 No Content
65
+ if (res.status === 204) {
66
+ return undefined;
67
+ }
68
+ return res.json();
69
+ }
70
+ async tryRefresh() {
71
+ const rt = await this.getRefreshToken();
72
+ if (!rt) {
73
+ debugLog.warn("api-client", "Token refresh skipped: no refresh token");
74
+ return false;
75
+ }
76
+ const url = `${this.baseUrl}/auth/token/refresh`;
77
+ try {
78
+ debugLog.apiRequest("POST", url);
79
+ const t0 = Date.now();
80
+ const res = await fetch(url, {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/json" },
83
+ body: JSON.stringify({ refresh_token: rt }),
84
+ });
85
+ debugLog.apiResponse("POST", url, res.status, Date.now() - t0);
86
+ if (!res.ok)
87
+ return false;
88
+ const data = (await res.json());
89
+ await this.onTokenRefresh(data.access_token, data.refresh_token);
90
+ debugLog.info("api-client", "Token refresh succeeded");
91
+ return true;
92
+ }
93
+ catch (err) {
94
+ debugLog.apiError("POST", url, err);
95
+ return false;
96
+ }
97
+ }
98
+ get(path) {
99
+ return this.request("GET", path);
100
+ }
101
+ post(path, body) {
102
+ return this.request("POST", path, body);
103
+ }
104
+ del(path) {
105
+ return this.request("DELETE", path);
106
+ }
107
+ }
@@ -0,0 +1,9 @@
1
+ import type { DeviceStartRequest, DeviceStartResponse, DevicePollResponse, TokenResponse } from "../contract-types.js";
2
+ import type { MemboxApiClient } from "./client.js";
3
+ export declare function startDeviceFlow(client: MemboxApiClient, params: DeviceStartRequest): Promise<DeviceStartResponse>;
4
+ export declare function pollDeviceFlowOnce(client: MemboxApiClient, deviceCode: string): Promise<DevicePollResponse>;
5
+ /**
6
+ * Poll the device code endpoint until authorization is granted, denied, or expired.
7
+ * Respects the server's interval and backs off on slow_down responses.
8
+ */
9
+ export declare function pollDeviceFlow(client: MemboxApiClient, deviceCode: string, interval: number, expiresIn: number): Promise<TokenResponse>;
@@ -0,0 +1,55 @@
1
+ export async function startDeviceFlow(client, params) {
2
+ return client.post("/device/start", params);
3
+ }
4
+ export async function pollDeviceFlowOnce(client, deviceCode) {
5
+ return client.get(`/device/poll/${deviceCode}`);
6
+ }
7
+ /**
8
+ * Poll the device code endpoint until authorization is granted, denied, or expired.
9
+ * Respects the server's interval and backs off on slow_down responses.
10
+ */
11
+ export async function pollDeviceFlow(client, deviceCode, interval, expiresIn) {
12
+ const deadline = Date.now() + expiresIn * 1000;
13
+ let waitMs = interval * 1000;
14
+ let transientErrors = 0;
15
+ const MAX_TRANSIENT_ERRORS = 5;
16
+ while (Date.now() < deadline) {
17
+ await sleep(waitMs);
18
+ let res;
19
+ try {
20
+ res = await pollDeviceFlowOnce(client, deviceCode);
21
+ transientErrors = 0; // reset on success
22
+ }
23
+ catch (err) {
24
+ transientErrors++;
25
+ if (transientErrors >= MAX_TRANSIENT_ERRORS) {
26
+ throw new Error(`Device code polling failed after ${MAX_TRANSIENT_ERRORS} consecutive errors: ${err instanceof Error ? err.message : String(err)}`);
27
+ }
28
+ waitMs = Math.min(waitMs + 1000, 10000); // backoff on transient error
29
+ continue;
30
+ }
31
+ switch (res.status) {
32
+ case "access_granted":
33
+ return {
34
+ access_token: res.access_token,
35
+ refresh_token: res.refresh_token,
36
+ token_type: res.token_type ?? "Bearer",
37
+ expires_in: res.expires_in,
38
+ device_id: res.device_id,
39
+ };
40
+ case "slow_down":
41
+ waitMs = Math.min(waitMs + 1000, 10000);
42
+ break;
43
+ case "expired_token":
44
+ throw new Error("Device code expired. Please try again.");
45
+ case "denied":
46
+ throw new Error("Device authorization was denied.");
47
+ case "authorization_pending":
48
+ break;
49
+ }
50
+ }
51
+ throw new Error("Device code flow timed out.");
52
+ }
53
+ function sleep(ms) {
54
+ return new Promise((r) => setTimeout(r, ms));
55
+ }
@@ -0,0 +1,10 @@
1
+ import type { DeviceSummary, DeviceGrantOut, DeviceGrantApproveInput, GenericMessage } from "../contract-types.js";
2
+ import type { MemboxApiClient } from "./client.js";
3
+ export declare function listDevices(c: MemboxApiClient): Promise<DeviceSummary[]>;
4
+ export declare function revokeDevice(c: MemboxApiClient, deviceId: string): Promise<GenericMessage>;
5
+ export declare function revokeCurrentDevice(c: MemboxApiClient): Promise<GenericMessage>;
6
+ export declare function requestGrant(c: MemboxApiClient, targetDeviceId: string): Promise<DeviceGrantOut>;
7
+ export declare function getPendingGrants(c: MemboxApiClient): Promise<DeviceGrantOut[]>;
8
+ export declare function approveGrant(c: MemboxApiClient, grantId: string, body: DeviceGrantApproveInput): Promise<DeviceGrantOut>;
9
+ export declare function rejectGrant(c: MemboxApiClient, grantId: string): Promise<GenericMessage>;
10
+ export declare function getGrant(c: MemboxApiClient, grantId: string): Promise<DeviceGrantOut>;
@@ -0,0 +1,24 @@
1
+ export function listDevices(c) {
2
+ return c.get("/devices/");
3
+ }
4
+ export function revokeDevice(c, deviceId) {
5
+ return c.post(`/devices/${deviceId}/revoke`);
6
+ }
7
+ export function revokeCurrentDevice(c) {
8
+ return c.post("/devices/self/revoke");
9
+ }
10
+ export function requestGrant(c, targetDeviceId) {
11
+ return c.post(`/devices/${targetDeviceId}/grants`);
12
+ }
13
+ export function getPendingGrants(c) {
14
+ return c.get("/devices/grants/pending");
15
+ }
16
+ export function approveGrant(c, grantId, body) {
17
+ return c.post(`/devices/grants/${grantId}/approve`, body);
18
+ }
19
+ export function rejectGrant(c, grantId) {
20
+ return c.post(`/devices/grants/${grantId}/reject`);
21
+ }
22
+ export function getGrant(c, grantId) {
23
+ return c.get(`/devices/grants/${grantId}`);
24
+ }
@@ -0,0 +1,5 @@
1
+ import type { RecoveryMaterialStatus, RecoveryMaterialUpload, RecoveryBundleDownload, RecoveryDownloadInput, GenericMessage } from "../contract-types.js";
2
+ import type { MemboxApiClient } from "./client.js";
3
+ export declare function getRecoveryStatus(c: MemboxApiClient): Promise<RecoveryMaterialStatus>;
4
+ export declare function uploadRecoveryMaterial(c: MemboxApiClient, input: RecoveryMaterialUpload): Promise<GenericMessage>;
5
+ export declare function downloadRecoveryBundle(c: MemboxApiClient, input?: RecoveryDownloadInput): Promise<RecoveryBundleDownload>;
@@ -0,0 +1,9 @@
1
+ export function getRecoveryStatus(c) {
2
+ return c.get("/recovery/materials");
3
+ }
4
+ export function uploadRecoveryMaterial(c, input) {
5
+ return c.post("/recovery/materials", input);
6
+ }
7
+ export function downloadRecoveryBundle(c, input) {
8
+ return c.post("/recovery/bundle/download", input ?? { material_type: "recovery_bundle" });
9
+ }
@@ -0,0 +1,9 @@
1
+ import type { SyncStatusResponse, SyncChangesResponse, SyncCommitInput, SyncCommitResponse, BinaryUploadInput, BinaryUploadResponse, BinaryDownloadResponse, ManifestResponse } from "../contract-types.js";
2
+ import type { MemboxApiClient } from "./client.js";
3
+ export declare function getSyncStatus(c: MemboxApiClient): Promise<SyncStatusResponse>;
4
+ export declare function getSyncChanges(c: MemboxApiClient, cursor: number): Promise<SyncChangesResponse>;
5
+ export declare function commitObject(c: MemboxApiClient, input: SyncCommitInput): Promise<SyncCommitResponse>;
6
+ export declare function uploadBlob(c: MemboxApiClient, input: BinaryUploadInput): Promise<BinaryUploadResponse>;
7
+ export declare function downloadBlob(c: MemboxApiClient, blobKey: string): Promise<BinaryDownloadResponse>;
8
+ export declare function getManifest(c: MemboxApiClient, objectId: string, version?: number): Promise<ManifestResponse>;
9
+ export declare function deleteObject(c: MemboxApiClient, objectId: string): Promise<void>;
@@ -0,0 +1,22 @@
1
+ export function getSyncStatus(c) {
2
+ return c.get("/sync/status");
3
+ }
4
+ export function getSyncChanges(c, cursor) {
5
+ return c.get(`/sync/changes?cursor=${cursor}`);
6
+ }
7
+ export function commitObject(c, input) {
8
+ return c.post("/sync/objects/commit", input);
9
+ }
10
+ export function uploadBlob(c, input) {
11
+ return c.post("/sync/blobs/upload", input);
12
+ }
13
+ export function downloadBlob(c, blobKey) {
14
+ return c.get(`/sync/blobs/${encodeURIComponent(blobKey)}`);
15
+ }
16
+ export function getManifest(c, objectId, version) {
17
+ const q = version != null ? `?version=${version}` : "";
18
+ return c.get(`/sync/objects/${objectId}/manifest${q}`);
19
+ }
20
+ export function deleteObject(c, objectId) {
21
+ return c.del(`/sync/objects/${objectId}`);
22
+ }
@@ -0,0 +1,37 @@
1
+ import type { AccountMe, Platform, TokenResponse } from "../contract-types.js";
2
+ import type { memboxConfig } from "../config.js";
3
+ import { type DeviceKeyPair } from "../crypto/device-keys.js";
4
+ import { MemboxApiClient } from "../api/client.js";
5
+ import { type PendingSetupState } from "../store/pending-setup.js";
6
+ export interface AuthorizedDeviceContext {
7
+ baseUrl: string;
8
+ deviceName: string;
9
+ platform: Platform;
10
+ deviceKeys: DeviceKeyPair;
11
+ tokens: TokenResponse;
12
+ client: MemboxApiClient;
13
+ account: AccountMe;
14
+ }
15
+ export interface SetupAuthorizationStatus {
16
+ status: "not_started" | "authorization_pending" | "slow_down" | "authorized" | "denied" | "expired";
17
+ pendingSetup: PendingSetupState | null;
18
+ retryAfterSeconds?: number;
19
+ message?: string;
20
+ }
21
+ export declare function toB64(data: Uint8Array): string;
22
+ export declare function fromB64(value: string): Uint8Array;
23
+ export declare function detectPlatform(): Platform;
24
+ export declare function tryOpenUrl(url: string): boolean;
25
+ export declare function clearPendingSetupArtifacts(): Promise<void>;
26
+ export declare function ensurePendingDeviceAuthorization(cfg: memboxConfig): Promise<{
27
+ pendingSetup: PendingSetupState;
28
+ created: boolean;
29
+ }>;
30
+ export declare function pollPendingDeviceAuthorizationOnce(cfg: memboxConfig): Promise<SetupAuthorizationStatus>;
31
+ export declare function authorizeDevice(cfg: memboxConfig): Promise<AuthorizedDeviceContext>;
32
+ export declare function persistVaultSecrets(params: {
33
+ deviceKeys: DeviceKeyPair;
34
+ passphrase: string;
35
+ amk: Uint8Array;
36
+ refreshToken: string;
37
+ }): Promise<void>;