@membox-cloud/membox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +159 -0
- package/dist/src/api/account.d.ts +3 -0
- package/dist/src/api/account.js +3 -0
- package/dist/src/api/client.d.ts +21 -0
- package/dist/src/api/client.js +107 -0
- package/dist/src/api/device-flow.d.ts +9 -0
- package/dist/src/api/device-flow.js +55 -0
- package/dist/src/api/devices.d.ts +10 -0
- package/dist/src/api/devices.js +24 -0
- package/dist/src/api/recovery.d.ts +5 -0
- package/dist/src/api/recovery.js +9 -0
- package/dist/src/api/sync.d.ts +9 -0
- package/dist/src/api/sync.js +22 -0
- package/dist/src/cli/bootstrap.d.ts +37 -0
- package/dist/src/cli/bootstrap.js +326 -0
- package/dist/src/cli/grants.d.ts +8 -0
- package/dist/src/cli/grants.js +76 -0
- package/dist/src/cli/helpers.d.ts +6 -0
- package/dist/src/cli/helpers.js +13 -0
- package/dist/src/cli/passphrase-input.d.ts +11 -0
- package/dist/src/cli/passphrase-input.js +94 -0
- package/dist/src/cli/pause.d.ts +2 -0
- package/dist/src/cli/pause.js +29 -0
- package/dist/src/cli/provisioning.d.ts +3 -0
- package/dist/src/cli/provisioning.js +30 -0
- package/dist/src/cli/pull.d.ts +31 -0
- package/dist/src/cli/pull.js +142 -0
- package/dist/src/cli/restore.d.ts +12 -0
- package/dist/src/cli/restore.js +90 -0
- package/dist/src/cli/setup.d.ts +17 -0
- package/dist/src/cli/setup.js +209 -0
- package/dist/src/cli/status.d.ts +1 -0
- package/dist/src/cli/status.js +31 -0
- package/dist/src/cli/sync.d.ts +4 -0
- package/dist/src/cli/sync.js +124 -0
- package/dist/src/cli/unlock.d.ts +19 -0
- package/dist/src/cli/unlock.js +153 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +12 -0
- package/dist/src/constants.d.ts +23 -0
- package/dist/src/constants.js +27 -0
- package/dist/src/contract-types.d.ts +301 -0
- package/dist/src/contract-types.js +52 -0
- package/dist/src/crypto/aes-gcm.d.ts +29 -0
- package/dist/src/crypto/aes-gcm.js +44 -0
- package/dist/src/crypto/device-keys.d.ts +18 -0
- package/dist/src/crypto/device-keys.js +25 -0
- package/dist/src/crypto/grant.d.ts +29 -0
- package/dist/src/crypto/grant.js +87 -0
- package/dist/src/crypto/kdf.d.ts +16 -0
- package/dist/src/crypto/kdf.js +24 -0
- package/dist/src/crypto/keys.d.ts +14 -0
- package/dist/src/crypto/keys.js +35 -0
- package/dist/src/crypto/manifest.d.ts +25 -0
- package/dist/src/crypto/manifest.js +41 -0
- package/dist/src/crypto/recovery.d.ts +16 -0
- package/dist/src/crypto/recovery.js +94 -0
- package/dist/src/crypto/types.d.ts +34 -0
- package/dist/src/crypto/types.js +1 -0
- package/dist/src/debug-logger.d.ts +32 -0
- package/dist/src/debug-logger.js +108 -0
- package/dist/src/hooks/gateway-lifecycle.d.ts +6 -0
- package/dist/src/hooks/gateway-lifecycle.js +40 -0
- package/dist/src/hooks/prompt-inject.d.ts +7 -0
- package/dist/src/hooks/prompt-inject.js +18 -0
- package/dist/src/store/keychain.d.ts +26 -0
- package/dist/src/store/keychain.js +151 -0
- package/dist/src/store/local-state.d.ts +27 -0
- package/dist/src/store/local-state.js +47 -0
- package/dist/src/store/managed-unlock.d.ts +8 -0
- package/dist/src/store/managed-unlock.js +46 -0
- package/dist/src/store/pending-setup.d.ts +23 -0
- package/dist/src/store/pending-setup.js +32 -0
- package/dist/src/store/session.d.ts +13 -0
- package/dist/src/store/session.js +28 -0
- package/dist/src/sync/auto-sync.d.ts +12 -0
- package/dist/src/sync/auto-sync.js +82 -0
- package/dist/src/sync/conflict.d.ts +24 -0
- package/dist/src/sync/conflict.js +92 -0
- package/dist/src/sync/diff.d.ts +25 -0
- package/dist/src/sync/diff.js +75 -0
- package/dist/src/sync/downloader.d.ts +16 -0
- package/dist/src/sync/downloader.js +73 -0
- package/dist/src/sync/scanner.d.ts +12 -0
- package/dist/src/sync/scanner.js +52 -0
- package/dist/src/sync/state.d.ts +4 -0
- package/dist/src/sync/state.js +22 -0
- package/dist/src/sync/uploader.d.ts +20 -0
- package/dist/src/sync/uploader.js +86 -0
- package/dist/src/tools/grants-approve-pending.d.ts +17 -0
- package/dist/src/tools/grants-approve-pending.js +50 -0
- package/dist/src/tools/pull.d.ts +31 -0
- package/dist/src/tools/pull.js +71 -0
- package/dist/src/tools/restore.d.ts +31 -0
- package/dist/src/tools/restore.js +96 -0
- package/dist/src/tools/result.d.ts +7 -0
- package/dist/src/tools/result.js +6 -0
- package/dist/src/tools/secret-file.d.ts +10 -0
- package/dist/src/tools/secret-file.js +37 -0
- package/dist/src/tools/setup-finish.d.ts +36 -0
- package/dist/src/tools/setup-finish.js +108 -0
- package/dist/src/tools/setup-poll.d.ts +27 -0
- package/dist/src/tools/setup-poll.js +83 -0
- package/dist/src/tools/setup-start.d.ts +18 -0
- package/dist/src/tools/setup-start.js +49 -0
- package/dist/src/tools/status.d.ts +17 -0
- package/dist/src/tools/status.js +40 -0
- package/dist/src/tools/sync.d.ts +17 -0
- package/dist/src/tools/sync.js +49 -0
- package/dist/src/tools/unlock-secret.d.ts +42 -0
- package/dist/src/tools/unlock-secret.js +87 -0
- package/dist/src/tools/unlock.d.ts +25 -0
- package/dist/src/tools/unlock.js +72 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +35 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const INSECURE_FILE_KEYCHAIN_ENV = "MEMBOX_ALLOW_INSECURE_FILE_KEYCHAIN";
|
|
2
|
+
export declare function storeBytes(account: string, data: Uint8Array): Promise<void>;
|
|
3
|
+
export declare function getBytes(account: string): Promise<Uint8Array | null>;
|
|
4
|
+
export declare function storeString(account: string, value: string): Promise<void>;
|
|
5
|
+
export declare function getString(account: string): Promise<string | null>;
|
|
6
|
+
export declare function deleteSecret(account: string): Promise<void>;
|
|
7
|
+
/** Keychain account names for each stored secret. */
|
|
8
|
+
export declare const ACCOUNTS: {
|
|
9
|
+
readonly ED25519_PRIVATE: "device-ed25519-private";
|
|
10
|
+
readonly ED25519_PUBLIC: "device-ed25519-public";
|
|
11
|
+
readonly X25519_PRIVATE: "device-x25519-private";
|
|
12
|
+
readonly X25519_PUBLIC: "device-x25519-public";
|
|
13
|
+
readonly WRAPPED_AMK: "wrapped-amk";
|
|
14
|
+
readonly WRAPPED_AMK_SALT: "wrapped-amk-salt";
|
|
15
|
+
readonly WRAPPED_AMK_IV: "wrapped-amk-iv";
|
|
16
|
+
readonly WRAPPED_AMK_TAG: "wrapped-amk-tag";
|
|
17
|
+
readonly REFRESH_TOKEN: "refresh-token";
|
|
18
|
+
readonly MANAGED_UNLOCK_PASSPHRASE: "managed-unlock-passphrase";
|
|
19
|
+
readonly PENDING_SETUP_ED25519_PRIVATE: "pending-setup-device-ed25519-private";
|
|
20
|
+
readonly PENDING_SETUP_ED25519_PUBLIC: "pending-setup-device-ed25519-public";
|
|
21
|
+
readonly PENDING_SETUP_X25519_PRIVATE: "pending-setup-device-x25519-private";
|
|
22
|
+
readonly PENDING_SETUP_X25519_PUBLIC: "pending-setup-device-x25519-public";
|
|
23
|
+
readonly PENDING_SETUP_REFRESH_TOKEN: "pending-setup-refresh-token";
|
|
24
|
+
};
|
|
25
|
+
export declare function clearStoredSecrets(): Promise<void>;
|
|
26
|
+
export declare function clearPendingSetupSecrets(): Promise<void>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { KEYCHAIN_SERVICE, resolveStateRoot } from "../constants.js";
|
|
4
|
+
export const INSECURE_FILE_KEYCHAIN_ENV = "MEMBOX_ALLOW_INSECURE_FILE_KEYCHAIN";
|
|
5
|
+
const DISABLE_KEYTAR_ENV = "MEMBOX_DISABLE_KEYTAR";
|
|
6
|
+
let keytarPromise = null;
|
|
7
|
+
let warnedAboutInsecureFallback = false;
|
|
8
|
+
function toB64(d) {
|
|
9
|
+
return Buffer.from(d).toString("base64");
|
|
10
|
+
}
|
|
11
|
+
function fromB64(s) {
|
|
12
|
+
return new Uint8Array(Buffer.from(s, "base64"));
|
|
13
|
+
}
|
|
14
|
+
function fallbackPath() {
|
|
15
|
+
return join(resolveStateRoot(), "secrets.json");
|
|
16
|
+
}
|
|
17
|
+
async function readFallbackSecrets() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(fallbackPath(), "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function writeFallbackSecrets(secrets) {
|
|
27
|
+
const path = fallbackPath();
|
|
28
|
+
await mkdir(dirname(path), { recursive: true });
|
|
29
|
+
const tmp = path + ".tmp." + Date.now();
|
|
30
|
+
await writeFile(tmp, JSON.stringify(secrets, null, 2), { mode: 0o600 });
|
|
31
|
+
await rename(tmp, path);
|
|
32
|
+
await chmod(path, 0o600).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
function allowInsecureFileKeychain() {
|
|
35
|
+
return process.env[INSECURE_FILE_KEYCHAIN_ENV] === "1";
|
|
36
|
+
}
|
|
37
|
+
function ensureInsecureFallbackAllowed() {
|
|
38
|
+
if (allowInsecureFileKeychain()) {
|
|
39
|
+
if (!warnedAboutInsecureFallback) {
|
|
40
|
+
warnedAboutInsecureFallback = true;
|
|
41
|
+
console.warn(`[membox] keytar unavailable; using plaintext file fallback at ${fallbackPath()}. ` +
|
|
42
|
+
`This is for local development only.`);
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
throw new Error("Secure keychain unavailable. Install keytar support for this platform, " +
|
|
47
|
+
`or set ${INSECURE_FILE_KEYCHAIN_ENV}=1 to opt into plaintext ~/.membox/secrets.json storage for local development.`);
|
|
48
|
+
}
|
|
49
|
+
async function loadKeytar() {
|
|
50
|
+
if (process.env[DISABLE_KEYTAR_ENV] === "1") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!keytarPromise) {
|
|
54
|
+
keytarPromise = import("keytar")
|
|
55
|
+
.then((mod) => (mod.default ?? mod))
|
|
56
|
+
.catch(() => null);
|
|
57
|
+
}
|
|
58
|
+
return keytarPromise;
|
|
59
|
+
}
|
|
60
|
+
async function storeSecret(account, value) {
|
|
61
|
+
const keytar = await loadKeytar();
|
|
62
|
+
if (keytar) {
|
|
63
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, account, value);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
ensureInsecureFallbackAllowed();
|
|
67
|
+
const secrets = await readFallbackSecrets();
|
|
68
|
+
secrets[account] = value;
|
|
69
|
+
await writeFallbackSecrets(secrets);
|
|
70
|
+
}
|
|
71
|
+
async function getSecret(account) {
|
|
72
|
+
const keytar = await loadKeytar();
|
|
73
|
+
if (keytar) {
|
|
74
|
+
return keytar.getPassword(KEYCHAIN_SERVICE, account);
|
|
75
|
+
}
|
|
76
|
+
ensureInsecureFallbackAllowed();
|
|
77
|
+
const secrets = await readFallbackSecrets();
|
|
78
|
+
return secrets[account] ?? null;
|
|
79
|
+
}
|
|
80
|
+
export async function storeBytes(account, data) {
|
|
81
|
+
await storeSecret(account, toB64(data));
|
|
82
|
+
}
|
|
83
|
+
export async function getBytes(account) {
|
|
84
|
+
const val = await getSecret(account);
|
|
85
|
+
return val ? fromB64(val) : null;
|
|
86
|
+
}
|
|
87
|
+
export async function storeString(account, value) {
|
|
88
|
+
await storeSecret(account, value);
|
|
89
|
+
}
|
|
90
|
+
export async function getString(account) {
|
|
91
|
+
return getSecret(account);
|
|
92
|
+
}
|
|
93
|
+
export async function deleteSecret(account) {
|
|
94
|
+
const keytar = await loadKeytar();
|
|
95
|
+
if (keytar) {
|
|
96
|
+
await keytar.deletePassword(KEYCHAIN_SERVICE, account);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
ensureInsecureFallbackAllowed();
|
|
100
|
+
const secrets = await readFallbackSecrets();
|
|
101
|
+
if (!(account in secrets)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
delete secrets[account];
|
|
105
|
+
await writeFallbackSecrets(secrets);
|
|
106
|
+
}
|
|
107
|
+
/** Keychain account names for each stored secret. */
|
|
108
|
+
export const ACCOUNTS = {
|
|
109
|
+
ED25519_PRIVATE: "device-ed25519-private",
|
|
110
|
+
ED25519_PUBLIC: "device-ed25519-public",
|
|
111
|
+
X25519_PRIVATE: "device-x25519-private",
|
|
112
|
+
X25519_PUBLIC: "device-x25519-public",
|
|
113
|
+
WRAPPED_AMK: "wrapped-amk",
|
|
114
|
+
WRAPPED_AMK_SALT: "wrapped-amk-salt",
|
|
115
|
+
WRAPPED_AMK_IV: "wrapped-amk-iv",
|
|
116
|
+
WRAPPED_AMK_TAG: "wrapped-amk-tag",
|
|
117
|
+
REFRESH_TOKEN: "refresh-token",
|
|
118
|
+
MANAGED_UNLOCK_PASSPHRASE: "managed-unlock-passphrase",
|
|
119
|
+
PENDING_SETUP_ED25519_PRIVATE: "pending-setup-device-ed25519-private",
|
|
120
|
+
PENDING_SETUP_ED25519_PUBLIC: "pending-setup-device-ed25519-public",
|
|
121
|
+
PENDING_SETUP_X25519_PRIVATE: "pending-setup-device-x25519-private",
|
|
122
|
+
PENDING_SETUP_X25519_PUBLIC: "pending-setup-device-x25519-public",
|
|
123
|
+
PENDING_SETUP_REFRESH_TOKEN: "pending-setup-refresh-token",
|
|
124
|
+
};
|
|
125
|
+
export async function clearStoredSecrets() {
|
|
126
|
+
await Promise.allSettled([
|
|
127
|
+
ACCOUNTS.ED25519_PRIVATE,
|
|
128
|
+
ACCOUNTS.ED25519_PUBLIC,
|
|
129
|
+
ACCOUNTS.X25519_PRIVATE,
|
|
130
|
+
ACCOUNTS.X25519_PUBLIC,
|
|
131
|
+
ACCOUNTS.WRAPPED_AMK,
|
|
132
|
+
ACCOUNTS.WRAPPED_AMK_SALT,
|
|
133
|
+
ACCOUNTS.WRAPPED_AMK_IV,
|
|
134
|
+
ACCOUNTS.WRAPPED_AMK_TAG,
|
|
135
|
+
ACCOUNTS.REFRESH_TOKEN,
|
|
136
|
+
ACCOUNTS.MANAGED_UNLOCK_PASSPHRASE,
|
|
137
|
+
].map(async (account) => {
|
|
138
|
+
await deleteSecret(account);
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
export async function clearPendingSetupSecrets() {
|
|
142
|
+
await Promise.allSettled([
|
|
143
|
+
ACCOUNTS.PENDING_SETUP_ED25519_PRIVATE,
|
|
144
|
+
ACCOUNTS.PENDING_SETUP_ED25519_PUBLIC,
|
|
145
|
+
ACCOUNTS.PENDING_SETUP_X25519_PRIVATE,
|
|
146
|
+
ACCOUNTS.PENDING_SETUP_X25519_PUBLIC,
|
|
147
|
+
ACCOUNTS.PENDING_SETUP_REFRESH_TOKEN,
|
|
148
|
+
].map(async (account) => {
|
|
149
|
+
await deleteSecret(account);
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface FileVersionEntry {
|
|
2
|
+
object_id: string;
|
|
3
|
+
version: number;
|
|
4
|
+
content_sha256: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LocalState {
|
|
7
|
+
server_url: string;
|
|
8
|
+
device_id: string;
|
|
9
|
+
device_name: string;
|
|
10
|
+
user_id: string;
|
|
11
|
+
sync_cursor: number;
|
|
12
|
+
file_versions: Record<string, FileVersionEntry>;
|
|
13
|
+
setup_complete: boolean;
|
|
14
|
+
sync_paused: boolean;
|
|
15
|
+
managed_unlock_enabled: boolean;
|
|
16
|
+
last_sync_at?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function readState(): Promise<LocalState | null>;
|
|
19
|
+
/** Write state atomically (write to tmp, then rename). */
|
|
20
|
+
export declare function writeState(state: LocalState): Promise<void>;
|
|
21
|
+
export declare function deleteState(): Promise<void>;
|
|
22
|
+
export declare function createInitialState(params: {
|
|
23
|
+
serverUrl: string;
|
|
24
|
+
deviceId: string;
|
|
25
|
+
deviceName: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
}): LocalState;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { STATE_FILE, resolveStateRoot } from "../constants.js";
|
|
4
|
+
function statePath() {
|
|
5
|
+
return join(resolveStateRoot(), STATE_FILE);
|
|
6
|
+
}
|
|
7
|
+
export async function readState() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readFile(statePath(), "utf-8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
// Backward compat: default sync_paused for pre-V1 state files
|
|
12
|
+
if (parsed.sync_paused === undefined) {
|
|
13
|
+
parsed.sync_paused = false;
|
|
14
|
+
}
|
|
15
|
+
if (parsed.managed_unlock_enabled === undefined) {
|
|
16
|
+
parsed.managed_unlock_enabled = false;
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Write state atomically (write to tmp, then rename). */
|
|
25
|
+
export async function writeState(state) {
|
|
26
|
+
const p = statePath();
|
|
27
|
+
await mkdir(dirname(p), { recursive: true });
|
|
28
|
+
const tmp = p + ".tmp." + Date.now();
|
|
29
|
+
await writeFile(tmp, JSON.stringify(state, null, 2));
|
|
30
|
+
await rename(tmp, p);
|
|
31
|
+
}
|
|
32
|
+
export async function deleteState() {
|
|
33
|
+
await unlink(statePath()).catch(() => { });
|
|
34
|
+
}
|
|
35
|
+
export function createInitialState(params) {
|
|
36
|
+
return {
|
|
37
|
+
server_url: params.serverUrl,
|
|
38
|
+
device_id: params.deviceId,
|
|
39
|
+
device_name: params.deviceName,
|
|
40
|
+
user_id: params.userId,
|
|
41
|
+
sync_cursor: 0,
|
|
42
|
+
file_versions: {},
|
|
43
|
+
setup_complete: true,
|
|
44
|
+
sync_paused: false,
|
|
45
|
+
managed_unlock_enabled: false,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ManagedUnlockStatus {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
secret_present: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function getManagedUnlockStatus(): Promise<ManagedUnlockStatus>;
|
|
6
|
+
export declare function getManagedUnlockPassphrase(): Promise<string | null>;
|
|
7
|
+
export declare function enableManagedUnlock(passphrase: string): Promise<void>;
|
|
8
|
+
export declare function disableManagedUnlock(): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ACCOUNTS, deleteSecret, getString, storeString, } from "./keychain.js";
|
|
2
|
+
import { readState, writeState } from "./local-state.js";
|
|
3
|
+
async function requireSetupState() {
|
|
4
|
+
const state = await readState();
|
|
5
|
+
if (!state?.setup_complete) {
|
|
6
|
+
throw new Error("Vault not set up. Complete `openclaw membox setup` before enabling managed unlock.");
|
|
7
|
+
}
|
|
8
|
+
return state;
|
|
9
|
+
}
|
|
10
|
+
export async function getManagedUnlockStatus() {
|
|
11
|
+
const state = await readState();
|
|
12
|
+
if (!state?.setup_complete) {
|
|
13
|
+
return {
|
|
14
|
+
enabled: false,
|
|
15
|
+
secret_present: false,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const secret = await getString(ACCOUNTS.MANAGED_UNLOCK_PASSPHRASE);
|
|
19
|
+
return {
|
|
20
|
+
enabled: state.managed_unlock_enabled === true,
|
|
21
|
+
secret_present: !!secret,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function getManagedUnlockPassphrase() {
|
|
25
|
+
const status = await getManagedUnlockStatus();
|
|
26
|
+
if (!status.enabled) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return getString(ACCOUNTS.MANAGED_UNLOCK_PASSPHRASE);
|
|
30
|
+
}
|
|
31
|
+
export async function enableManagedUnlock(passphrase) {
|
|
32
|
+
const state = await requireSetupState();
|
|
33
|
+
await storeString(ACCOUNTS.MANAGED_UNLOCK_PASSPHRASE, passphrase);
|
|
34
|
+
if (!state.managed_unlock_enabled) {
|
|
35
|
+
state.managed_unlock_enabled = true;
|
|
36
|
+
await writeState(state);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function disableManagedUnlock() {
|
|
40
|
+
const state = await readState();
|
|
41
|
+
await deleteSecret(ACCOUNTS.MANAGED_UNLOCK_PASSPHRASE).catch(() => { });
|
|
42
|
+
if (state?.setup_complete && state.managed_unlock_enabled) {
|
|
43
|
+
state.managed_unlock_enabled = false;
|
|
44
|
+
await writeState(state);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Platform } from "../contract-types.js";
|
|
2
|
+
export type PendingSetupStatus = "pending" | "authorized";
|
|
3
|
+
export interface PendingSetupState {
|
|
4
|
+
status: PendingSetupStatus;
|
|
5
|
+
server_url: string;
|
|
6
|
+
device_name: string;
|
|
7
|
+
platform: Platform;
|
|
8
|
+
device_code: string;
|
|
9
|
+
user_code: string;
|
|
10
|
+
verification_uri: string;
|
|
11
|
+
verification_uri_complete: string;
|
|
12
|
+
interval_seconds: number;
|
|
13
|
+
expires_at: string;
|
|
14
|
+
started_at: string;
|
|
15
|
+
authorized_at?: string;
|
|
16
|
+
device_id?: string;
|
|
17
|
+
user_id?: string;
|
|
18
|
+
account_label?: string | null;
|
|
19
|
+
}
|
|
20
|
+
export declare function readPendingSetup(): Promise<PendingSetupState | null>;
|
|
21
|
+
export declare function writePendingSetup(state: PendingSetupState): Promise<void>;
|
|
22
|
+
export declare function deletePendingSetup(): Promise<void>;
|
|
23
|
+
export declare function isPendingSetupExpired(state: Pick<PendingSetupState, "status" | "expires_at">): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { PENDING_SETUP_FILE, resolveStateRoot } from "../constants.js";
|
|
4
|
+
function pendingSetupPath() {
|
|
5
|
+
return join(resolveStateRoot(), PENDING_SETUP_FILE);
|
|
6
|
+
}
|
|
7
|
+
export async function readPendingSetup() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readFile(pendingSetupPath(), "utf-8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function writePendingSetup(state) {
|
|
17
|
+
const path = pendingSetupPath();
|
|
18
|
+
await mkdir(dirname(path), { recursive: true });
|
|
19
|
+
const tmp = `${path}.tmp.${Date.now()}`;
|
|
20
|
+
await writeFile(tmp, JSON.stringify(state, null, 2));
|
|
21
|
+
await rename(tmp, path);
|
|
22
|
+
}
|
|
23
|
+
export async function deletePendingSetup() {
|
|
24
|
+
await unlink(pendingSetupPath()).catch(() => { });
|
|
25
|
+
}
|
|
26
|
+
export function isPendingSetupExpired(state) {
|
|
27
|
+
if (state.status !== "pending") {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const expiresAt = Date.parse(state.expires_at);
|
|
31
|
+
return Number.isFinite(expiresAt) && expiresAt <= Date.now();
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory vault session holding the unlocked AMK.
|
|
3
|
+
* Exists for the lifetime of the gateway process.
|
|
4
|
+
*/
|
|
5
|
+
export declare class VaultSession {
|
|
6
|
+
private amk;
|
|
7
|
+
isUnlocked(): boolean;
|
|
8
|
+
getAMK(): Uint8Array;
|
|
9
|
+
unlock(amk: Uint8Array): void;
|
|
10
|
+
lock(): void;
|
|
11
|
+
}
|
|
12
|
+
/** Singleton session for the gateway process. */
|
|
13
|
+
export declare const vaultSession: VaultSession;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory vault session holding the unlocked AMK.
|
|
3
|
+
* Exists for the lifetime of the gateway process.
|
|
4
|
+
*/
|
|
5
|
+
export class VaultSession {
|
|
6
|
+
amk = null;
|
|
7
|
+
isUnlocked() {
|
|
8
|
+
return this.amk !== null;
|
|
9
|
+
}
|
|
10
|
+
getAMK() {
|
|
11
|
+
if (!this.amk) {
|
|
12
|
+
throw new Error("Vault is locked. Run `openclaw membox unlock` first.");
|
|
13
|
+
}
|
|
14
|
+
return this.amk;
|
|
15
|
+
}
|
|
16
|
+
unlock(amk) {
|
|
17
|
+
// Store a copy so the caller can zero their original
|
|
18
|
+
this.amk = new Uint8Array(amk);
|
|
19
|
+
}
|
|
20
|
+
lock() {
|
|
21
|
+
if (this.amk) {
|
|
22
|
+
this.amk.fill(0);
|
|
23
|
+
this.amk = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Singleton session for the gateway process. */
|
|
28
|
+
export const vaultSession = new VaultSession();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class AutoSyncService {
|
|
2
|
+
private workspaceDir;
|
|
3
|
+
private watchers;
|
|
4
|
+
private debounceTimer;
|
|
5
|
+
private running;
|
|
6
|
+
constructor(workspaceDir: string);
|
|
7
|
+
start(): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
triggerNow(): void;
|
|
10
|
+
private onFileChange;
|
|
11
|
+
private executeSync;
|
|
12
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readState } from "../store/local-state.js";
|
|
4
|
+
import { vaultSession } from "../store/session.js";
|
|
5
|
+
const DEBOUNCE_MS = 3000;
|
|
6
|
+
export class AutoSyncService {
|
|
7
|
+
workspaceDir;
|
|
8
|
+
watchers = [];
|
|
9
|
+
debounceTimer;
|
|
10
|
+
running = false;
|
|
11
|
+
constructor(workspaceDir) {
|
|
12
|
+
this.workspaceDir = workspaceDir;
|
|
13
|
+
}
|
|
14
|
+
start() {
|
|
15
|
+
if (this.running)
|
|
16
|
+
return;
|
|
17
|
+
this.running = true;
|
|
18
|
+
// Watch MEMORY.md
|
|
19
|
+
try {
|
|
20
|
+
const memoryPath = join(this.workspaceDir, "MEMORY.md");
|
|
21
|
+
const w = watch(memoryPath, () => this.onFileChange());
|
|
22
|
+
this.watchers.push(w);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// File may not exist yet
|
|
26
|
+
}
|
|
27
|
+
// Watch memory/ directory
|
|
28
|
+
try {
|
|
29
|
+
const memoryDir = join(this.workspaceDir, "memory");
|
|
30
|
+
const w = watch(memoryDir, { recursive: true }, () => this.onFileChange());
|
|
31
|
+
this.watchers.push(w);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Directory may not exist yet
|
|
35
|
+
}
|
|
36
|
+
console.log("[membox] Auto-sync started.");
|
|
37
|
+
}
|
|
38
|
+
stop() {
|
|
39
|
+
if (!this.running)
|
|
40
|
+
return;
|
|
41
|
+
this.running = false;
|
|
42
|
+
if (this.debounceTimer) {
|
|
43
|
+
clearTimeout(this.debounceTimer);
|
|
44
|
+
this.debounceTimer = undefined;
|
|
45
|
+
}
|
|
46
|
+
for (const w of this.watchers) {
|
|
47
|
+
w.close();
|
|
48
|
+
}
|
|
49
|
+
this.watchers = [];
|
|
50
|
+
console.log("[membox] Auto-sync stopped.");
|
|
51
|
+
}
|
|
52
|
+
triggerNow() {
|
|
53
|
+
if (this.debounceTimer) {
|
|
54
|
+
clearTimeout(this.debounceTimer);
|
|
55
|
+
this.debounceTimer = undefined;
|
|
56
|
+
}
|
|
57
|
+
this.executeSync();
|
|
58
|
+
}
|
|
59
|
+
onFileChange() {
|
|
60
|
+
if (this.debounceTimer) {
|
|
61
|
+
clearTimeout(this.debounceTimer);
|
|
62
|
+
}
|
|
63
|
+
this.debounceTimer = setTimeout(() => {
|
|
64
|
+
this.debounceTimer = undefined;
|
|
65
|
+
this.executeSync();
|
|
66
|
+
}, DEBOUNCE_MS);
|
|
67
|
+
}
|
|
68
|
+
async executeSync() {
|
|
69
|
+
try {
|
|
70
|
+
const state = await readState();
|
|
71
|
+
if (!state?.setup_complete || state.sync_paused)
|
|
72
|
+
return;
|
|
73
|
+
if (!vaultSession.isUnlocked())
|
|
74
|
+
return;
|
|
75
|
+
const { syncAction } = await import("../cli/sync.js");
|
|
76
|
+
await syncAction({ onConflict: "conflict-copy" });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.warn("[membox] Auto-sync error:", err instanceof Error ? err.message : err);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MemboxApiClient } from "../api/client.js";
|
|
2
|
+
import type { ScannedFile } from "./scanner.js";
|
|
3
|
+
import type { FileVersionEntry } from "../store/local-state.js";
|
|
4
|
+
export type ConflictStrategy = "local-wins" | "remote-wins" | "conflict-copy";
|
|
5
|
+
export interface ConflictItem {
|
|
6
|
+
logicalPath: string;
|
|
7
|
+
objectId: string;
|
|
8
|
+
localFile?: ScannedFile;
|
|
9
|
+
existingVersion?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ConflictResult {
|
|
12
|
+
/** If set, the caller should persist this file version update. */
|
|
13
|
+
fileVersionUpdate?: {
|
|
14
|
+
path: string;
|
|
15
|
+
entry: FileVersionEntry;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export declare function resolveConflict(item: ConflictItem, strategy: ConflictStrategy, params: {
|
|
19
|
+
amk: Uint8Array;
|
|
20
|
+
client: MemboxApiClient;
|
|
21
|
+
userId: string;
|
|
22
|
+
deviceId: string;
|
|
23
|
+
workspaceDir: string;
|
|
24
|
+
}): Promise<ConflictResult>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { writeFile, mkdir, rename } from "node:fs/promises";
|
|
2
|
+
import { resolve, dirname, relative, isAbsolute } from "node:path";
|
|
3
|
+
import { downloadAndDecrypt } from "./downloader.js";
|
|
4
|
+
import { uploadFile } from "./uploader.js";
|
|
5
|
+
import { computeSha256Hex } from "../crypto/manifest.js";
|
|
6
|
+
function isSafePath(base, logicalPath) {
|
|
7
|
+
const resolved = resolve(base, logicalPath);
|
|
8
|
+
const rel = relative(base, resolved);
|
|
9
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
10
|
+
}
|
|
11
|
+
function conflictCopyPath(logicalPath) {
|
|
12
|
+
return logicalPath.replace(/\.md$/, ".conflict.md");
|
|
13
|
+
}
|
|
14
|
+
export async function resolveConflict(item, strategy, params) {
|
|
15
|
+
switch (strategy) {
|
|
16
|
+
case "local-wins": {
|
|
17
|
+
if (!item.localFile) {
|
|
18
|
+
console.log(` Skip conflict for ${item.logicalPath}: no local file.`);
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
console.log(` Conflict ${item.logicalPath}: keeping local, uploading to remote.`);
|
|
22
|
+
const result = await uploadFile({
|
|
23
|
+
amk: params.amk,
|
|
24
|
+
content: item.localFile.content,
|
|
25
|
+
logicalPath: item.logicalPath,
|
|
26
|
+
fileKind: item.localFile.fileKind,
|
|
27
|
+
userId: params.userId,
|
|
28
|
+
deviceId: params.deviceId,
|
|
29
|
+
objectId: item.objectId,
|
|
30
|
+
objectVersion: item.existingVersion,
|
|
31
|
+
client: params.client,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
fileVersionUpdate: {
|
|
35
|
+
path: item.logicalPath,
|
|
36
|
+
entry: {
|
|
37
|
+
object_id: result.objectId,
|
|
38
|
+
version: result.version,
|
|
39
|
+
content_sha256: item.localFile.sha256,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
case "remote-wins": {
|
|
45
|
+
console.log(` Conflict ${item.logicalPath}: downloading remote version.`);
|
|
46
|
+
const result = await downloadAndDecrypt({
|
|
47
|
+
amk: params.amk,
|
|
48
|
+
objectId: item.objectId,
|
|
49
|
+
client: params.client,
|
|
50
|
+
});
|
|
51
|
+
if (!isSafePath(params.workspaceDir, result.logicalPath)) {
|
|
52
|
+
console.log(` Skipping unsafe path: ${result.logicalPath}`);
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
const outPath = resolve(params.workspaceDir, result.logicalPath);
|
|
56
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
57
|
+
const tmp = outPath + ".tmp." + Date.now();
|
|
58
|
+
await writeFile(tmp, result.content);
|
|
59
|
+
await rename(tmp, outPath);
|
|
60
|
+
return {
|
|
61
|
+
fileVersionUpdate: {
|
|
62
|
+
path: result.logicalPath,
|
|
63
|
+
entry: {
|
|
64
|
+
object_id: item.objectId,
|
|
65
|
+
version: result.version,
|
|
66
|
+
content_sha256: computeSha256Hex(result.content),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
case "conflict-copy": {
|
|
72
|
+
console.log(` Conflict ${item.logicalPath}: saving remote as conflict copy.`);
|
|
73
|
+
const result = await downloadAndDecrypt({
|
|
74
|
+
amk: params.amk,
|
|
75
|
+
objectId: item.objectId,
|
|
76
|
+
client: params.client,
|
|
77
|
+
});
|
|
78
|
+
const copyPath = conflictCopyPath(result.logicalPath);
|
|
79
|
+
if (!isSafePath(params.workspaceDir, copyPath)) {
|
|
80
|
+
console.log(` Skipping unsafe path: ${copyPath}`);
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
const outPath = resolve(params.workspaceDir, copyPath);
|
|
84
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
85
|
+
const tmp = outPath + ".tmp." + Date.now();
|
|
86
|
+
await writeFile(tmp, result.content);
|
|
87
|
+
await rename(tmp, outPath);
|
|
88
|
+
console.log(` -> ${copyPath} (merge manually, then delete)`);
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ChangeItem } from "../contract-types.js";
|
|
2
|
+
import type { LocalState } from "../store/local-state.js";
|
|
3
|
+
import type { ScannedFile } from "./scanner.js";
|
|
4
|
+
export interface SyncDiff {
|
|
5
|
+
toUpload: Array<{
|
|
6
|
+
logicalPath: string;
|
|
7
|
+
file: ScannedFile;
|
|
8
|
+
existingObjectId?: string;
|
|
9
|
+
existingVersion?: number;
|
|
10
|
+
}>;
|
|
11
|
+
toDownload: Array<{
|
|
12
|
+
objectId: string;
|
|
13
|
+
version: number;
|
|
14
|
+
changeType: "upsert" | "delete";
|
|
15
|
+
}>;
|
|
16
|
+
conflicts: Array<{
|
|
17
|
+
logicalPath: string;
|
|
18
|
+
objectId: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Compute the diff between local files and remote changes.
|
|
23
|
+
* V0.1: conflicts (both sides changed) are flagged and skipped.
|
|
24
|
+
*/
|
|
25
|
+
export declare function computeSyncDiff(localFiles: Map<string, ScannedFile>, remoteChanges: ChangeItem[], state: LocalState): SyncDiff;
|