@membox-cloud/membox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +159 -0
- package/dist/src/api/account.d.ts +3 -0
- package/dist/src/api/account.js +3 -0
- package/dist/src/api/client.d.ts +21 -0
- package/dist/src/api/client.js +107 -0
- package/dist/src/api/device-flow.d.ts +9 -0
- package/dist/src/api/device-flow.js +55 -0
- package/dist/src/api/devices.d.ts +10 -0
- package/dist/src/api/devices.js +24 -0
- package/dist/src/api/recovery.d.ts +5 -0
- package/dist/src/api/recovery.js +9 -0
- package/dist/src/api/sync.d.ts +9 -0
- package/dist/src/api/sync.js +22 -0
- package/dist/src/cli/bootstrap.d.ts +37 -0
- package/dist/src/cli/bootstrap.js +326 -0
- package/dist/src/cli/grants.d.ts +8 -0
- package/dist/src/cli/grants.js +76 -0
- package/dist/src/cli/helpers.d.ts +6 -0
- package/dist/src/cli/helpers.js +13 -0
- package/dist/src/cli/passphrase-input.d.ts +11 -0
- package/dist/src/cli/passphrase-input.js +94 -0
- package/dist/src/cli/pause.d.ts +2 -0
- package/dist/src/cli/pause.js +29 -0
- package/dist/src/cli/provisioning.d.ts +3 -0
- package/dist/src/cli/provisioning.js +30 -0
- package/dist/src/cli/pull.d.ts +31 -0
- package/dist/src/cli/pull.js +142 -0
- package/dist/src/cli/restore.d.ts +12 -0
- package/dist/src/cli/restore.js +90 -0
- package/dist/src/cli/setup.d.ts +17 -0
- package/dist/src/cli/setup.js +209 -0
- package/dist/src/cli/status.d.ts +1 -0
- package/dist/src/cli/status.js +31 -0
- package/dist/src/cli/sync.d.ts +4 -0
- package/dist/src/cli/sync.js +124 -0
- package/dist/src/cli/unlock.d.ts +19 -0
- package/dist/src/cli/unlock.js +153 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +12 -0
- package/dist/src/constants.d.ts +23 -0
- package/dist/src/constants.js +27 -0
- package/dist/src/contract-types.d.ts +301 -0
- package/dist/src/contract-types.js +52 -0
- package/dist/src/crypto/aes-gcm.d.ts +29 -0
- package/dist/src/crypto/aes-gcm.js +44 -0
- package/dist/src/crypto/device-keys.d.ts +18 -0
- package/dist/src/crypto/device-keys.js +25 -0
- package/dist/src/crypto/grant.d.ts +29 -0
- package/dist/src/crypto/grant.js +87 -0
- package/dist/src/crypto/kdf.d.ts +16 -0
- package/dist/src/crypto/kdf.js +24 -0
- package/dist/src/crypto/keys.d.ts +14 -0
- package/dist/src/crypto/keys.js +35 -0
- package/dist/src/crypto/manifest.d.ts +25 -0
- package/dist/src/crypto/manifest.js +41 -0
- package/dist/src/crypto/recovery.d.ts +16 -0
- package/dist/src/crypto/recovery.js +94 -0
- package/dist/src/crypto/types.d.ts +34 -0
- package/dist/src/crypto/types.js +1 -0
- package/dist/src/debug-logger.d.ts +32 -0
- package/dist/src/debug-logger.js +108 -0
- package/dist/src/hooks/gateway-lifecycle.d.ts +6 -0
- package/dist/src/hooks/gateway-lifecycle.js +40 -0
- package/dist/src/hooks/prompt-inject.d.ts +7 -0
- package/dist/src/hooks/prompt-inject.js +18 -0
- package/dist/src/store/keychain.d.ts +26 -0
- package/dist/src/store/keychain.js +151 -0
- package/dist/src/store/local-state.d.ts +27 -0
- package/dist/src/store/local-state.js +47 -0
- package/dist/src/store/managed-unlock.d.ts +8 -0
- package/dist/src/store/managed-unlock.js +46 -0
- package/dist/src/store/pending-setup.d.ts +23 -0
- package/dist/src/store/pending-setup.js +32 -0
- package/dist/src/store/session.d.ts +13 -0
- package/dist/src/store/session.js +28 -0
- package/dist/src/sync/auto-sync.d.ts +12 -0
- package/dist/src/sync/auto-sync.js +82 -0
- package/dist/src/sync/conflict.d.ts +24 -0
- package/dist/src/sync/conflict.js +92 -0
- package/dist/src/sync/diff.d.ts +25 -0
- package/dist/src/sync/diff.js +75 -0
- package/dist/src/sync/downloader.d.ts +16 -0
- package/dist/src/sync/downloader.js +73 -0
- package/dist/src/sync/scanner.d.ts +12 -0
- package/dist/src/sync/scanner.js +52 -0
- package/dist/src/sync/state.d.ts +4 -0
- package/dist/src/sync/state.js +22 -0
- package/dist/src/sync/uploader.d.ts +20 -0
- package/dist/src/sync/uploader.js +86 -0
- package/dist/src/tools/grants-approve-pending.d.ts +17 -0
- package/dist/src/tools/grants-approve-pending.js +50 -0
- package/dist/src/tools/pull.d.ts +31 -0
- package/dist/src/tools/pull.js +71 -0
- package/dist/src/tools/restore.d.ts +31 -0
- package/dist/src/tools/restore.js +96 -0
- package/dist/src/tools/result.d.ts +7 -0
- package/dist/src/tools/result.js +6 -0
- package/dist/src/tools/secret-file.d.ts +10 -0
- package/dist/src/tools/secret-file.js +37 -0
- package/dist/src/tools/setup-finish.d.ts +36 -0
- package/dist/src/tools/setup-finish.js +108 -0
- package/dist/src/tools/setup-poll.d.ts +27 -0
- package/dist/src/tools/setup-poll.js +83 -0
- package/dist/src/tools/setup-start.d.ts +18 -0
- package/dist/src/tools/setup-start.js +49 -0
- package/dist/src/tools/status.d.ts +17 -0
- package/dist/src/tools/status.js +40 -0
- package/dist/src/tools/sync.d.ts +17 -0
- package/dist/src/tools/sync.js +49 -0
- package/dist/src/tools/unlock-secret.d.ts +42 -0
- package/dist/src/tools/unlock-secret.js +87 -0
- package/dist/src/tools/unlock.d.ts +25 -0
- package/dist/src/tools/unlock.js +72 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +35 -0
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.
|
package/dist/index.d.ts
ADDED
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,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>;
|