@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
@@ -0,0 +1,75 @@
1
+ function normalizeRemoteChangeType(changeType) {
2
+ return changeType === "delete" ? "delete" : "upsert";
3
+ }
4
+ /**
5
+ * Compute the diff between local files and remote changes.
6
+ * V0.1: conflicts (both sides changed) are flagged and skipped.
7
+ */
8
+ export function computeSyncDiff(localFiles, remoteChanges, state) {
9
+ const diff = { toUpload: [], toDownload: [], conflicts: [] };
10
+ const latestRemoteChanges = new Map();
11
+ for (const change of remoteChanges) {
12
+ const existing = latestRemoteChanges.get(change.object_id);
13
+ if (!existing ||
14
+ change.version > existing.version ||
15
+ (change.version === existing.version && change.seq > existing.seq)) {
16
+ latestRemoteChanges.set(change.object_id, change);
17
+ }
18
+ }
19
+ const effectiveRemoteChanges = [...latestRemoteChanges.values()];
20
+ const remoteChangedObjectIds = new Set(effectiveRemoteChanges.map((c) => c.object_id));
21
+ // Check local files for changes since last sync
22
+ for (const [path, file] of localFiles) {
23
+ const existing = state.file_versions[path];
24
+ if (!existing) {
25
+ // New local file, upload it
26
+ diff.toUpload.push({ logicalPath: path, file });
27
+ }
28
+ else if (existing.content_sha256 !== file.sha256) {
29
+ // Local changed — check for conflict with remote
30
+ if (remoteChangedObjectIds.has(existing.object_id)) {
31
+ diff.conflicts.push({
32
+ logicalPath: path,
33
+ objectId: existing.object_id,
34
+ });
35
+ }
36
+ else {
37
+ diff.toUpload.push({
38
+ logicalPath: path,
39
+ file,
40
+ existingObjectId: existing.object_id,
41
+ existingVersion: existing.version,
42
+ });
43
+ }
44
+ }
45
+ }
46
+ // Check remote changes for new/updated objects to download
47
+ const conflictObjectIds = new Set(diff.conflicts.map((c) => c.objectId));
48
+ const uploadObjectIds = new Set(diff.toUpload.filter((u) => u.existingObjectId).map((u) => u.existingObjectId));
49
+ for (const change of effectiveRemoteChanges) {
50
+ // Skip if this object is already in conflicts or being uploaded
51
+ if (conflictObjectIds.has(change.object_id))
52
+ continue;
53
+ if (uploadObjectIds.has(change.object_id))
54
+ continue;
55
+ // Check if we already have this object locally
56
+ const localEntry = Object.entries(state.file_versions).find(([, v]) => v.object_id === change.object_id);
57
+ if (!localEntry) {
58
+ // New remote object we don't have
59
+ diff.toDownload.push({
60
+ objectId: change.object_id,
61
+ version: change.version,
62
+ changeType: normalizeRemoteChangeType(change.change_type),
63
+ });
64
+ }
65
+ else if (change.version > localEntry[1].version) {
66
+ // Remote has newer version and local hasn't changed
67
+ diff.toDownload.push({
68
+ objectId: change.object_id,
69
+ version: change.version,
70
+ changeType: normalizeRemoteChangeType(change.change_type),
71
+ });
72
+ }
73
+ }
74
+ return diff;
75
+ }
@@ -0,0 +1,16 @@
1
+ import type { MemboxApiClient } from "../api/client.js";
2
+ /**
3
+ * Download and decrypt a synced object from the Membox service.
4
+ * Fetch manifest → download blobs → unwrap DEK → decrypt content → validate hash.
5
+ */
6
+ export declare function downloadAndDecrypt(params: {
7
+ amk: Uint8Array;
8
+ objectId: string;
9
+ client: MemboxApiClient;
10
+ version?: number;
11
+ }): Promise<{
12
+ logicalPath: string;
13
+ content: Uint8Array;
14
+ version: number;
15
+ fileKind: string;
16
+ }>;
@@ -0,0 +1,73 @@
1
+ import { unwrapDEK } from "../crypto/keys.js";
2
+ import { decrypt } from "../crypto/aes-gcm.js";
3
+ import { computeSha256Hex, serializeAad } from "../crypto/manifest.js";
4
+ import { getManifest, downloadBlob } from "../api/sync.js";
5
+ function fromB64(s) {
6
+ return new Uint8Array(Buffer.from(s, "base64"));
7
+ }
8
+ /**
9
+ * Download and decrypt a synced object from the Membox service.
10
+ * Fetch manifest → download blobs → unwrap DEK → decrypt content → validate hash.
11
+ */
12
+ export async function downloadAndDecrypt(params) {
13
+ // Fetch manifest
14
+ const { manifest } = await getManifest(params.client, params.objectId, params.version);
15
+ const aadBytes = serializeAad(manifest.aad);
16
+ // Unwrap DEK from AMK
17
+ const dek = unwrapDEK(params.amk, {
18
+ encrypted_key: fromB64(manifest.dek_envelope.encrypted_dek_b64),
19
+ iv: fromB64(manifest.dek_envelope.iv_b64),
20
+ tag: fromB64(manifest.dek_envelope.tag_b64),
21
+ }, aadBytes);
22
+ try {
23
+ // Download and decrypt content blob
24
+ const contentBlobData = await downloadBlob(params.client, manifest.blob.blob_key);
25
+ const contentSealed = fromB64(contentBlobData.payload_b64);
26
+ // Verify blob-level integrity before decryption
27
+ const blobSha256 = computeSha256Hex(contentSealed);
28
+ if (contentBlobData.content_sha256 && blobSha256 !== contentBlobData.content_sha256) {
29
+ throw new Error(`Content blob hash mismatch (transport): expected ${contentBlobData.content_sha256}, got ${blobSha256}`);
30
+ }
31
+ // Cross-check: manifest.content_sha256 must also match the sealed blob
32
+ if (blobSha256 !== manifest.content_sha256) {
33
+ throw new Error(`Content blob hash mismatch (manifest): expected ${manifest.content_sha256}, got ${blobSha256}`);
34
+ }
35
+ const contentIv = fromB64(manifest.blob.iv_b64);
36
+ const contentTag = fromB64(manifest.blob.tag_b64);
37
+ // The sealed blob is ciphertext + tag; we need just ciphertext
38
+ const contentCiphertext = contentSealed.subarray(0, contentSealed.length - contentTag.length);
39
+ const content = decrypt(dek, contentCiphertext, contentIv, contentTag, aadBytes);
40
+ // Download and decrypt metadata blob
41
+ const metaBlobData = await downloadBlob(params.client, manifest.metadata.blob_key);
42
+ const metaSealed = fromB64(metaBlobData.payload_b64);
43
+ // Verify metadata blob integrity
44
+ const metaBlobSha256 = computeSha256Hex(metaSealed);
45
+ if (metaBlobData.content_sha256 && metaBlobSha256 !== metaBlobData.content_sha256) {
46
+ throw new Error(`Metadata blob hash mismatch (transport): expected ${metaBlobData.content_sha256}, got ${metaBlobSha256}`);
47
+ }
48
+ if (metaBlobSha256 !== manifest.metadata_sha256) {
49
+ throw new Error(`Metadata blob hash mismatch (manifest): expected ${manifest.metadata_sha256}, got ${metaBlobSha256}`);
50
+ }
51
+ const metaIv = fromB64(manifest.metadata.iv_b64);
52
+ const metaTag = fromB64(manifest.metadata.tag_b64);
53
+ const metaCiphertext = metaSealed.subarray(0, metaSealed.length - metaTag.length);
54
+ const metaPlain = decrypt(dek, metaCiphertext, metaIv, metaTag, aadBytes);
55
+ const metadata = JSON.parse(new TextDecoder().decode(metaPlain));
56
+ // Verify plaintext content hash against the value stored in encrypted metadata
57
+ if (metadata.content_sha256) {
58
+ const actualSha256 = computeSha256Hex(content);
59
+ if (actualSha256 !== metadata.content_sha256) {
60
+ throw new Error(`Content hash mismatch: expected ${metadata.content_sha256}, got ${actualSha256}`);
61
+ }
62
+ }
63
+ return {
64
+ logicalPath: metadata.logical_path,
65
+ content,
66
+ version: manifest.object_version,
67
+ fileKind: metadata.file_kind,
68
+ };
69
+ }
70
+ finally {
71
+ dek.fill(0);
72
+ }
73
+ }
@@ -0,0 +1,12 @@
1
+ export interface ScannedFile {
2
+ logicalPath: string;
3
+ absolutePath: string;
4
+ content: Uint8Array;
5
+ sha256: string;
6
+ fileKind: "memory_root" | "memory_daily";
7
+ }
8
+ /**
9
+ * Scan the workspace for memory files.
10
+ * Only returns MEMORY.md and memory/YYYY-MM-DD.md (V1 core coverage).
11
+ */
12
+ export declare function scanLocalMemoryFiles(workspaceDir: string): Promise<Map<string, ScannedFile>>;
@@ -0,0 +1,52 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { computeSha256Hex } from "../crypto/manifest.js";
4
+ import { MEMORY_ROOT_PATH, MEMORY_DAILY_PATTERN } from "../constants.js";
5
+ /**
6
+ * Scan the workspace for memory files.
7
+ * Only returns MEMORY.md and memory/YYYY-MM-DD.md (V1 core coverage).
8
+ */
9
+ export async function scanLocalMemoryFiles(workspaceDir) {
10
+ const result = new Map();
11
+ // MEMORY.md
12
+ const memoryPath = join(workspaceDir, MEMORY_ROOT_PATH);
13
+ try {
14
+ const content = new Uint8Array(await readFile(memoryPath));
15
+ result.set(MEMORY_ROOT_PATH, {
16
+ logicalPath: MEMORY_ROOT_PATH,
17
+ absolutePath: memoryPath,
18
+ content,
19
+ sha256: computeSha256Hex(content),
20
+ fileKind: "memory_root",
21
+ });
22
+ }
23
+ catch {
24
+ /* file doesn't exist yet — ok */
25
+ }
26
+ // memory/YYYY-MM-DD.md files
27
+ const memoryDir = join(workspaceDir, "memory");
28
+ try {
29
+ const entries = await readdir(memoryDir);
30
+ for (const entry of entries) {
31
+ const logicalPath = `memory/${entry}`;
32
+ if (!MEMORY_DAILY_PATTERN.test(logicalPath))
33
+ continue;
34
+ const absPath = join(memoryDir, entry);
35
+ const s = await stat(absPath);
36
+ if (!s.isFile())
37
+ continue;
38
+ const content = new Uint8Array(await readFile(absPath));
39
+ result.set(logicalPath, {
40
+ logicalPath,
41
+ absolutePath: absPath,
42
+ content,
43
+ sha256: computeSha256Hex(content),
44
+ fileKind: "memory_daily",
45
+ });
46
+ }
47
+ }
48
+ catch {
49
+ /* memory/ dir doesn't exist — ok */
50
+ }
51
+ return result;
52
+ }
@@ -0,0 +1,4 @@
1
+ import { type LocalState, type FileVersionEntry } from "../store/local-state.js";
2
+ export declare function updateCursor(cursor: number): Promise<void>;
3
+ export declare function updateFileVersion(path: string, entry: FileVersionEntry): Promise<void>;
4
+ export declare function getState(): Promise<LocalState>;
@@ -0,0 +1,22 @@
1
+ import { readState, writeState, } from "../store/local-state.js";
2
+ export async function updateCursor(cursor) {
3
+ const state = await readState();
4
+ if (!state)
5
+ throw new Error("State not initialized");
6
+ state.sync_cursor = cursor;
7
+ await writeState(state);
8
+ }
9
+ export async function updateFileVersion(path, entry) {
10
+ const state = await readState();
11
+ if (!state)
12
+ throw new Error("State not initialized");
13
+ state.file_versions[path] = entry;
14
+ await writeState(state);
15
+ }
16
+ export async function getState() {
17
+ const state = await readState();
18
+ if (!state) {
19
+ throw new Error("Vault not set up. Run `openclaw membox setup` first.");
20
+ }
21
+ return state;
22
+ }
@@ -0,0 +1,20 @@
1
+ import type { MemboxApiClient } from "../api/client.js";
2
+ /**
3
+ * Encrypt a local file and upload it to the Membox sync service.
4
+ * Full pipeline: genDEK → encrypt content → encrypt metadata → wrapDEK → buildManifest → upload blobs → commit.
5
+ */
6
+ export declare function uploadFile(params: {
7
+ amk: Uint8Array;
8
+ content: Uint8Array;
9
+ logicalPath: string;
10
+ fileKind: "memory_root" | "memory_daily";
11
+ userId: string;
12
+ deviceId: string;
13
+ objectId?: string;
14
+ objectVersion?: number;
15
+ client: MemboxApiClient;
16
+ }): Promise<{
17
+ objectId: string;
18
+ version: number;
19
+ seq: number;
20
+ }>;
@@ -0,0 +1,86 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { generateDEK, wrapDEK } from "../crypto/keys.js";
3
+ import { encrypt } from "../crypto/aes-gcm.js";
4
+ import { buildManifest, computeSha256Hex, serializeAad } from "../crypto/manifest.js";
5
+ import { uploadBlob, commitObject } from "../api/sync.js";
6
+ function toB64(d) {
7
+ return Buffer.from(d).toString("base64");
8
+ }
9
+ /**
10
+ * Encrypt a local file and upload it to the Membox sync service.
11
+ * Full pipeline: genDEK → encrypt content → encrypt metadata → wrapDEK → buildManifest → upload blobs → commit.
12
+ */
13
+ export async function uploadFile(params) {
14
+ const objectId = params.objectId ?? randomUUID();
15
+ const version = (params.objectVersion ?? 0) + 1;
16
+ const dek = generateDEK();
17
+ try {
18
+ const aad = {
19
+ user_id: params.userId,
20
+ object_id: objectId,
21
+ object_version: version,
22
+ created_by_device_id: params.deviceId,
23
+ };
24
+ const aadBytes = serializeAad(aad);
25
+ // Encrypt content blob
26
+ const contentResult = encrypt(dek, params.content, aadBytes);
27
+ const contentSealed = new Uint8Array([
28
+ ...contentResult.ciphertext,
29
+ ...contentResult.tag,
30
+ ]);
31
+ const contentSealedSha256 = computeSha256Hex(contentSealed);
32
+ const contentUpload = await uploadBlob(params.client, {
33
+ payload_b64: toB64(contentSealed),
34
+ content_sha256: contentSealedSha256,
35
+ });
36
+ // Encrypt metadata blob
37
+ const metadataPlain = new TextEncoder().encode(JSON.stringify({
38
+ logical_path: params.logicalPath,
39
+ file_kind: params.fileKind,
40
+ updated_at: new Date().toISOString(),
41
+ content_sha256: computeSha256Hex(params.content),
42
+ }));
43
+ const metaResult = encrypt(dek, metadataPlain, aadBytes);
44
+ const metaSealed = new Uint8Array([
45
+ ...metaResult.ciphertext,
46
+ ...metaResult.tag,
47
+ ]);
48
+ const metaSealedSha256 = computeSha256Hex(metaSealed);
49
+ const metaUpload = await uploadBlob(params.client, {
50
+ payload_b64: toB64(metaSealed),
51
+ content_sha256: metaSealedSha256,
52
+ });
53
+ // Wrap DEK with AMK
54
+ const dekEnvelope = wrapDEK(params.amk, dek, aadBytes);
55
+ // Build manifest
56
+ const manifest = buildManifest({
57
+ objectId,
58
+ objectVersion: version,
59
+ contentBlob: {
60
+ blob_key: contentUpload.blob_key,
61
+ iv_b64: toB64(contentResult.iv),
62
+ tag_b64: toB64(contentResult.tag),
63
+ },
64
+ metadataBlob: {
65
+ blob_key: metaUpload.blob_key,
66
+ iv_b64: toB64(metaResult.iv),
67
+ tag_b64: toB64(metaResult.tag),
68
+ },
69
+ dekEnvelope: {
70
+ encrypted_dek: dekEnvelope.encrypted_key,
71
+ iv: dekEnvelope.iv,
72
+ tag: dekEnvelope.tag,
73
+ },
74
+ aad,
75
+ ciphertextSize: contentResult.ciphertext.length,
76
+ contentSha256: contentSealedSha256,
77
+ metadataSha256: metaSealedSha256,
78
+ });
79
+ // Commit manifest
80
+ const commitResult = await commitObject(params.client, { manifest });
81
+ return { objectId: commitResult.object_id, version: commitResult.version, seq: commitResult.seq };
82
+ }
83
+ finally {
84
+ dek.fill(0);
85
+ }
86
+ }
@@ -0,0 +1,17 @@
1
+ export declare function createApprovePendingGrantsTool(): {
2
+ name: string;
3
+ label: string;
4
+ description: string;
5
+ parameters: {
6
+ type: "object";
7
+ properties: {};
8
+ additionalProperties: boolean;
9
+ };
10
+ execute(): Promise<{
11
+ content: {
12
+ type: "text";
13
+ text: string;
14
+ }[];
15
+ details: unknown;
16
+ }>;
17
+ };
@@ -0,0 +1,50 @@
1
+ import { readState } from "../store/local-state.js";
2
+ import { approvePendingGrants } from "../cli/grants.js";
3
+ import { ensureVaultUnlocked } from "../cli/unlock.js";
4
+ import { jsonResult } from "./result.js";
5
+ export function createApprovePendingGrantsTool() {
6
+ return {
7
+ name: "membox_grants_approve_pending",
8
+ label: "Membox Approve Pending Grants",
9
+ description: "Approve pending trusted-device requests using the current unlocked vault session.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {},
13
+ additionalProperties: false,
14
+ },
15
+ async execute() {
16
+ const state = await readState();
17
+ if (!state?.setup_complete) {
18
+ return jsonResult({
19
+ error: true,
20
+ message: "Vault not set up. Complete setup before approving trusted-device grants.",
21
+ });
22
+ }
23
+ if (!(await ensureVaultUnlocked({
24
+ allowPrompt: false,
25
+ announceSuccess: true,
26
+ }))) {
27
+ return jsonResult({
28
+ error: true,
29
+ message: "Vault is locked. Call `membox_unlock` with a local `passphrase_file`, or explicitly enable `membox_unlock_secret_enable` first.",
30
+ });
31
+ }
32
+ try {
33
+ const result = await approvePendingGrants();
34
+ return jsonResult({
35
+ ok: true,
36
+ ...result,
37
+ message: result.total === 0
38
+ ? "No pending trusted-device approvals."
39
+ : `Grant approval complete. approved=${result.approved} skipped=${result.skipped}.`,
40
+ });
41
+ }
42
+ catch (err) {
43
+ return jsonResult({
44
+ error: true,
45
+ message: `Grant approval failed: ${err instanceof Error ? err.message : String(err)}`,
46
+ });
47
+ }
48
+ },
49
+ };
50
+ }
@@ -0,0 +1,31 @@
1
+ import type { ConflictStrategy } from "../sync/conflict.js";
2
+ export declare function createPullTool(): {
3
+ name: string;
4
+ label: string;
5
+ description: string;
6
+ parameters: {
7
+ type: "object";
8
+ properties: {
9
+ preview: {
10
+ type: "boolean";
11
+ description: string;
12
+ };
13
+ on_conflict: {
14
+ type: "string";
15
+ enum: string[];
16
+ description: string;
17
+ };
18
+ };
19
+ additionalProperties: boolean;
20
+ };
21
+ execute(_toolCallId?: string, params?: {
22
+ preview?: boolean;
23
+ on_conflict?: ConflictStrategy;
24
+ }): Promise<{
25
+ content: {
26
+ type: "text";
27
+ text: string;
28
+ }[];
29
+ details: unknown;
30
+ }>;
31
+ };
@@ -0,0 +1,71 @@
1
+ import { readState } from "../store/local-state.js";
2
+ import { pullAction } from "../cli/pull.js";
3
+ import { ensureVaultUnlocked } from "../cli/unlock.js";
4
+ import { jsonResult } from "./result.js";
5
+ export function createPullTool() {
6
+ return {
7
+ name: "membox_pull",
8
+ label: "Membox Vault Pull",
9
+ description: "Pull remote encrypted memory changes into the local workspace. Vault must already be unlocked.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {
13
+ preview: {
14
+ type: "boolean",
15
+ description: "When true, return a preview of pending downloads and conflicts without writing files.",
16
+ },
17
+ on_conflict: {
18
+ type: "string",
19
+ enum: ["local-wins", "remote-wins", "conflict-copy"],
20
+ description: "Conflict strategy to apply when local and remote changes diverge.",
21
+ },
22
+ },
23
+ additionalProperties: false,
24
+ },
25
+ async execute(_toolCallId, params) {
26
+ const state = await readState();
27
+ if (!state?.setup_complete) {
28
+ return jsonResult({
29
+ error: true,
30
+ message: "Vault not set up. Complete setup before pulling remote memory.",
31
+ });
32
+ }
33
+ if (!(await ensureVaultUnlocked({
34
+ allowPrompt: false,
35
+ announceSuccess: true,
36
+ }))) {
37
+ return jsonResult({
38
+ error: true,
39
+ message: "Vault is locked. Call `membox_unlock` with a local `passphrase_file`, or explicitly enable `membox_unlock_secret_enable` first.",
40
+ });
41
+ }
42
+ try {
43
+ const result = await pullAction({
44
+ preview: params?.preview,
45
+ onConflict: params?.on_conflict,
46
+ });
47
+ if (result.status === "locked") {
48
+ return jsonResult({
49
+ error: true,
50
+ message: result.message,
51
+ });
52
+ }
53
+ return jsonResult({
54
+ ok: true,
55
+ ...result,
56
+ message: result.status === "preview"
57
+ ? "Pull preview complete."
58
+ : result.status === "up_to_date"
59
+ ? "Already up to date."
60
+ : `Pull complete. ${result.downloaded} file(s) downloaded.`,
61
+ });
62
+ }
63
+ catch (err) {
64
+ return jsonResult({
65
+ error: true,
66
+ message: `Pull failed: ${err instanceof Error ? err.message : String(err)}`,
67
+ });
68
+ }
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,31 @@
1
+ import type { memboxConfig } from "../config.js";
2
+ export declare function createRestoreTool(cfg: memboxConfig): {
3
+ name: string;
4
+ label: string;
5
+ description: string;
6
+ parameters: {
7
+ type: "object";
8
+ properties: {
9
+ recovery_code_file: {
10
+ type: "string";
11
+ description: string;
12
+ };
13
+ new_passphrase_file: {
14
+ type: "string";
15
+ description: string;
16
+ };
17
+ };
18
+ required: string[];
19
+ additionalProperties: boolean;
20
+ };
21
+ execute(_toolCallId?: string, params?: {
22
+ recovery_code_file?: string;
23
+ new_passphrase_file?: string;
24
+ }): Promise<{
25
+ content: {
26
+ type: "text";
27
+ text: string;
28
+ }[];
29
+ details: unknown;
30
+ }>;
31
+ };
@@ -0,0 +1,96 @@
1
+ import { readState } from "../store/local-state.js";
2
+ import { authorizeDevice, pollPendingDeviceAuthorizationOnce, } from "../cli/bootstrap.js";
3
+ import { restoreAuthorizedDeviceFromRecovery } from "../cli/restore.js";
4
+ import { readSecretFile } from "./secret-file.js";
5
+ import { jsonResult } from "./result.js";
6
+ export function createRestoreTool(cfg) {
7
+ return {
8
+ name: "membox_restore",
9
+ label: "Membox Vault Restore",
10
+ description: "Restore vault access on a new machine using local recovery-code and passphrase files after browser authorization is complete.",
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ recovery_code_file: {
15
+ type: "string",
16
+ description: "Path to a local file containing the recovery code.",
17
+ },
18
+ new_passphrase_file: {
19
+ type: "string",
20
+ description: "Path to a local file containing the new vault passphrase. On Unix, the file must not be readable by group or others.",
21
+ },
22
+ },
23
+ required: ["recovery_code_file", "new_passphrase_file"],
24
+ additionalProperties: false,
25
+ },
26
+ async execute(_toolCallId, params) {
27
+ const state = await readState();
28
+ if (state?.setup_complete) {
29
+ return jsonResult({
30
+ already_setup: true,
31
+ message: "Vault is already set up on this machine. Use pull/sync/status instead of restore.",
32
+ });
33
+ }
34
+ if (!params?.recovery_code_file || !params?.new_passphrase_file) {
35
+ return jsonResult({
36
+ error: true,
37
+ message: "Missing required parameters: `recovery_code_file` and `new_passphrase_file`.",
38
+ });
39
+ }
40
+ const authStatus = await pollPendingDeviceAuthorizationOnce(cfg);
41
+ if (authStatus.status === "not_started") {
42
+ return jsonResult({
43
+ status: "not_started",
44
+ error: true,
45
+ message: "No authorized pending setup exists. Call `membox_setup_start`, send the verification link to the user, then poll with `membox_setup_poll` until authorization completes.",
46
+ });
47
+ }
48
+ if (authStatus.status === "authorization_pending" ||
49
+ authStatus.status === "slow_down") {
50
+ return jsonResult({
51
+ status: authStatus.status,
52
+ user_code: authStatus.pendingSetup?.user_code,
53
+ verification_uri_complete: authStatus.pendingSetup?.verification_uri_complete,
54
+ retry_after_seconds: authStatus.retryAfterSeconds,
55
+ message: authStatus.message ??
56
+ "Still waiting for browser authorization before restore can continue.",
57
+ });
58
+ }
59
+ if (authStatus.status === "denied" || authStatus.status === "expired") {
60
+ return jsonResult({
61
+ status: authStatus.status,
62
+ error: true,
63
+ message: authStatus.message,
64
+ });
65
+ }
66
+ try {
67
+ const [{ value: recoveryCode, resolvedPath: recoveryCodePath }, { value: passphrase, resolvedPath: passphrasePath, }] = await Promise.all([
68
+ readSecretFile(params.recovery_code_file, {
69
+ label: "Recovery code",
70
+ trimWhitespace: true,
71
+ }),
72
+ readSecretFile(params.new_passphrase_file, {
73
+ label: "New passphrase",
74
+ trimWhitespace: false,
75
+ }),
76
+ ]);
77
+ const auth = await authorizeDevice(cfg);
78
+ const result = await restoreAuthorizedDeviceFromRecovery(cfg, auth, {
79
+ recoveryCode,
80
+ passphrase,
81
+ });
82
+ return jsonResult({
83
+ ...result,
84
+ recovery_code_file: recoveryCodePath,
85
+ new_passphrase_file: passphrasePath,
86
+ });
87
+ }
88
+ catch (err) {
89
+ return jsonResult({
90
+ error: true,
91
+ message: `Restore failed: ${err instanceof Error ? err.message : String(err)}`,
92
+ });
93
+ }
94
+ },
95
+ };
96
+ }