@safekb/mcp 0.5.0 → 0.6.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 (3) hide show
  1. package/README.md +63 -2
  2. package/dist/safekb-mcp.js +3167 -516
  3. package/package.json +2 -2
@@ -2354,12 +2354,12 @@ var require_index_umd = __commonJS((exports, module) => {
2354
2354
  });
2355
2355
 
2356
2356
  // src/main.ts
2357
- import { existsSync as existsSync4 } from "node:fs";
2357
+ import { existsSync as existsSync5 } from "node:fs";
2358
2358
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2359
2359
  // package.json
2360
2360
  var package_default = {
2361
2361
  name: "@safekb/mcp",
2362
- version: "0.5.0",
2362
+ version: "0.6.0",
2363
2363
  description: "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
2364
2364
  license: "SEE LICENSE IN LICENSE",
2365
2365
  author: "Mustafa Öztürk",
@@ -2448,13 +2448,25 @@ async function followUp(done, undone, tidy) {
2448
2448
  var JSON_TIMEOUT_MS = 30000;
2449
2449
  var TRANSFER_TIMEOUT_MS = 10 * 60000;
2450
2450
  var MAX_ERROR_TEXT_CHARS = 500;
2451
+ function withoutInput(entry) {
2452
+ if (typeof entry !== "object" || entry === null)
2453
+ return entry;
2454
+ const {
2455
+ input: _input,
2456
+ ctx: _ctx,
2457
+ ...rest
2458
+ } = entry;
2459
+ return rest;
2460
+ }
2451
2461
  function describeDetail(payload) {
2452
2462
  if (typeof payload !== "object" || payload === null)
2453
2463
  return null;
2454
2464
  const detail = payload.detail;
2455
2465
  if (detail === undefined || detail === null)
2456
2466
  return null;
2457
- return typeof detail === "string" ? detail : JSON.stringify(detail);
2467
+ if (typeof detail === "string")
2468
+ return detail;
2469
+ return JSON.stringify(Array.isArray(detail) ? detail.map(withoutInput) : detail);
2458
2470
  }
2459
2471
  async function errorDetail(response) {
2460
2472
  const text = (await response.text()).trim();
@@ -2502,6 +2514,31 @@ class HttpClient {
2502
2514
  contentType: "application/json"
2503
2515
  });
2504
2516
  }
2517
+ putJson(path, body, query = {}) {
2518
+ return this.json("PUT", path, {
2519
+ query,
2520
+ body: JSON.stringify(body),
2521
+ contentType: "application/json"
2522
+ });
2523
+ }
2524
+ patchJson(path, body, query = {}) {
2525
+ return this.json("PATCH", path, {
2526
+ query,
2527
+ body: JSON.stringify(body),
2528
+ contentType: "application/json"
2529
+ });
2530
+ }
2531
+ async delete(path) {
2532
+ await this.send("DELETE", path, { timeoutMs: JSON_TIMEOUT_MS });
2533
+ }
2534
+ async postFields(path, fields) {
2535
+ const response = await this.send("POST", path, {
2536
+ body: fields.toString(),
2537
+ contentType: "application/x-www-form-urlencoded",
2538
+ timeoutMs: TRANSFER_TIMEOUT_MS
2539
+ });
2540
+ return await response.json();
2541
+ }
2505
2542
  async postForm(path, form) {
2506
2543
  const response = await this.send("POST", path, {
2507
2544
  body: form,
@@ -2559,11 +2596,319 @@ class HttpClient {
2559
2596
  }
2560
2597
  }
2561
2598
 
2599
+ // src/api/kbApi.ts
2600
+ var LINK_TYPES = [
2601
+ "relates",
2602
+ "blocks",
2603
+ "blocked_by",
2604
+ "project",
2605
+ "spec",
2606
+ "wiki",
2607
+ "supersedes"
2608
+ ];
2609
+ var TASK_PAGE_SIZE = 500;
2610
+ var CREDENTIAL_PAGE_SIZE = 200;
2611
+ var TASK_TEXT_LIMIT = 200;
2612
+ var ROW_PAGE_SIZE = 200;
2613
+ var BLOCK_BATCH_SIZE = 500;
2614
+ var ATTACHMENT_ROWS_BATCH_SIZE = 100;
2615
+ function taskOf(raw) {
2616
+ return {
2617
+ id: raw.id,
2618
+ title: raw.title,
2619
+ status: raw.status,
2620
+ priority: raw.priority ?? null,
2621
+ due_date: raw.due_date ?? null,
2622
+ due_date_label: raw.due_date_label ?? null,
2623
+ assignee_id: raw.assignee_id ?? null,
2624
+ waiting_on: raw.waiting_on ?? null,
2625
+ project_id: raw.project_id ?? null,
2626
+ parent_task_id: raw.parent_task_id ?? null,
2627
+ exec_order: raw.exec_order ?? null,
2628
+ space_id: raw.space_id ?? null,
2629
+ spec_doc_id: raw.spec_doc_id ?? null,
2630
+ external_refs: raw.external_refs ?? [],
2631
+ position: raw.position,
2632
+ merged_into: raw.merged_into ?? null
2633
+ };
2634
+ }
2635
+ function projectOf(raw) {
2636
+ return {
2637
+ id: raw.id,
2638
+ name: raw.name,
2639
+ slug: raw.slug,
2640
+ status: raw.status,
2641
+ description: raw.description ?? null,
2642
+ spec_doc_id: raw.spec_doc_id ?? null,
2643
+ space_id: raw.space_id ?? null,
2644
+ icon: raw.icon ?? null,
2645
+ color: raw.color ?? null
2646
+ };
2647
+ }
2648
+ function rowOf(raw) {
2649
+ return {
2650
+ id: raw._id,
2651
+ values: { ...raw.indexed_fields, ...raw.unindexed_fields }
2652
+ };
2653
+ }
2654
+ function fileOf(upload) {
2655
+ const form = new FormData;
2656
+ form.set("file", new Blob([upload.bytes], { type: upload.mime_type }), upload.file_name);
2657
+ return form;
2658
+ }
2659
+
2660
+ class KbApi {
2661
+ http;
2662
+ constructor(http) {
2663
+ this.http = http;
2664
+ }
2665
+ spaces() {
2666
+ return this.http.getJson("/spaces");
2667
+ }
2668
+ createSpace(space) {
2669
+ return this.http.postJson("/spaces", space);
2670
+ }
2671
+ async allowClientUploads(spaceId) {
2672
+ await this.http.putJson(`/spaces/${spaceId}`, {
2673
+ allow_client_uploads: true
2674
+ });
2675
+ }
2676
+ async tree(spaceId) {
2677
+ const tree = await this.http.getJson(`/spaces/${spaceId}/tree`, { include_archived: "true" });
2678
+ return tree.documents;
2679
+ }
2680
+ document(id) {
2681
+ return this.http.getJson(`/documents/${id}`);
2682
+ }
2683
+ async body(id) {
2684
+ const stored = await this.http.getJson(`/documents/${id}/body`);
2685
+ return stored.body;
2686
+ }
2687
+ async bodyJson(id) {
2688
+ const stored = await this.http.getJson(`/documents/${id}/body-json`);
2689
+ return stored.body_json;
2690
+ }
2691
+ createFolder(folder) {
2692
+ return this.http.postJson("/documents/folders", folder);
2693
+ }
2694
+ createDocument(document) {
2695
+ const fields = this.placed(document);
2696
+ fields.set("doc_type", document.doc_type);
2697
+ if (document.body !== null)
2698
+ fields.set("body", document.body);
2699
+ if (document.body_json !== null) {
2700
+ fields.set("body_json", JSON.stringify(document.body_json));
2701
+ }
2702
+ if (document.source_id !== null) {
2703
+ fields.set("source_id", document.source_id);
2704
+ }
2705
+ for (const tag of document.tags)
2706
+ fields.append("tags", tag);
2707
+ if (document.external_refs.length > 0) {
2708
+ fields.set("external_refs", JSON.stringify(document.external_refs));
2709
+ }
2710
+ return this.http.postFields("/documents", fields);
2711
+ }
2712
+ createVaultDocument(document) {
2713
+ const fields = this.placed(document);
2714
+ fields.set("encrypted", "true");
2715
+ fields.set("doc_id", document.doc_id);
2716
+ fields.set("enc_body", document.enc_body);
2717
+ if (document.enc_meta !== null)
2718
+ fields.set("enc_meta", document.enc_meta);
2719
+ if (document.enc_key_id !== null) {
2720
+ fields.set("enc_key_id", document.enc_key_id);
2721
+ }
2722
+ if (document.cipher !== null)
2723
+ fields.set("cipher", document.cipher);
2724
+ return this.http.postFields("/documents", fields);
2725
+ }
2726
+ placed(node) {
2727
+ const fields = new URLSearchParams;
2728
+ fields.set("title", node.title);
2729
+ fields.set("space_id", node.space_id);
2730
+ fields.set("position", String(node.position));
2731
+ if (node.parent_id !== null)
2732
+ fields.set("parent_id", node.parent_id);
2733
+ return fields;
2734
+ }
2735
+ async labelDocument(id, labels) {
2736
+ await this.http.putJson(`/documents/${id}`, labels);
2737
+ }
2738
+ async replaceContent(id, content) {
2739
+ await this.http.putJson(`/documents/${id}`, content.body_json === null ? { body: content.body } : { body_json: content.body_json });
2740
+ }
2741
+ async archiveDocument(id) {
2742
+ await this.http.postJson(`/documents/${id}/archive`, {});
2743
+ }
2744
+ async blocks(documentId) {
2745
+ const page = await this.http.getJson(`/documents/${documentId}/blocks`, { limit: 0 });
2746
+ return page.blocks;
2747
+ }
2748
+ async addBlocks(documentId, blocks) {
2749
+ await this.http.patchJson(`/documents/${documentId}/blocks`, {
2750
+ add: blocks,
2751
+ update: [],
2752
+ delete: []
2753
+ });
2754
+ }
2755
+ async attachments(documentId) {
2756
+ const page = await this.http.getJson(`/documents/${documentId}/attachments`, { limit: 0 });
2757
+ return page.attachments;
2758
+ }
2759
+ attachmentFile(documentId, attachmentId) {
2760
+ return this.http.getStream(`/documents/${documentId}/attachments/${attachmentId}/file`);
2761
+ }
2762
+ attach(documentId, upload) {
2763
+ return this.http.postForm(`/documents/${documentId}/attachments`, fileOf(upload));
2764
+ }
2765
+ async showAttachmentOnRows(documentId, attachmentId, rowIds) {
2766
+ await this.http.postJson(`/documents/${documentId}/attachments/${attachmentId}/links`, { row_ids: rowIds });
2767
+ }
2768
+ async database(id) {
2769
+ const raw = await this.http.getJson(`/databases/${id}`);
2770
+ return {
2771
+ id: raw._id,
2772
+ title: raw.title,
2773
+ description: await this.body(id),
2774
+ columns: raw.schema.columns,
2775
+ compound_indexes: raw.schema.compound_indexes ?? [],
2776
+ primary_column_id: raw.schema.primary_column_id ?? null
2777
+ };
2778
+ }
2779
+ async createDatabase(database) {
2780
+ const raw = await this.http.postJson("/databases", database);
2781
+ return { id: raw._id };
2782
+ }
2783
+ async addColumn(databaseId, column) {
2784
+ await this.http.patchJson(`/databases/${databaseId}/schema`, {
2785
+ mutations: [{ op: "add_column", column }]
2786
+ });
2787
+ }
2788
+ async addIndex(databaseId, index) {
2789
+ await this.http.patchJson(`/databases/${databaseId}/schema`, {
2790
+ mutations: [{ op: "add_compound_index", index }]
2791
+ });
2792
+ }
2793
+ views(databaseId) {
2794
+ return this.http.getJson(`/databases/${databaseId}/views`);
2795
+ }
2796
+ async createView(databaseId, view) {
2797
+ await this.http.postJson(`/databases/${databaseId}/views`, view);
2798
+ }
2799
+ async* rows(databaseId) {
2800
+ const page = (number) => this.http.postJson(`/databases/${databaseId}/rows/query`, { page: number, page_size: ROW_PAGE_SIZE });
2801
+ const { total } = await page(1);
2802
+ for (let last = Math.ceil(total / ROW_PAGE_SIZE);last >= 1; last--) {
2803
+ const { rows } = await page(last);
2804
+ if (rows.length > 0)
2805
+ yield rows.map(rowOf);
2806
+ }
2807
+ }
2808
+ async createRows(databaseId, valuesList) {
2809
+ const rows = await this.http.postJson(`/databases/${databaseId}/rows/bulk`, { values_list: valuesList });
2810
+ return rows.map((row) => row._id);
2811
+ }
2812
+ async updateRow(rowId, values) {
2813
+ await this.http.patchJson(`/databases/rows/${rowId}`, { values });
2814
+ }
2815
+ rowBody(rowId) {
2816
+ return this.http.getJson(`/databases/rows/${rowId}/body`);
2817
+ }
2818
+ async replaceRowBody(rowId, blocks) {
2819
+ await this.http.putJson(`/databases/rows/${rowId}/body`, { blocks });
2820
+ }
2821
+ rowAttachments(databaseId, rowIds) {
2822
+ return this.http.postJson(`/databases/${databaseId}/rows/attachments-batch`, {
2823
+ row_ids: rowIds
2824
+ });
2825
+ }
2826
+ rowAttachmentFile(rowId, attachmentId) {
2827
+ return this.http.getStream(`/databases/rows/${rowId}/attachments/${attachmentId}/file`);
2828
+ }
2829
+ attachToRow(rowId, upload) {
2830
+ return this.http.postForm(`/databases/rows/${rowId}/attachments`, fileOf(upload));
2831
+ }
2832
+ async* credentials() {
2833
+ for (const archived of ["false", "true"]) {
2834
+ for (let offset = 0;; offset += CREDENTIAL_PAGE_SIZE) {
2835
+ const page = await this.http.getJson("/credentials", {
2836
+ archived,
2837
+ limit: CREDENTIAL_PAGE_SIZE,
2838
+ offset,
2839
+ sort: "created_at"
2840
+ });
2841
+ yield* page;
2842
+ if (page.length < CREDENTIAL_PAGE_SIZE)
2843
+ break;
2844
+ }
2845
+ }
2846
+ }
2847
+ async reveal(credentialId, field) {
2848
+ const revealed = await this.http.postJson(`/credentials/${credentialId}/reveal`, { field });
2849
+ return revealed.value;
2850
+ }
2851
+ createCredential(credential) {
2852
+ return this.http.postJson("/credentials", credential);
2853
+ }
2854
+ archiveCredential(id) {
2855
+ return this.http.delete(`/credentials/${id}`);
2856
+ }
2857
+ async projects() {
2858
+ const projects = await this.http.getJson("/projects");
2859
+ return projects.map(projectOf);
2860
+ }
2861
+ async createProject(project) {
2862
+ return projectOf(await this.http.postJson("/projects", project));
2863
+ }
2864
+ async* tasks() {
2865
+ for (let skip = 0;; skip += TASK_PAGE_SIZE) {
2866
+ const page = await this.http.getJson("/tasks", {
2867
+ limit: TASK_PAGE_SIZE,
2868
+ skip
2869
+ });
2870
+ yield* page.map(taskOf);
2871
+ if (page.length < TASK_PAGE_SIZE)
2872
+ return;
2873
+ }
2874
+ }
2875
+ createTask(task) {
2876
+ return this.http.postJson("/tasks", task);
2877
+ }
2878
+ decisions(projectId) {
2879
+ return this.http.getJson(`/projects/${projectId}/decisions`, {
2880
+ compact: "false"
2881
+ });
2882
+ }
2883
+ createDecision(decision) {
2884
+ return this.http.postJson("/decisions", decision);
2885
+ }
2886
+ async setDecisionStatus(id, status) {
2887
+ await this.http.patchJson(`/decisions/${id}`, { status });
2888
+ }
2889
+ async outgoingLinks(type, id) {
2890
+ const graph = await this.http.getJson(`/links/${type}/${id}`);
2891
+ return graph.outgoing;
2892
+ }
2893
+ async createLink(link) {
2894
+ const ownType = LINK_TYPES.includes(link.link_type);
2895
+ if (ownType || link.source.type !== "doc" || link.target.type !== "doc") {
2896
+ await this.http.postJson("/links", link);
2897
+ return;
2898
+ }
2899
+ await this.http.postJson(`/documents/${link.source.id}/links`, {
2900
+ target_id: link.target.id,
2901
+ link_type: link.link_type,
2902
+ note: link.note
2903
+ });
2904
+ }
2905
+ }
2906
+
2562
2907
  // src/api/sealedApi.ts
2563
2908
  var KEY_MODE_SEALED = "sealed";
2564
2909
  var MAX_PAGE_SIZE = 500;
2565
2910
  var MAX_INDEX_ENTRY_IDS = 100;
2566
- var CREDENTIAL_PAGE_SIZE = 200;
2911
+ var CREDENTIAL_PAGE_SIZE2 = 200;
2567
2912
  function toSealedNode(document) {
2568
2913
  const { seal_root_id, seal_seq, enc_meta } = document;
2569
2914
  if (document.key_mode !== KEY_MODE_SEALED || typeof seal_root_id !== "string" || typeof seal_seq !== "number" || typeof enc_meta !== "string") {
@@ -2674,25 +3019,25 @@ class SealedApi {
2674
3019
  return this.http.postForm(`/documents/${id}/sealed/attach`, form);
2675
3020
  }
2676
3021
  createPlainDocument(request) {
2677
- const form = new FormData;
2678
- form.set("title", request.title);
2679
- form.set("body", request.body);
2680
- form.set("space_id", request.space_id);
3022
+ const fields = new URLSearchParams;
3023
+ fields.set("title", request.title);
3024
+ fields.set("body", request.body);
3025
+ fields.set("space_id", request.space_id);
2681
3026
  if (request.parent_id !== null)
2682
- form.set("parent_id", request.parent_id);
3027
+ fields.set("parent_id", request.parent_id);
2683
3028
  for (const tag of request.tags)
2684
- form.append("tags", tag);
2685
- return this.http.postForm("/documents", form);
3029
+ fields.append("tags", tag);
3030
+ return this.http.postFields("/documents", fields);
2686
3031
  }
2687
3032
  getCredential(id) {
2688
3033
  return this.http.getJson(`/credentials/${id}`);
2689
3034
  }
2690
3035
  async* credentials() {
2691
3036
  for (const archived of ["false", "true"]) {
2692
- for (let offset = 0;; offset += CREDENTIAL_PAGE_SIZE) {
2693
- const page = await this.http.getJson("/credentials", { archived, limit: CREDENTIAL_PAGE_SIZE, offset, sort: "created_at" });
3037
+ for (let offset = 0;; offset += CREDENTIAL_PAGE_SIZE2) {
3038
+ const page = await this.http.getJson("/credentials", { archived, limit: CREDENTIAL_PAGE_SIZE2, offset, sort: "created_at" });
2694
3039
  yield* page;
2695
- if (page.length < CREDENTIAL_PAGE_SIZE)
3040
+ if (page.length < CREDENTIAL_PAGE_SIZE2)
2696
3041
  break;
2697
3042
  }
2698
3043
  }
@@ -6276,9 +6621,9 @@ async function authorize(request) {
6276
6621
  }
6277
6622
  }
6278
6623
 
6279
- // src/claudeCode.ts
6280
- import { spawn as spawn2 } from "node:child_process";
6281
- import { basename as basename2 } from "node:path";
6624
+ // src/backup/console.ts
6625
+ import { createReadStream, existsSync as existsSync2, rmSync } from "node:fs";
6626
+ import { open as open3, stat as stat2 } from "node:fs/promises";
6282
6627
 
6283
6628
  // src/printable.ts
6284
6629
  var UNPRINTABLE2 = /[\p{Cc}\p{Cf}]/gu;
@@ -6291,118 +6636,2435 @@ function printableLines(text) {
6291
6636
  `);
6292
6637
  }
6293
6638
 
6294
- // src/claudeCode.ts
6295
- var SERVER_NAME = "safekb";
6296
- var PACKAGE_NAME = "@safekb/mcp";
6297
- var BIN_NAME = "safekb-mcp";
6298
- var SERVE_FLAG = "--mcp";
6299
- var PACKAGE_COMMAND = ["npx", "-y", PACKAGE_NAME, SERVE_FLAG];
6300
- var SCOPES = ["user", "local", "project"];
6301
- function quotedForCmd(arg) {
6302
- return /[\s"&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
6639
+ // src/transfer/passphrase.ts
6640
+ var RECOMMENDED_EXPORT_PASSPHRASE_LENGTH = 12;
6641
+ async function readNewPassphrase(reader, guarded) {
6642
+ for (;; ) {
6643
+ const first = await reader.read(`Passphrase for the export file (not shown; ${RECOMMENDED_EXPORT_PASSPHRASE_LENGTH} or more characters recommended): `, { hidden: true });
6644
+ if (first.length === 0) {
6645
+ reader.write(`Nothing entered.
6646
+ `);
6647
+ continue;
6648
+ }
6649
+ if (first.length < RECOMMENDED_EXPORT_PASSPHRASE_LENGTH) {
6650
+ reader.write(`Note: shorter than the recommended ${RECOMMENDED_EXPORT_PASSPHRASE_LENGTH} characters; ${guarded} is only as safe as this passphrase.
6651
+ `);
6652
+ }
6653
+ const second = await reader.read("Repeat it: ", { hidden: true });
6654
+ if (first === second)
6655
+ return first;
6656
+ reader.write(`The two entries differ.
6657
+ `);
6658
+ }
6303
6659
  }
6304
- var runProgram = (program, args) => new Promise((resolve, reject) => {
6305
- const windows = process.platform === "win32";
6306
- const child = spawn2(program, windows ? args.map(quotedForCmd) : args, {
6307
- stdio: ["ignore", "pipe", "pipe"],
6308
- shell: windows
6309
- });
6310
- let output = "";
6311
- child.stdout.on("data", (chunk) => {
6312
- output += chunk;
6660
+ function readPassphrase(reader) {
6661
+ return reader.read("Passphrase of the export file (not shown): ", {
6662
+ hidden: true
6313
6663
  });
6314
- child.stderr.on("data", (chunk) => {
6315
- output += chunk;
6316
- });
6317
- child.on("error", (error) => {
6318
- if (error.code === "ENOENT")
6319
- resolve({ code: null, output: "" });
6320
- else
6321
- reject(error);
6322
- });
6323
- child.on("close", (code) => resolve({ code: code ?? 1, output }));
6324
- });
6325
- function addCommand(serverArgs) {
6326
- return [
6327
- "mcp",
6328
- "add",
6329
- "--scope",
6330
- "user",
6331
- SERVER_NAME,
6332
- "--",
6333
- ...PACKAGE_COMMAND,
6334
- ...serverArgs
6335
- ];
6336
- }
6337
- var REMOTE_TYPES = ["http", "sse"];
6338
- var ENVIRONMENT = /^([ \t]*)Environment:[ \t]*\n((?:\1[ \t]+\S.*(?:\n|$))*)/m;
6339
- var SERVE_FLAG_SINCE = [0, 5, 0];
6340
- var EXACT_VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
6341
- function field(description, name) {
6342
- return (new RegExp(`^\\s*${name}:(.*)$`, "m").exec(description)?.[1] ?? "").trim();
6343
6664
  }
6344
- function registrationOf(output) {
6345
- const description = output.replace(/\r\n?/g, `
6346
- `);
6347
- if (REMOTE_TYPES.includes(field(description, "Type")))
6348
- return null;
6349
- const command = field(description, "Command");
6350
- if (command.length === 0) {
6351
- throw new StartupError(`Claude Code has an MCP server "${SERVER_NAME}", but what it starts could not be read from "claude mcp get ${SERVER_NAME}", so it is not known whether it starts this client with ${SERVE_FLAG}. Nothing was changed.`);
6665
+
6666
+ // src/backup/archive/container.ts
6667
+ import { Readable } from "node:stream";
6668
+ import { pipeline } from "node:stream/promises";
6669
+ import { createGunzip, createGzip } from "node:zlib";
6670
+
6671
+ // src/backup/archive/byteReader.ts
6672
+ class ByteReader {
6673
+ source;
6674
+ cutShort;
6675
+ chunks = [];
6676
+ offset = 0;
6677
+ available = 0;
6678
+ ended = false;
6679
+ constructor(source, cutShort) {
6680
+ this.source = source[Symbol.asyncIterator]();
6681
+ this.cutShort = cutShort;
6682
+ }
6683
+ async readOrEnd(length) {
6684
+ while (this.available < length && await this.pull())
6685
+ ;
6686
+ if (this.available === 0 && length > 0)
6687
+ return null;
6688
+ if (this.available < length)
6689
+ throw new ClientError(this.cutShort);
6690
+ return this.take(length);
6691
+ }
6692
+ async read(length) {
6693
+ const bytes = await this.readOrEnd(length);
6694
+ if (bytes === null)
6695
+ throw new ClientError(this.cutShort);
6696
+ return bytes;
6697
+ }
6698
+ async readUntil(delimiter, limit) {
6699
+ while (this.available <= limit && await this.pull())
6700
+ ;
6701
+ const head = this.take(Math.min(this.available, limit + 1));
6702
+ const at = head.indexOf(delimiter);
6703
+ if (at < 0)
6704
+ return null;
6705
+ this.putBack(head.subarray(at + 1));
6706
+ return head.subarray(0, at);
6707
+ }
6708
+ async readUpTo(limit) {
6709
+ while (this.available === 0) {
6710
+ if (!await this.pull())
6711
+ throw new ClientError(this.cutShort);
6712
+ }
6713
+ const first = this.chunks[0];
6714
+ return this.take(Math.min(limit, first.length - this.offset));
6715
+ }
6716
+ putBack(bytes) {
6717
+ const first = this.chunks[0];
6718
+ if (first !== undefined && this.offset > 0) {
6719
+ this.chunks[0] = first.subarray(this.offset);
6720
+ this.offset = 0;
6721
+ }
6722
+ this.chunks.unshift(bytes);
6723
+ this.available += bytes.length;
6724
+ }
6725
+ take(length) {
6726
+ const bytes = new Uint8Array(length);
6727
+ let filled = 0;
6728
+ while (filled < length) {
6729
+ const first = this.chunks[0];
6730
+ const piece = first.subarray(this.offset, this.offset + length - filled);
6731
+ bytes.set(piece, filled);
6732
+ filled += piece.length;
6733
+ this.offset += piece.length;
6734
+ if (this.offset === first.length) {
6735
+ this.chunks.shift();
6736
+ this.offset = 0;
6737
+ }
6738
+ }
6739
+ this.available -= length;
6740
+ return bytes;
6741
+ }
6742
+ async pull() {
6743
+ if (this.ended)
6744
+ return false;
6745
+ const next = await this.source.next();
6746
+ if (next.done) {
6747
+ this.ended = true;
6748
+ return false;
6749
+ }
6750
+ if (next.value.length > 0) {
6751
+ this.chunks.push(next.value);
6752
+ this.available += next.value.length;
6753
+ }
6754
+ return true;
6352
6755
  }
6353
- const scope = field(description, "Scope").split(" ")[0]?.toLowerCase();
6354
- return {
6355
- scope: SCOPES.find((name) => name === scope) ?? null,
6356
- command,
6357
- args: field(description, "Args"),
6358
- environment: (ENVIRONMENT.exec(description)?.[2] ?? "").split(`
6359
- `).map((line) => line.trim().split("=")[0] ?? "").filter((name) => name.length > 0),
6360
- timeout: field(description, "Timeout")
6361
- };
6362
6756
  }
6363
- function words(registration) {
6364
- return [registration.command, ...registration.args.split(/\s+/)];
6757
+ function uint32(value) {
6758
+ const bytes = new Uint8Array(4);
6759
+ new DataView(bytes.buffer).setUint32(0, value, false);
6760
+ return bytes;
6365
6761
  }
6366
- function startsThisClient(registration) {
6367
- return words(registration).some((word) => word === PACKAGE_NAME || word.startsWith(`${PACKAGE_NAME}@`) || [BIN_NAME, `${BIN_NAME}.js`].includes(basename2(word)));
6762
+ function readUint32(bytes) {
6763
+ return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, false);
6368
6764
  }
6369
- function pinnedBeforeServeFlag(registration) {
6370
- for (const word of words(registration)) {
6371
- if (!word.startsWith(`${PACKAGE_NAME}@`))
6765
+
6766
+ // src/backup/archive/container.ts
6767
+ var ARCHIVE_KIND = "safekb-export";
6768
+ var ARCHIVE_VERSION = 1;
6769
+ var COMPRESSIONS = ["none", "gzip"];
6770
+ var KEY_INFO = "safekb/export/v1";
6771
+ var ARCHIVE_KDF = MIN_KDF_PARAMS;
6772
+ var MAX_KDF = { m: 256 * 1024, t: 8, p: 4 };
6773
+ var PART_BYTES = 1024 * 1024;
6774
+ var TAG_BYTES2 = 16;
6775
+ var NONCE_PREFIX_BYTES2 = 8;
6776
+ var MAX_PARTS = 2 ** 32;
6777
+ var MAX_HEADER_BYTES = 4096;
6778
+ var NEWLINE = 10;
6779
+ var CUT_SHORT = "The export file is cut short or damaged";
6780
+ function malformed(why) {
6781
+ return new ClientError(`Not a SafeKB export: ${why}`);
6782
+ }
6783
+ async function archiveKey(passphrase, header) {
6784
+ const ikm = await derivePassphraseIkm(passphrase, fromBase64(header.salt), header.kdf);
6785
+ return deriveKey(ikm, KEY_INFO);
6786
+ }
6787
+ function partNonce(prefix, index) {
6788
+ if (index >= MAX_PARTS)
6789
+ throw new ClientError("The export is too large");
6790
+ return concatBytes(prefix, uint32(index));
6791
+ }
6792
+ function partAad(headerDigest, part, isLast) {
6793
+ return utf8Encode(`${KEY_INFO}|${headerDigest}|${part}|${isLast ? 1 : 0}`);
6794
+ }
6795
+ async function* inParts(source, size) {
6796
+ let pieces = [];
6797
+ let held = 0;
6798
+ for await (const piece of source) {
6799
+ pieces.push(piece);
6800
+ held += piece.length;
6801
+ if (held <= size)
6372
6802
  continue;
6373
- const version = word.slice(PACKAGE_NAME.length + 1);
6374
- const numbers = EXACT_VERSION.exec(version)?.slice(1).map(Number);
6375
- if (numbers === undefined)
6376
- return null;
6377
- const differing = numbers.findIndex((number, index) => number !== SERVE_FLAG_SINCE[index]);
6378
- return differing !== -1 && numbers[differing] < SERVE_FLAG_SINCE[differing] ? version : null;
6803
+ let pending = concatBytes(...pieces);
6804
+ while (pending.length > size) {
6805
+ yield { bytes: pending.slice(0, size), isLast: false };
6806
+ pending = pending.slice(size);
6807
+ }
6808
+ pieces = [pending];
6809
+ held = pending.length;
6810
+ }
6811
+ yield { bytes: concatBytes(...pieces), isLast: true };
6812
+ }
6813
+ function through(source, transform) {
6814
+ pipeline(Readable.from(source, { objectMode: false }), transform).catch(() => {});
6815
+ return transform;
6816
+ }
6817
+ async function* sealArchive(content, passphrase, compression) {
6818
+ const prefix = randomBytes(NONCE_PREFIX_BYTES2);
6819
+ const header = {
6820
+ kind: ARCHIVE_KIND,
6821
+ v: ARCHIVE_VERSION,
6822
+ kdf: { ...ARCHIVE_KDF },
6823
+ salt: toBase64(randomBytes(SALT_BYTES)),
6824
+ nonce_prefix: toBase64(prefix),
6825
+ compression
6826
+ };
6827
+ const line = utf8Encode(JSON.stringify(header));
6828
+ const digest = await sha256Hex(line);
6829
+ const key = await archiveKey(passphrase, header);
6830
+ yield concatBytes(line, new Uint8Array([NEWLINE]));
6831
+ const plain = compression === "gzip" ? through(content, createGzip()) : content;
6832
+ let index = 0;
6833
+ for await (const part of inParts(plain, PART_BYTES)) {
6834
+ const sealed = await aesGcmEncrypt(key, partNonce(prefix, index), part.bytes, partAad(digest, index, part.isLast));
6835
+ yield concatBytes(uint32(sealed.length), sealed);
6836
+ index++;
6379
6837
  }
6380
- return null;
6381
6838
  }
6382
- function lacksServeFlag(registration) {
6383
- return startsThisClient(registration) && !words(registration).includes(SERVE_FLAG);
6384
- }
6385
- function writtenBySetup(registration) {
6386
- const { scope, command, args, environment, timeout } = registration;
6387
- if (scope !== "user" || command !== "npx" || environment.length > 0 || timeout.length > 0) {
6388
- return null;
6839
+ function parseHeader(line) {
6840
+ let value;
6841
+ try {
6842
+ value = JSON.parse(utf8Decode(line));
6843
+ } catch {
6844
+ throw malformed("it does not start with a header");
6389
6845
  }
6390
- const own = `-y ${PACKAGE_NAME}`;
6391
- if (args === own)
6392
- return [];
6393
- const config = `${own} --config `;
6394
- if (!args.startsWith(config))
6395
- return null;
6396
- const path = args.slice(config.length);
6397
- return /\s-/.test(path) ? null : ["--config", path];
6846
+ const raw = value;
6847
+ if (raw?.kind !== ARCHIVE_KIND) {
6848
+ throw malformed(`its kind is not '${ARCHIVE_KIND}'`);
6849
+ }
6850
+ if (raw.v !== ARCHIVE_VERSION) {
6851
+ throw malformed(`its version is not ${ARCHIVE_VERSION}; use the version of @safekb/mcp that wrote it`);
6852
+ }
6853
+ const { kdf, salt, nonce_prefix, compression } = raw;
6854
+ if (typeof kdf !== "object" || kdf === null || typeof salt !== "string" || typeof nonce_prefix !== "string" || !COMPRESSIONS.includes(compression)) {
6855
+ throw malformed("its header is incomplete");
6856
+ }
6857
+ const params = assertKdfParams(kdf);
6858
+ if (params.m > MAX_KDF.m || params.t > MAX_KDF.t || params.p > MAX_KDF.p) {
6859
+ throw malformed("its KDF parameters are beyond what this client runs");
6860
+ }
6861
+ if (fromBase64(nonce_prefix).length !== NONCE_PREFIX_BYTES2) {
6862
+ throw malformed("its header is incomplete");
6863
+ }
6864
+ return {
6865
+ kind: raw.kind,
6866
+ v: raw.v,
6867
+ kdf: params,
6868
+ salt,
6869
+ nonce_prefix,
6870
+ compression
6871
+ };
6398
6872
  }
6399
- function quotedForSh(word) {
6400
- return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, "'\\''")}'`;
6873
+ async function* openParts(reader, key, header, digest) {
6874
+ const prefix = fromBase64(header.nonce_prefix);
6875
+ let length = await reader.readOrEnd(4);
6876
+ for (let index = 0;length !== null; index++) {
6877
+ const size = readUint32(length);
6878
+ if (size < TAG_BYTES2 || size > PART_BYTES + TAG_BYTES2) {
6879
+ throw new ClientError(CUT_SHORT);
6880
+ }
6881
+ const sealed = await reader.read(size);
6882
+ length = await reader.readOrEnd(4);
6883
+ try {
6884
+ yield await aesGcmDecrypt(key, partNonce(prefix, index), sealed, partAad(digest, index, length === null));
6885
+ } catch (error) {
6886
+ if (!(error instanceof DecryptionError))
6887
+ throw error;
6888
+ throw new ClientError(index === 0 ? "Wrong passphrase, or the export file was changed or damaged" : CUT_SHORT);
6889
+ }
6890
+ if (length === null)
6891
+ return;
6892
+ }
6893
+ throw new ClientError(CUT_SHORT);
6401
6894
  }
6402
- function typed(command) {
6403
- return command.map(process.platform === "win32" ? quotedForCmd : quotedForSh).join(" ");
6895
+ async function* openArchive(file, passphrase) {
6896
+ const reader = new ByteReader(file, CUT_SHORT);
6897
+ const line = await reader.readUntil(NEWLINE, MAX_HEADER_BYTES);
6898
+ if (line === null)
6899
+ throw malformed("it does not start with a header");
6900
+ const header = parseHeader(line);
6901
+ const key = await archiveKey(passphrase, header);
6902
+ const parts = openParts(reader, key, header, await sha256Hex(line));
6903
+ yield* header.compression === "gzip" ? through(parts, createGunzip()) : parts;
6404
6904
  }
6405
- function howToAddServeFlag(registration) {
6905
+
6906
+ // src/backup/archive/entries.ts
6907
+ var MAX_RECORD_BYTES = 512 * 1024 * 1024;
6908
+ var CUT_SHORT2 = "The export ends in the middle of a record";
6909
+ async function* blobOf(record, blob) {
6910
+ let written = 0;
6911
+ for await (const piece of blob) {
6912
+ written += piece.length;
6913
+ yield piece;
6914
+ }
6915
+ if (written !== record.blob_bytes) {
6916
+ throw new ClientError(`Attachment ${record.id} ('${record.file_name}') is ${written} bytes, not the ${record.blob_bytes} the server reports for it`);
6917
+ }
6918
+ }
6919
+ async function* encodeEntries(entries) {
6920
+ for await (const entry of entries) {
6921
+ const json = utf8Encode(JSON.stringify(entry.record));
6922
+ yield uint32(json.length);
6923
+ yield json;
6924
+ if ("blob" in entry)
6925
+ yield* blobOf(entry.record, entry.blob);
6926
+ }
6927
+ }
6928
+ function parseRecord(json) {
6929
+ let value;
6930
+ try {
6931
+ value = JSON.parse(utf8Decode(json));
6932
+ } catch {
6933
+ throw new ClientError("The export holds a record that is not valid JSON");
6934
+ }
6935
+ if (typeof value?.kind !== "string") {
6936
+ throw new ClientError("The export holds a record without a kind");
6937
+ }
6938
+ return value;
6939
+ }
6940
+ async function* decodeEntries(content) {
6941
+ const reader = new ByteReader(content, CUT_SHORT2);
6942
+ for (;; ) {
6943
+ const length = await reader.readOrEnd(4);
6944
+ if (length === null)
6945
+ return;
6946
+ const size = readUint32(length);
6947
+ if (size > MAX_RECORD_BYTES)
6948
+ throw new ClientError(CUT_SHORT2);
6949
+ const record = parseRecord(await reader.read(size));
6950
+ if (record.kind !== "attachment") {
6951
+ yield { record };
6952
+ continue;
6953
+ }
6954
+ let left = record.blob_bytes;
6955
+ const blob = async function* () {
6956
+ while (left > 0) {
6957
+ const piece = await reader.readUpTo(left);
6958
+ left -= piece.length;
6959
+ yield piece;
6960
+ }
6961
+ }();
6962
+ yield { record, blob };
6963
+ while (left > 0)
6964
+ left -= (await reader.readUpTo(left)).length;
6965
+ }
6966
+ }
6967
+
6968
+ // src/backup/archive/records.ts
6969
+ var RECORD_FORMAT = 1;
6970
+ var ENTITY_TYPES = ["doc", "task", "project", "decision"];
6971
+
6972
+ // src/backup/import/labels.ts
6973
+ async function label(ctx, id, record) {
6974
+ const labels = {};
6975
+ if (record.kind === "folder" || record.kind === "database") {
6976
+ if (record.tags.length > 0)
6977
+ labels.tags = record.tags;
6978
+ if (record.external_refs.length > 0) {
6979
+ labels.external_refs = record.external_refs;
6980
+ }
6981
+ }
6982
+ if (record.workflow_status !== null) {
6983
+ labels.workflow_status = record.workflow_status;
6984
+ }
6985
+ if (record.due_date !== null)
6986
+ labels.due_date = record.due_date;
6987
+ if (record.due_date_label !== null) {
6988
+ labels.due_date_label = record.due_date_label;
6989
+ }
6990
+ if (Object.keys(labels).length > 0)
6991
+ await ctx.kb.labelDocument(id, labels);
6992
+ }
6993
+
6994
+ // src/backup/import/databases.ts
6995
+ var ORPHANED = "database content left out: the database or row it belongs to was not imported";
6996
+ var RELATION = "relation";
6997
+ var TARGET = "target_database_id";
6998
+ var COLUMN_SETTINGS = [
6999
+ "id",
7000
+ "name",
7001
+ "type",
7002
+ "indexed",
7003
+ "searchable",
7004
+ "embedded",
7005
+ "required",
7006
+ "options",
7007
+ "default",
7008
+ "description",
7009
+ TARGET,
7010
+ "cardinality"
7011
+ ];
7012
+ function settingsOf(column) {
7013
+ const settings = {};
7014
+ for (const name of COLUMN_SETTINGS) {
7015
+ if (column[name] !== undefined)
7016
+ settings[name] = column[name];
7017
+ }
7018
+ return settings;
7019
+ }
7020
+ function indexOf(index) {
7021
+ return { id: index.id, fields: index.fields, unique: index.unique === true };
7022
+ }
7023
+ function batches(items, size) {
7024
+ const result = [];
7025
+ for (let at = 0;at < items.length; at += size) {
7026
+ result.push(items.slice(at, at + size));
7027
+ }
7028
+ return result;
7029
+ }
7030
+
7031
+ class DatabaseImporter {
7032
+ ctx;
7033
+ relationColumns = [];
7034
+ relationIndexes = [];
7035
+ relationValues = new Map;
7036
+ leftOut = new Set;
7037
+ relationsDone = false;
7038
+ constructor(ctx) {
7039
+ this.ctx = ctx;
7040
+ }
7041
+ async database(record, spaceId) {
7042
+ const { kb, ids, report, archivals } = this.ctx;
7043
+ const later = new Set(record.columns.filter((column) => column.type === RELATION).map((column) => column.id));
7044
+ const onLater = (index) => index.fields.some((field) => later.has(field));
7045
+ const created = await kb.createDatabase({
7046
+ title: record.title,
7047
+ space_id: spaceId,
7048
+ parent_id: ids.get("node", record.parent_id),
7049
+ position: record.position,
7050
+ description: record.description === null ? null : ids.rewrite(record.description),
7051
+ columns: record.columns.filter((column) => !later.has(column.id)).map(settingsOf),
7052
+ compound_indexes: record.compound_indexes.filter((index) => !onLater(index)).map(indexOf),
7053
+ primary_column_id: record.primary_column_id
7054
+ });
7055
+ for (const column of record.columns) {
7056
+ if (later.has(column.id)) {
7057
+ this.relationColumns.push({ databaseId: created.id, column });
7058
+ }
7059
+ }
7060
+ for (const index of record.compound_indexes.filter(onLater)) {
7061
+ this.relationIndexes.push({ databaseId: created.id, index });
7062
+ }
7063
+ await label(this.ctx, created.id, record);
7064
+ ids.set("node", record.id, created.id);
7065
+ report.count("database");
7066
+ if (record.archived)
7067
+ archivals.push(() => kb.archiveDocument(created.id));
7068
+ }
7069
+ async rows(record) {
7070
+ const { kb, ids, report } = this.ctx;
7071
+ const databaseId = ids.get("node", record.database_id);
7072
+ if (databaseId === null)
7073
+ return report.note(ORPHANED);
7074
+ const relations = new Set(this.relationColumns.filter((later) => later.databaseId === databaseId).map((later) => later.column.id));
7075
+ const plain = record.rows.map((row) => ids.rewriteJson(Object.fromEntries(Object.entries(row.values).filter(([column]) => !relations.has(column)))));
7076
+ const created = await kb.createRows(databaseId, plain);
7077
+ record.rows.forEach((row, at) => {
7078
+ const id = created[at];
7079
+ ids.set("row", row.id, id);
7080
+ const related = Object.entries(row.values).filter(([column, value]) => relations.has(column) && value !== null);
7081
+ if (related.length > 0) {
7082
+ this.relationValues.set(id, {
7083
+ databaseId,
7084
+ values: Object.fromEntries(related)
7085
+ });
7086
+ }
7087
+ });
7088
+ report.count("rows", created.length);
7089
+ }
7090
+ async rowBody(record) {
7091
+ const { kb, ids } = this.ctx;
7092
+ const rowId = ids.get("row", record.row_id);
7093
+ if (rowId === null)
7094
+ return this.ctx.report.note(ORPHANED);
7095
+ await kb.replaceRowBody(rowId, ids.rewriteJson(record.blocks));
7096
+ this.ctx.report.count("row-body");
7097
+ }
7098
+ async attachmentRows(record) {
7099
+ const { kb, ids } = this.ctx;
7100
+ const documentId = ids.get("node", record.document_id);
7101
+ const attachmentId = ids.get("attachment", record.attachment_id);
7102
+ const rowIds = record.row_ids.map((id) => ids.get("row", id)).filter((id) => id !== null);
7103
+ if (documentId === null || attachmentId === null) {
7104
+ return this.ctx.report.note(ORPHANED);
7105
+ }
7106
+ for (const batch of batches(rowIds, ATTACHMENT_ROWS_BATCH_SIZE)) {
7107
+ await kb.showAttachmentOnRows(documentId, attachmentId, batch);
7108
+ }
7109
+ this.ctx.report.count("attachment-rows");
7110
+ }
7111
+ async view(record) {
7112
+ const { kb, ids, report } = this.ctx;
7113
+ await this.relations();
7114
+ const databaseId = ids.get("node", record.database_id);
7115
+ if (databaseId === null)
7116
+ return report.note(ORPHANED);
7117
+ await kb.createView(databaseId, {
7118
+ name: record.name,
7119
+ type: record.type,
7120
+ config: ids.rewriteJson(record.config),
7121
+ is_default: record.is_default
7122
+ });
7123
+ report.count("view");
7124
+ }
7125
+ async blocks(record) {
7126
+ const { kb, ids, report } = this.ctx;
7127
+ const documentId = ids.get("node", record.document_id);
7128
+ if (documentId === null) {
7129
+ return report.note("canvas content left out: its document was not imported");
7130
+ }
7131
+ for (const batch of batches(record.blocks, BLOCK_BATCH_SIZE)) {
7132
+ await kb.addBlocks(documentId, ids.rewriteJson(batch));
7133
+ }
7134
+ report.count("blocks");
7135
+ }
7136
+ async relations() {
7137
+ if (this.relationsDone)
7138
+ return;
7139
+ this.relationsDone = true;
7140
+ const { kb, ids, report } = this.ctx;
7141
+ for (const { databaseId, column } of this.relationColumns) {
7142
+ const target = ids.get("node", column[TARGET]);
7143
+ if (target === null) {
7144
+ this.leftOut.add(`${databaseId}|${column.id}`);
7145
+ report.note("relation column(s) left out: the database they point at was not imported");
7146
+ continue;
7147
+ }
7148
+ await kb.addColumn(databaseId, {
7149
+ ...settingsOf(column),
7150
+ [TARGET]: target
7151
+ });
7152
+ }
7153
+ for (const [rowId, row] of this.relationValues) {
7154
+ const related = this.related(row);
7155
+ if (Object.keys(related).length > 0)
7156
+ await kb.updateRow(rowId, related);
7157
+ }
7158
+ for (const { databaseId, index } of this.relationIndexes) {
7159
+ const usable = index.fields.every((field) => !this.leftOut.has(`${databaseId}|${field}`));
7160
+ if (usable)
7161
+ await kb.addIndex(databaseId, indexOf(index));
7162
+ }
7163
+ }
7164
+ related(row) {
7165
+ const { ids } = this.ctx;
7166
+ const related = {};
7167
+ for (const [column, value] of Object.entries(row.values)) {
7168
+ if (this.leftOut.has(`${row.databaseId}|${column}`))
7169
+ continue;
7170
+ if (Array.isArray(value)) {
7171
+ related[column] = value.map((id) => ids.get("row", id)).filter((id) => id !== null);
7172
+ } else {
7173
+ const row2 = ids.get("row", value);
7174
+ if (row2 !== null)
7175
+ related[column] = row2;
7176
+ }
7177
+ }
7178
+ return related;
7179
+ }
7180
+ }
7181
+
7182
+ // src/backup/import/idMap.ts
7183
+ var OBJECT_ID = /\b[0-9a-f]{24}\b/g;
7184
+ var TASK_LINK = "taskId";
7185
+
7186
+ class IdMap {
7187
+ byKind = new Map;
7188
+ all = new Map;
7189
+ set(kind, exported, created) {
7190
+ this.of(kind).set(exported, created);
7191
+ if (kind !== "seal-root")
7192
+ this.all.set(exported, created);
7193
+ }
7194
+ get(kind, exported) {
7195
+ return exported === null ? null : this.of(kind).get(exported) ?? null;
7196
+ }
7197
+ rewrite(text) {
7198
+ return text.replace(OBJECT_ID, (id) => this.all.get(id) ?? id);
7199
+ }
7200
+ mentionsUnknown(text) {
7201
+ return (text.match(OBJECT_ID) ?? []).some((id) => !this.all.has(id));
7202
+ }
7203
+ static mentionsId(text) {
7204
+ return text.search(OBJECT_ID) >= 0;
7205
+ }
7206
+ rewriteJson(value) {
7207
+ if (typeof value === "string")
7208
+ return this.rewrite(value);
7209
+ if (Array.isArray(value)) {
7210
+ return value.map((item) => this.rewriteJson(item));
7211
+ }
7212
+ if (typeof value !== "object" || value === null)
7213
+ return value;
7214
+ const rewritten = {};
7215
+ for (const [key, item] of Object.entries(value)) {
7216
+ if (key === TASK_LINK && typeof item === "string" && !this.all.has(item)) {
7217
+ continue;
7218
+ }
7219
+ rewritten[key] = this.rewriteJson(item);
7220
+ }
7221
+ return rewritten;
7222
+ }
7223
+ of(kind) {
7224
+ let ids = this.byKind.get(kind);
7225
+ if (ids === undefined) {
7226
+ ids = new Map;
7227
+ this.byKind.set(kind, ids);
7228
+ }
7229
+ return ids;
7230
+ }
7231
+ }
7232
+
7233
+ // src/backup/import/context.ts
7234
+ async function collect2(source) {
7235
+ const pieces = [];
7236
+ let length = 0;
7237
+ for await (const piece of source) {
7238
+ pieces.push(piece);
7239
+ length += piece.length;
7240
+ }
7241
+ const bytes = new Uint8Array(length);
7242
+ let at = 0;
7243
+ for (const piece of pieces) {
7244
+ bytes.set(piece, at);
7245
+ at += piece.length;
7246
+ }
7247
+ return bytes;
7248
+ }
7249
+
7250
+ // src/backup/import/nodes.ts
7251
+ var NOT_FOUND = 404;
7252
+
7253
+ class NodeImporter {
7254
+ ctx;
7255
+ constructor(ctx) {
7256
+ this.ctx = ctx;
7257
+ }
7258
+ async space(record) {
7259
+ const { kb, request, ids, report } = this.ctx;
7260
+ if (request.intoSpaceId !== null) {
7261
+ ids.set("space", record.id, request.intoSpaceId);
7262
+ return;
7263
+ }
7264
+ const created = await kb.createSpace({
7265
+ name: record.name,
7266
+ icon: record.icon,
7267
+ color: record.color,
7268
+ description: record.description,
7269
+ access_mode: record.access_mode,
7270
+ workflow_states: record.workflow_states,
7271
+ encryption_policy: record.encryption_policy
7272
+ });
7273
+ if (record.allow_client_uploads)
7274
+ await kb.allowClientUploads(created.id);
7275
+ ids.set("space", record.id, created.id);
7276
+ report.count("space");
7277
+ }
7278
+ async folder(record) {
7279
+ const created = await this.ctx.kb.createFolder(this.placed(record));
7280
+ await label(this.ctx, created.id, record);
7281
+ this.created(record, created.id);
7282
+ }
7283
+ async document(record) {
7284
+ const { kb, rewrites } = this.ctx;
7285
+ const written = this.contentOf(record);
7286
+ const created = await kb.createDocument({
7287
+ ...this.placed(record),
7288
+ doc_type: "document",
7289
+ ...written,
7290
+ source_id: record.source_id,
7291
+ tags: record.tags,
7292
+ external_refs: record.external_refs
7293
+ });
7294
+ await label(this.ctx, created.id, record);
7295
+ this.created(record, created.id);
7296
+ if (!IdMap.mentionsId(JSON.stringify([record.body, record.body_json]))) {
7297
+ return;
7298
+ }
7299
+ rewrites.push(async () => {
7300
+ const complete = this.contentOf(record);
7301
+ if (JSON.stringify(complete) !== JSON.stringify(written)) {
7302
+ await kb.replaceContent(created.id, complete);
7303
+ }
7304
+ });
7305
+ }
7306
+ contentOf(record) {
7307
+ const { ids } = this.ctx;
7308
+ return {
7309
+ body: record.body === null ? null : ids.rewrite(record.body),
7310
+ body_json: record.body_json === null ? null : ids.rewriteJson(record.body_json)
7311
+ };
7312
+ }
7313
+ async canvas(record) {
7314
+ const created = await this.ctx.kb.createDocument({
7315
+ ...this.placed(record),
7316
+ doc_type: "canvas",
7317
+ body: null,
7318
+ body_json: null,
7319
+ source_id: record.source_id,
7320
+ tags: record.tags,
7321
+ external_refs: record.external_refs
7322
+ });
7323
+ await label(this.ctx, created.id, record);
7324
+ this.created(record, created.id);
7325
+ }
7326
+ async vaultDocument(record) {
7327
+ const { kb, request, report } = this.ctx;
7328
+ if (record.tenant_id !== request.tenantId) {
7329
+ report.note("vault document(s) left out: they open only in the account they were encrypted for");
7330
+ return;
7331
+ }
7332
+ if (await this.exists(record.id)) {
7333
+ report.note("vault document(s) left out: they still exist in this account");
7334
+ return;
7335
+ }
7336
+ await kb.createVaultDocument({
7337
+ ...this.placed(record),
7338
+ doc_id: record.id,
7339
+ enc_body: record.enc_body,
7340
+ enc_meta: record.enc_meta,
7341
+ enc_key_id: record.enc_key_id,
7342
+ cipher: record.cipher
7343
+ });
7344
+ this.created(record, record.id);
7345
+ }
7346
+ async attachment(entry) {
7347
+ const { kb, ids, report } = this.ctx;
7348
+ const { record } = entry;
7349
+ const owner = ids.get(record.owner === "row" ? "row" : "node", record.owner_id);
7350
+ if (owner === null) {
7351
+ report.note("attachment(s) left out: what they belong to was not imported");
7352
+ return;
7353
+ }
7354
+ const upload = {
7355
+ file_name: record.file_name,
7356
+ mime_type: record.mime_type,
7357
+ bytes: await collect2(entry.blob)
7358
+ };
7359
+ const created = record.owner === "row" ? await kb.attachToRow(owner, upload) : await kb.attach(owner, upload);
7360
+ ids.set("attachment", record.id, created.id);
7361
+ report.count("attachment");
7362
+ }
7363
+ placed(record) {
7364
+ const { ids } = this.ctx;
7365
+ return {
7366
+ title: record.title,
7367
+ space_id: this.spaceOf(record.space_id),
7368
+ parent_id: ids.get("node", record.parent_id),
7369
+ position: record.position
7370
+ };
7371
+ }
7372
+ spaceOf(exported) {
7373
+ const space = this.ctx.ids.get("space", exported);
7374
+ if (space === null) {
7375
+ throw new ClientError(`The export holds something of space ${exported} but not that space`);
7376
+ }
7377
+ return space;
7378
+ }
7379
+ created(record, id) {
7380
+ const { kb, ids, report, archivals } = this.ctx;
7381
+ ids.set("node", record.id, id);
7382
+ report.count(record.kind);
7383
+ if (record.archived)
7384
+ archivals.push(() => kb.archiveDocument(id));
7385
+ }
7386
+ async exists(documentId) {
7387
+ try {
7388
+ await this.ctx.kb.document(documentId);
7389
+ return true;
7390
+ } catch (error) {
7391
+ if (error instanceof ApiError && error.status === NOT_FOUND)
7392
+ return false;
7393
+ throw error;
7394
+ }
7395
+ }
7396
+ }
7397
+
7398
+ // src/backup/import/report.ts
7399
+ class ImportReport {
7400
+ created = {};
7401
+ notes = new Map;
7402
+ sealRoots = [];
7403
+ count(kind, by = 1) {
7404
+ this.created[kind] = (this.created[kind] ?? 0) + by;
7405
+ }
7406
+ note(reason, times = 1) {
7407
+ this.notes.set(reason, (this.notes.get(reason) ?? 0) + times);
7408
+ }
7409
+ }
7410
+
7411
+ // src/stores/secretStore.ts
7412
+ var KEY_STORE_NAMES = ["system", "file"];
7413
+ var KEY_STORE_LABELS = {
7414
+ system: "the system keyring",
7415
+ file: "the key file directory"
7416
+ };
7417
+ var STORE_NAMES = [...KEY_STORE_NAMES, "sidecar"];
7418
+ function passphraseNote(seal2) {
7419
+ return `seal root ${seal2.rootId} is opened by a passphrase, which only the sidecar can hold`;
7420
+ }
7421
+
7422
+ // src/backup/import/sealed.ts
7423
+ var POSITION_DECIMALS = 6;
7424
+ var IMPORTED = "imported";
7425
+ var OBJECT_ID2 = /^[0-9a-f]{24}$/;
7426
+
7427
+ class SealedImporter {
7428
+ ctx;
7429
+ sealed;
7430
+ services;
7431
+ rootOf = new Map;
7432
+ seqOf = new Map;
7433
+ constructor(ctx, sealed, services) {
7434
+ this.ctx = ctx;
7435
+ this.sealed = sealed;
7436
+ this.services = services;
7437
+ }
7438
+ async folder(record, spaceId) {
7439
+ const place = await this.placeOf(record, spaceId);
7440
+ this.created(record, place.title, await this.services.creator.createFolder({
7441
+ name: place.title,
7442
+ ...place.args
7443
+ }));
7444
+ }
7445
+ async document(record, spaceId) {
7446
+ const place = await this.placeOf(record, spaceId);
7447
+ if (this.ctx.ids.mentionsUnknown(record.body)) {
7448
+ this.ctx.report.note("sealed document(s) name something by an id the import did not know yet; that id stays as exported");
7449
+ }
7450
+ this.created(record, place.title, await this.services.creator.insertDocument({
7451
+ title: place.title,
7452
+ body: this.ctx.ids.rewrite(record.body),
7453
+ summary: record.summary,
7454
+ summary_short: record.summary_short,
7455
+ keywords: record.keywords,
7456
+ tags: record.tags,
7457
+ ...place.args
7458
+ }));
7459
+ }
7460
+ async attachment(entry) {
7461
+ const { ids, report } = this.ctx;
7462
+ const { record } = entry;
7463
+ const documentId = ids.get("node", record.owner_id);
7464
+ if (documentId === null) {
7465
+ report.note("attachment(s) left out: what they belong to was not imported");
7466
+ return;
7467
+ }
7468
+ const bytes = await collect2(entry.blob);
7469
+ const attached = await this.services.attachments.attach({
7470
+ document_id: documentId,
7471
+ expected_seq: this.seqOf.get(documentId),
7472
+ size: bytes.length,
7473
+ file: {
7474
+ filename: record.file_name,
7475
+ mime: record.mime_type,
7476
+ bytes: [bytes]
7477
+ }
7478
+ });
7479
+ this.seqOf.set(documentId, attached.seal_seq);
7480
+ ids.set("attachment", record.id, attached.attachment_id);
7481
+ report.count("attachment");
7482
+ }
7483
+ async credential(record, spaceId) {
7484
+ const { kb, ids, report } = this.ctx;
7485
+ const rootId = ids.get("seal-root", record.seal_root_id);
7486
+ if (rootId === null) {
7487
+ report.note("sealed credential(s) left out: their seal root was not imported");
7488
+ return;
7489
+ }
7490
+ const created = await this.services.credentials.insert({
7491
+ root_id: rootId,
7492
+ space_id: spaceId,
7493
+ folder_id: ids.get("node", record.folder_id) ?? undefined,
7494
+ title: record.title,
7495
+ username: record.username ?? undefined,
7496
+ url: record.url ?? undefined,
7497
+ tags: record.tags,
7498
+ notes: record.notes === null ? undefined : ids.rewrite(record.notes),
7499
+ password: record.password ?? undefined,
7500
+ totp: record.totp ?? undefined,
7501
+ sealed_notes: record.sealed_notes === null ? undefined : ids.rewrite(record.sealed_notes),
7502
+ custom: record.custom.map((field) => ({
7503
+ key: field.key,
7504
+ value: field.value,
7505
+ sealed: field.secret
7506
+ })),
7507
+ display: {
7508
+ icon: record.icon,
7509
+ color: record.color,
7510
+ favorite: record.favorite,
7511
+ expires_at: record.expires_at,
7512
+ external_refs: record.external_refs.map(({ system, key }) => ({
7513
+ system,
7514
+ key
7515
+ }))
7516
+ }
7517
+ });
7518
+ if (record.archived)
7519
+ await kb.archiveCredential(created.id);
7520
+ ids.set("credential", record.id, created.id);
7521
+ report.count("credential");
7522
+ }
7523
+ async placeOf(record, spaceId) {
7524
+ const parentId = this.ctx.ids.get("node", record.parent_id);
7525
+ const inside = parentId !== null && this.rootOf.has(parentId);
7526
+ const ownRoot = record.id === record.seal_root_id || !inside;
7527
+ return {
7528
+ title: ownRoot ? await this.rootName(record.title) : record.title,
7529
+ args: {
7530
+ space_id: spaceId,
7531
+ parent_id: parentId ?? undefined,
7532
+ position: record.position.toFixed(POSITION_DECIMALS),
7533
+ ...ownRoot ? { store: this.ctx.request.store } : {}
7534
+ }
7535
+ };
7536
+ }
7537
+ async rootName(title) {
7538
+ const taken = new Set((await this.sealed.names.all()).values());
7539
+ const usable = (name) => !OBJECT_ID2.test(name) && !taken.has(name);
7540
+ if (usable(title))
7541
+ return title;
7542
+ for (let copy = 1;; copy++) {
7543
+ const name = `${title} (${IMPORTED}${copy === 1 ? "" : ` ${copy}`})`;
7544
+ if (!usable(name))
7545
+ continue;
7546
+ this.ctx.report.note("seal root(s) renamed, because their name cannot be given to another seal root on this machine");
7547
+ return name;
7548
+ }
7549
+ }
7550
+ created(record, title, node) {
7551
+ const { ids, report, archivals } = this.ctx;
7552
+ ids.set("node", record.id, node.id);
7553
+ this.rootOf.set(node.id, node.seal_root_id);
7554
+ this.seqOf.set(node.id, node.seal_seq);
7555
+ report.count(record.kind);
7556
+ if (record.id === record.seal_root_id) {
7557
+ ids.set("seal-root", record.id, node.id);
7558
+ }
7559
+ if (node.new_seal_root) {
7560
+ report.sealRoots.push({
7561
+ id: node.id,
7562
+ name: title,
7563
+ kept: node.secret_store === "system" || node.secret_store === "file" ? KEY_STORE_LABELS[node.secret_store] : "this machine"
7564
+ });
7565
+ }
7566
+ if (record.archived) {
7567
+ archivals.push(async () => {
7568
+ await this.services.tree.archive(node.id, this.seqOf.get(node.id));
7569
+ });
7570
+ }
7571
+ }
7572
+ }
7573
+
7574
+ // src/backup/import/work.ts
7575
+ var CONFLICT = 409;
7576
+ var ACTIVE = "active";
7577
+ var LINKED = {
7578
+ doc: "node",
7579
+ task: "task",
7580
+ project: "project",
7581
+ decision: "decision"
7582
+ };
7583
+
7584
+ class WorkImporter {
7585
+ ctx;
7586
+ takenSlugs = null;
7587
+ constructor(ctx) {
7588
+ this.ctx = ctx;
7589
+ }
7590
+ async credential(record, spaceId) {
7591
+ const { kb, ids, report } = this.ctx;
7592
+ const created = await kb.createCredential({
7593
+ space_id: spaceId,
7594
+ folder_id: ids.get("node", record.folder_id),
7595
+ title: record.title,
7596
+ username: record.username,
7597
+ url: record.url,
7598
+ tags: record.tags,
7599
+ password: record.password,
7600
+ totp: record.totp,
7601
+ notes: record.notes === null ? null : ids.rewrite(record.notes),
7602
+ custom: record.custom.map((field) => ({
7603
+ key: field.key,
7604
+ value: field.value,
7605
+ is_secret: field.secret
7606
+ })),
7607
+ icon: record.icon,
7608
+ color: record.color,
7609
+ favorite: record.favorite,
7610
+ expires_at: record.expires_at,
7611
+ external_refs: record.external_refs.map(({ system, key }) => ({
7612
+ system,
7613
+ key
7614
+ }))
7615
+ });
7616
+ if (record.archived)
7617
+ await kb.archiveCredential(created.id);
7618
+ ids.set("credential", record.id, created.id);
7619
+ report.count("credential");
7620
+ }
7621
+ async project(record) {
7622
+ const { kb, ids, report } = this.ctx;
7623
+ this.takenSlugs ??= new Set((await kb.projects()).map((project) => project.slug));
7624
+ const slug = this.freeSlug(record.slug);
7625
+ if (slug !== record.slug) {
7626
+ report.note("project(s) given a new slug, because theirs is taken here");
7627
+ }
7628
+ const created = await kb.createProject({
7629
+ name: record.name,
7630
+ slug,
7631
+ description: record.description === null ? null : ids.rewrite(record.description),
7632
+ status: record.status,
7633
+ spec_doc_id: ids.get("node", record.spec_doc_id),
7634
+ space_id: ids.get("space", record.space_id),
7635
+ icon: record.icon,
7636
+ color: record.color
7637
+ });
7638
+ this.takenSlugs.add(created.slug);
7639
+ ids.set("project", record.id, created.id);
7640
+ report.count("project");
7641
+ }
7642
+ freeSlug(slug) {
7643
+ const taken = this.takenSlugs;
7644
+ if (!taken.has(slug))
7645
+ return slug;
7646
+ for (let copy = 2;; copy++) {
7647
+ if (!taken.has(`${slug}-${copy}`))
7648
+ return `${slug}-${copy}`;
7649
+ }
7650
+ }
7651
+ async task(record) {
7652
+ const { kb, ids, report, request, sourceTenantId } = this.ctx;
7653
+ const sameTenant = sourceTenantId === request.tenantId;
7654
+ if (record.assignee_id !== null && !sameTenant) {
7655
+ report.note("task(s) left without their assignee: a person of the other account");
7656
+ }
7657
+ const created = await kb.createTask({
7658
+ title: this.fitting(record.title),
7659
+ status: record.status,
7660
+ priority: record.priority,
7661
+ due_date: record.due_date,
7662
+ due_date_label: record.due_date_label,
7663
+ assignee_id: sameTenant ? record.assignee_id : null,
7664
+ waiting_on: record.waiting_on === null ? null : this.fitting(record.waiting_on),
7665
+ project_id: ids.get("project", record.project_id),
7666
+ parent_task_id: ids.get("task", record.parent_task_id),
7667
+ exec_order: record.exec_order,
7668
+ space_id: ids.get("space", record.space_id),
7669
+ spec_doc_id: ids.get("node", record.spec_doc_id),
7670
+ external_refs: record.external_refs
7671
+ });
7672
+ ids.set("task", record.id, created.id);
7673
+ report.count("task");
7674
+ }
7675
+ fitting(text) {
7676
+ const characters = [...text];
7677
+ if (characters.length <= TASK_TEXT_LIMIT)
7678
+ return text;
7679
+ this.ctx.report.note(`task text(s) cut to the ${TASK_TEXT_LIMIT} characters the server takes for a title or 'waiting on'`);
7680
+ return `${characters.slice(0, TASK_TEXT_LIMIT - 1).join("")}…`;
7681
+ }
7682
+ async decision(record) {
7683
+ const { kb, ids, report } = this.ctx;
7684
+ const projectId = ids.get("project", record.project_id);
7685
+ if (projectId === null) {
7686
+ return report.note("decision(s) left out: their project was not imported");
7687
+ }
7688
+ const created = await kb.createDecision({
7689
+ project_id: projectId,
7690
+ title: record.title,
7691
+ rationale: ids.rewrite(record.rationale)
7692
+ });
7693
+ if (record.status !== ACTIVE) {
7694
+ await kb.setDecisionStatus(created.id, record.status);
7695
+ }
7696
+ ids.set("decision", record.id, created.id);
7697
+ report.count("decision");
7698
+ }
7699
+ async link(record) {
7700
+ const { kb, ids, report } = this.ctx;
7701
+ const source = ids.get(LINKED[record.source.type], record.source.id);
7702
+ const target = ids.get(LINKED[record.target.type], record.target.id);
7703
+ if (source === null || target === null) {
7704
+ report.note("link(s) left out: one of their ends was not imported");
7705
+ return;
7706
+ }
7707
+ try {
7708
+ await kb.createLink({
7709
+ source: { type: record.source.type, id: source },
7710
+ target: { type: record.target.type, id: target },
7711
+ link_type: record.link_type,
7712
+ note: record.note
7713
+ });
7714
+ } catch (error) {
7715
+ if (error instanceof ApiError && error.status === CONFLICT)
7716
+ return;
7717
+ throw error;
7718
+ }
7719
+ report.count("link");
7720
+ }
7721
+ }
7722
+
7723
+ // src/backup/import/importer.ts
7724
+ class ImportStopped extends ClientError {
7725
+ report;
7726
+ constructor(message, report) {
7727
+ super(message);
7728
+ this.report = report;
7729
+ }
7730
+ }
7731
+ var WHAT_STAYS = "What was imported before it stays in the account; nothing was removed.";
7732
+ function describe(record) {
7733
+ const named = record;
7734
+ const name = named.title ?? named.name ?? named.file_name;
7735
+ return name === undefined ? `a ${record.kind}` : `${record.kind} '${name}'`;
7736
+ }
7737
+
7738
+ class Importer {
7739
+ ctx;
7740
+ nodes;
7741
+ sealed;
7742
+ databases;
7743
+ work;
7744
+ progress;
7745
+ end = null;
7746
+ section = null;
7747
+ constructor(targets, request, progress) {
7748
+ this.ctx = {
7749
+ kb: targets.kb,
7750
+ request,
7751
+ ids: new IdMap,
7752
+ report: new ImportReport,
7753
+ sourceTenantId: null,
7754
+ rewrites: [],
7755
+ archivals: []
7756
+ };
7757
+ this.nodes = new NodeImporter(this.ctx);
7758
+ this.sealed = new SealedImporter(this.ctx, targets.sealed, targets.services);
7759
+ this.databases = new DatabaseImporter(this.ctx);
7760
+ this.work = new WorkImporter(this.ctx);
7761
+ this.progress = progress;
7762
+ }
7763
+ async run(entries) {
7764
+ await this.assertTargetSpace();
7765
+ let first = true;
7766
+ try {
7767
+ for await (const entry of entries) {
7768
+ if (first && entry.record.kind !== "manifest") {
7769
+ throw new ClientError("The export does not start with its manifest");
7770
+ }
7771
+ first = false;
7772
+ await this.guarded(entry.record, () => this.take(entry));
7773
+ }
7774
+ } catch (error) {
7775
+ if (first || !(error instanceof ClientError))
7776
+ throw error;
7777
+ if (error instanceof ImportStopped)
7778
+ throw error;
7779
+ throw new ImportStopped(`The import stopped: ${error.message}
7780
+ ${WHAT_STAYS}`, this.ctx.report);
7781
+ }
7782
+ if (this.end === null) {
7783
+ throw new ImportStopped("The export ends without its closing record, so it is incomplete. What was imported from it stays in the account.", this.ctx.report);
7784
+ }
7785
+ return this.ctx.report;
7786
+ }
7787
+ get leftOutByExport() {
7788
+ return this.end?.left_out ?? [];
7789
+ }
7790
+ async assertTargetSpace() {
7791
+ const { intoSpaceId } = this.ctx.request;
7792
+ if (intoSpaceId === null)
7793
+ return;
7794
+ const spaces = await this.ctx.kb.spaces();
7795
+ if (!spaces.some((space) => space.id === intoSpaceId)) {
7796
+ throw new ClientError(`Space ${intoSpaceId} does not exist or is not one this token may use`);
7797
+ }
7798
+ }
7799
+ async guarded(record, step) {
7800
+ try {
7801
+ await step();
7802
+ } catch (error) {
7803
+ if (!(error instanceof ClientError) || error instanceof ImportStopped) {
7804
+ throw error;
7805
+ }
7806
+ throw new ImportStopped(`The import stopped at ${describe(record)}: ${error.message}
7807
+ ${WHAT_STAYS}`, this.ctx.report);
7808
+ }
7809
+ }
7810
+ announce(section) {
7811
+ if (this.section === section)
7812
+ return;
7813
+ this.section = section;
7814
+ this.progress(section);
7815
+ }
7816
+ async take(entry) {
7817
+ const { record } = entry;
7818
+ switch (record.kind) {
7819
+ case "manifest":
7820
+ if (record.format !== RECORD_FORMAT) {
7821
+ throw new ClientError(`The export has record format ${record.format}, this client reads ${RECORD_FORMAT}; use the version of @safekb/mcp that wrote it`);
7822
+ }
7823
+ this.ctx.sourceTenantId = record.source.tenant_id;
7824
+ return;
7825
+ case "space":
7826
+ this.announce("Spaces");
7827
+ return this.nodes.space(record);
7828
+ case "folder":
7829
+ this.announce("Documents and folders");
7830
+ return this.nodes.folder(record);
7831
+ case "document":
7832
+ this.announce("Documents and folders");
7833
+ return this.nodes.document(record);
7834
+ case "canvas":
7835
+ this.announce("Documents and folders");
7836
+ return this.nodes.canvas(record);
7837
+ case "vault-document":
7838
+ return this.nodes.vaultDocument(record);
7839
+ case "sealed-folder":
7840
+ this.announce("Documents and folders");
7841
+ return this.sealed.folder(record, this.nodes.spaceOf(record.space_id));
7842
+ case "sealed-document":
7843
+ this.announce("Documents and folders");
7844
+ return this.sealed.document(record, this.nodes.spaceOf(record.space_id));
7845
+ case "database":
7846
+ this.announce("Documents and folders");
7847
+ return this.databases.database(record, this.nodes.spaceOf(record.space_id));
7848
+ case "attachment": {
7849
+ const file = entry;
7850
+ return record.owner === "sealed-document" ? this.sealed.attachment(file) : this.nodes.attachment(file);
7851
+ }
7852
+ case "credential": {
7853
+ this.announce("Credentials");
7854
+ const spaceId = this.nodes.spaceOf(record.space_id);
7855
+ return record.seal_root_id === null ? this.work.credential(record, spaceId) : this.sealed.credential(record, spaceId);
7856
+ }
7857
+ case "project":
7858
+ this.announce("Projects, tasks and decisions");
7859
+ return this.work.project(record);
7860
+ case "task":
7861
+ this.announce("Projects, tasks and decisions");
7862
+ return this.work.task(record);
7863
+ case "decision":
7864
+ return this.work.decision(record);
7865
+ case "blocks":
7866
+ this.announce("Canvas blocks and database rows");
7867
+ return this.databases.blocks(record);
7868
+ case "rows":
7869
+ this.announce("Canvas blocks and database rows");
7870
+ return this.databases.rows(record);
7871
+ case "row-body":
7872
+ return this.databases.rowBody(record);
7873
+ case "attachment-rows":
7874
+ return this.databases.attachmentRows(record);
7875
+ case "view":
7876
+ return this.databases.view(record);
7877
+ case "link":
7878
+ await this.databases.relations();
7879
+ this.announce("Links");
7880
+ return this.work.link(record);
7881
+ case "end":
7882
+ this.end = record;
7883
+ return this.finish();
7884
+ default:
7885
+ throw new ClientError(`The export holds a record of a kind this client does not know: ${record.kind}`);
7886
+ }
7887
+ }
7888
+ async finish() {
7889
+ await this.databases.relations();
7890
+ for (const rewrite of this.ctx.rewrites)
7891
+ await rewrite();
7892
+ for (const archive of this.ctx.archivals.reverse())
7893
+ await archive();
7894
+ }
7895
+ }
7896
+
7897
+ // src/backup/console.ts
7898
+ var EXPORT_FILE_MODE = 384;
7899
+ var NOT_IN_AN_EXPORT = "Not in an export: version history, who created what and when, shares, public links, space members, and the account's settings.";
7900
+ var LABELS = [
7901
+ ["space", "space(s)"],
7902
+ ["folder", "folder(s)"],
7903
+ ["document", "document(s)"],
7904
+ ["canvas", "canvas document(s)"],
7905
+ [
7906
+ "vault-document",
7907
+ "vault document(s), still encrypted to their owner's vault"
7908
+ ],
7909
+ ["sealed-folder", "sealed folder(s)"],
7910
+ ["sealed-document", "sealed document(s)"],
7911
+ ["database", "database(s)"],
7912
+ ["rows", "database row(s)"],
7913
+ ["view", "database view(s)"],
7914
+ ["attachment", "attachment(s)"],
7915
+ ["credential", "credential(s)"],
7916
+ ["project", "project(s)"],
7917
+ ["task", "task(s)"],
7918
+ ["decision", "decision(s)"],
7919
+ ["link", "link(s)"]
7920
+ ];
7921
+ function countLines(counts) {
7922
+ const lines = LABELS.filter(([kind]) => (counts[kind] ?? 0) > 0).map(([kind, label2]) => ` ${counts[kind]} ${label2}
7923
+ `);
7924
+ return lines.length === 0 ? ` nothing
7925
+ ` : lines.join("");
7926
+ }
7927
+ function alreadyThere(path) {
7928
+ return new ClientError(`${path} already exists; choose a new path`);
7929
+ }
7930
+ async function* bytesOf(path) {
7931
+ try {
7932
+ for await (const chunk of createReadStream(path))
7933
+ yield chunk;
7934
+ } catch (error) {
7935
+ throw fileError(error);
7936
+ }
7937
+ }
7938
+
7939
+ class BackupConsole {
7940
+ reader;
7941
+ constructor(reader) {
7942
+ this.reader = reader;
7943
+ }
7944
+ progress = (line) => {
7945
+ this.reader.write(`${printable2(line)}…
7946
+ `);
7947
+ };
7948
+ async export(exporter, path, compression) {
7949
+ if (existsSync2(path))
7950
+ throw alreadyThere(path);
7951
+ const prepared = await exporter.prepare();
7952
+ const passphrase = await readNewPassphrase(this.reader, "everything in the file");
7953
+ const file = await open3(path, "wx", EXPORT_FILE_MODE).catch((error) => {
7954
+ throw error.code === "EEXIST" ? alreadyThere(path) : fileError(error);
7955
+ });
7956
+ let complete = false;
7957
+ const discard = () => {
7958
+ if (!complete)
7959
+ rmSync(path, { force: true });
7960
+ };
7961
+ process.once("exit", discard);
7962
+ try {
7963
+ const content = encodeEntries(exporter.entries(prepared));
7964
+ for await (const part of sealArchive(content, passphrase, compression)) {
7965
+ await file.write(part);
7966
+ }
7967
+ await file.sync();
7968
+ complete = true;
7969
+ } finally {
7970
+ await file.close();
7971
+ discard();
7972
+ process.removeListener("exit", discard);
7973
+ }
7974
+ const { counts, leftOut } = exporter.written;
7975
+ this.reader.write(`Exported to ${path}:
7976
+ ${countLines(counts)}`);
7977
+ this.writeNotes("Left out", leftOut);
7978
+ this.reader.write(`${NOT_IN_AN_EXPORT}
7979
+ The file opens only with the passphrase you typed: keep both, and keep them apart.
7980
+ `);
7981
+ }
7982
+ async import(importer, path) {
7983
+ await stat2(path).catch((error) => {
7984
+ throw fileError(error);
7985
+ });
7986
+ const passphrase = await readPassphrase(this.reader);
7987
+ const entries = decodeEntries(openArchive(bytesOf(path), passphrase));
7988
+ let report;
7989
+ try {
7990
+ report = await importer.run(entries);
7991
+ } catch (error) {
7992
+ if (error instanceof ImportStopped) {
7993
+ this.writeReport("Imported before the import stopped", error.report);
7994
+ }
7995
+ throw error;
7996
+ }
7997
+ this.writeReport(`Imported from ${path}`, report);
7998
+ this.writeNotes("The export was made without", importer.leftOutByExport);
7999
+ }
8000
+ writeReport(heading, report) {
8001
+ this.reader.write(`${heading}:
8002
+ ${countLines(report.created)}`);
8003
+ this.writeNotes("Notes", [...report.notes].map(([reason, times]) => `${times} ${reason}`));
8004
+ if (report.sealRoots.length === 0)
8005
+ return;
8006
+ this.reader.write(`New seal roots, each with a newly generated key. Back the keys up (safekb-mcp --export-keys): a lost key cannot be recovered.
8007
+ `);
8008
+ for (const root of report.sealRoots) {
8009
+ this.reader.write(` "${printable2(root.name)}" (${root.id}), key in ${root.kept}
8010
+ `);
8011
+ }
8012
+ }
8013
+ writeNotes(heading, notes) {
8014
+ if (notes.length === 0)
8015
+ return;
8016
+ this.reader.write(`${heading}:
8017
+ `);
8018
+ for (const note of notes)
8019
+ this.reader.write(` ${printable2(note)}
8020
+ `);
8021
+ }
8022
+ }
8023
+
8024
+ // src/sealed/credentialAccess.ts
8025
+ function visibleOf(credential2) {
8026
+ return {
8027
+ title: credential2.title,
8028
+ username: credential2.username,
8029
+ url: credential2.url,
8030
+ tags: credential2.tags,
8031
+ notes: credential2.notes,
8032
+ custom: credential2.custom.flatMap(({ key, value }) => value === undefined ? [] : [{ key, value }])
8033
+ };
8034
+ }
8035
+ function assertDistinctCustomKeys(visible, values) {
8036
+ const keys = [...visible.custom, ...values.custom].map(({ key }) => key);
8037
+ if (keys.some((key) => key.length === 0)) {
8038
+ throw new ClientError("A custom field needs a key");
8039
+ }
8040
+ if (new Set(keys).size !== keys.length) {
8041
+ throw new ClientError("Custom field keys must be unique");
8042
+ }
8043
+ }
8044
+
8045
+ class CredentialAccess {
8046
+ ctx;
8047
+ constructor(ctx) {
8048
+ this.ctx = ctx;
8049
+ }
8050
+ context(credentialId) {
8051
+ return { tenant: this.ctx.tenant, credential: credentialId };
8052
+ }
8053
+ async newDek(rootId, credentialId) {
8054
+ const { keys } = this.ctx.session.require(rootId);
8055
+ const dek = generateDek();
8056
+ return {
8057
+ dek,
8058
+ encDek: serializeEnvelope(await wrapDek(keys, credentialId, dek))
8059
+ };
8060
+ }
8061
+ async rewrapDek(dek, rootId, credentialId) {
8062
+ const { keys } = this.ctx.session.require(rootId);
8063
+ return serializeEnvelope(await wrapDek(keys, credentialId, dek));
8064
+ }
8065
+ async open(credentialId) {
8066
+ const credential2 = await this.ctx.api.fetchSealedCredential(credentialId);
8067
+ const { keys } = this.ctx.session.require(credential2.seal_root_id);
8068
+ const dek = await unwrapDek(keys, credentialId, parseEnvelope(credential2.enc_dek));
8069
+ const values = await unsealCredentialFields(dek, this.context(credentialId), credential2.enc_fields);
8070
+ return { credential: credential2, dek, values };
8071
+ }
8072
+ async seal(credentialId, dek, visible, values) {
8073
+ assertDistinctCustomKeys(visible, values);
8074
+ return {
8075
+ visible,
8076
+ encFields: await sealCredentialFields(dek, this.context(credentialId), values),
8077
+ visibleDigest: await visibleFieldsDigest(visible)
8078
+ };
8079
+ }
8080
+ async sign(message, signerRootIds) {
8081
+ const signers = signerRootIds.map((rootId) => this.ctx.session.require(rootId).keys);
8082
+ return signMessage(await buildCredentialMessage(message), signers);
8083
+ }
8084
+ }
8085
+
8086
+ // src/sealed/nodeAccess.ts
8087
+ function assertSeq(node, expectedSeq) {
8088
+ if (node.seal_seq !== expectedSeq) {
8089
+ throw new ClientError(`${node.id} changed: its seal_seq is ${node.seal_seq}, not ${expectedSeq}. Read it again before writing.`);
8090
+ }
8091
+ }
8092
+ function assertAccepted(node) {
8093
+ if (node.deposited) {
8094
+ throw new ClientError(`${node.id} is a pending deposit; accept_sealed_deposit or reject_sealed_deposit it first`);
8095
+ }
8096
+ }
8097
+ function writeResult(node) {
8098
+ return {
8099
+ id: node.id,
8100
+ seal_root_id: node.seal_root_id,
8101
+ seal_seq: node.seal_seq
8102
+ };
8103
+ }
8104
+
8105
+ class NodeAccess {
8106
+ ctx;
8107
+ constructor(ctx) {
8108
+ this.ctx = ctx;
8109
+ }
8110
+ nodeContext(nodeId) {
8111
+ return { tenant: this.ctx.tenant, node: nodeId };
8112
+ }
8113
+ async dekOf(node) {
8114
+ const { keys } = this.ctx.session.require(node.seal_root_id);
8115
+ if (node.enc_dek !== null) {
8116
+ return unwrapDek(keys, node.id, parseEnvelope(node.enc_dek));
8117
+ }
8118
+ if (node.box_dek !== null) {
8119
+ return openBoxDek(keys, node.id, parseBoxDek(node.box_dek));
8120
+ }
8121
+ throw new ClientError(`Sealed node ${node.id} has no wrapped key`);
8122
+ }
8123
+ async open(nodeId) {
8124
+ return this.openNode(await this.ctx.api.getNode(nodeId));
8125
+ }
8126
+ async openNode(node) {
8127
+ const root = this.ctx.session.require(node.seal_root_id);
8128
+ const dek = await this.dekOf(node);
8129
+ const meta = await decryptMeta(dek, this.nodeContext(node.id), parseEnvelope(node.enc_meta));
8130
+ return { node, root, dek, meta };
8131
+ }
8132
+ async sign(message, signerRootIds) {
8133
+ const signers = signerRootIds.map((rootId) => this.ctx.session.require(rootId).keys);
8134
+ return signMessage(await buildMessage(message), signers);
8135
+ }
8136
+ async signPlain(op, node) {
8137
+ const sigs = await this.sign({
8138
+ op,
8139
+ tenant: this.ctx.tenant,
8140
+ node: node.id,
8141
+ root: node.seal_root_id,
8142
+ baseSeq: node.seal_seq
8143
+ }, [node.seal_root_id]);
8144
+ return { base_seq: node.seal_seq, sigs };
8145
+ }
8146
+ }
8147
+
8148
+ // src/backup/export/credentials.ts
8149
+ var CUSTOM_FIELD = "custom:";
8150
+
8151
+ class CredentialExporter {
8152
+ kb;
8153
+ sealed;
8154
+ roots;
8155
+ exported;
8156
+ constructor(kb, sealed, roots, exported) {
8157
+ this.kb = kb;
8158
+ this.sealed = sealed;
8159
+ this.roots = roots;
8160
+ this.exported = exported;
8161
+ }
8162
+ async* entries(credentials, sealRoots) {
8163
+ for (const credential2 of credentials) {
8164
+ const sealRootId = sealRoots.get(credential2.id) ?? null;
8165
+ if (sealRootId !== null && this.roots.isLocked(sealRootId))
8166
+ continue;
8167
+ const secrets = sealRootId === null ? await this.revealed(credential2) : await this.unsealed(credential2.id);
8168
+ this.exported.count("credential");
8169
+ yield { record: this.record(credential2, sealRootId, secrets) };
8170
+ }
8171
+ }
8172
+ record(credential2, sealRootId, secrets) {
8173
+ return {
8174
+ kind: "credential",
8175
+ id: credential2.id,
8176
+ space_id: credential2.space_id,
8177
+ folder_id: credential2.folder_id,
8178
+ seal_root_id: sealRootId,
8179
+ title: credential2.title,
8180
+ username: credential2.username,
8181
+ url: credential2.url,
8182
+ tags: credential2.tags,
8183
+ icon: credential2.icon,
8184
+ color: credential2.color,
8185
+ favorite: credential2.favorite,
8186
+ expires_at: credential2.expires_at,
8187
+ external_refs: credential2.external_refs,
8188
+ archived: credential2.archived,
8189
+ ...secrets
8190
+ };
8191
+ }
8192
+ async revealed(credential2) {
8193
+ const reveal = (has, field) => has ? this.kb.reveal(credential2.id, field) : null;
8194
+ const custom = [];
8195
+ for (const field of credential2.custom) {
8196
+ custom.push(field.is_secret === true ? {
8197
+ key: field.key,
8198
+ value: await this.kb.reveal(credential2.id, `${CUSTOM_FIELD}${field.key}`),
8199
+ secret: true
8200
+ } : { key: field.key, value: field.value ?? "", secret: false });
8201
+ }
8202
+ return {
8203
+ password: await reveal(credential2.has_password, "password"),
8204
+ totp: await reveal(credential2.has_totp, "totp"),
8205
+ notes: await reveal(credential2.has_notes, "notes"),
8206
+ sealed_notes: null,
8207
+ custom
8208
+ };
8209
+ }
8210
+ async unsealed(credentialId) {
8211
+ const { credential: credential2, values } = await this.sealed.open(credentialId);
8212
+ const visible = credential2.custom.filter((field) => field.sealed !== true && field.is_secret !== true);
8213
+ return {
8214
+ password: values.password,
8215
+ totp: values.totp,
8216
+ notes: credential2.notes,
8217
+ sealed_notes: values.sealed_notes,
8218
+ custom: [
8219
+ ...visible.map((field) => ({
8220
+ key: field.key,
8221
+ value: field.value ?? "",
8222
+ secret: false
8223
+ })),
8224
+ ...values.custom.map((field) => ({ ...field, secret: true }))
8225
+ ]
8226
+ };
8227
+ }
8228
+ }
8229
+
8230
+ // src/backup/export/databases.ts
8231
+ var OWNED = "owned";
8232
+
8233
+ class ContentExporter {
8234
+ kb;
8235
+ exported;
8236
+ constructor(kb, exported) {
8237
+ this.kb = kb;
8238
+ this.exported = exported;
8239
+ }
8240
+ async* blocks() {
8241
+ for (const documentId of this.exported.canvases) {
8242
+ const blocks = await this.kb.blocks(documentId);
8243
+ if (blocks.length === 0)
8244
+ continue;
8245
+ this.exported.count("blocks");
8246
+ yield {
8247
+ record: {
8248
+ kind: "blocks",
8249
+ document_id: documentId,
8250
+ blocks: blocks.map(blockOf)
8251
+ }
8252
+ };
8253
+ }
8254
+ }
8255
+ async* views() {
8256
+ for (const databaseId of this.exported.databases) {
8257
+ for (const view of await this.kb.views(databaseId)) {
8258
+ this.exported.count("view");
8259
+ yield {
8260
+ record: {
8261
+ kind: "view",
8262
+ database_id: databaseId,
8263
+ name: view.name,
8264
+ type: view.type,
8265
+ config: view.config,
8266
+ is_default: view.is_default
8267
+ }
8268
+ };
8269
+ }
8270
+ }
8271
+ }
8272
+ async* rows() {
8273
+ for (const databaseId of this.exported.databases) {
8274
+ for await (const rows of this.kb.rows(databaseId)) {
8275
+ for (const row of rows)
8276
+ this.exported.rows.add(row.id);
8277
+ this.exported.count("rows", rows.length);
8278
+ yield { record: { kind: "rows", database_id: databaseId, rows } };
8279
+ yield* this.bodies(rows);
8280
+ yield* this.files(databaseId, rows);
8281
+ }
8282
+ }
8283
+ }
8284
+ async* bodies(rows) {
8285
+ for (const row of rows) {
8286
+ const blocks = await this.kb.rowBody(row.id);
8287
+ if (blocks.length === 0)
8288
+ continue;
8289
+ this.exported.count("row-body");
8290
+ yield {
8291
+ record: {
8292
+ kind: "row-body",
8293
+ row_id: row.id,
8294
+ blocks: blocks.map(blockOf)
8295
+ }
8296
+ };
8297
+ }
8298
+ }
8299
+ async* files(databaseId, rows) {
8300
+ const byRow = await this.kb.rowAttachments(databaseId, rows.map((row) => row.id));
8301
+ for (const [rowId, attachments] of Object.entries(byRow)) {
8302
+ for (const attachment of attachments) {
8303
+ if (attachment.restricted) {
8304
+ this.exported.leftOut.push(`an attachment shown on row ${rowId} that this token may not read`);
8305
+ continue;
8306
+ }
8307
+ if (attachment.link !== OWNED)
8308
+ continue;
8309
+ this.exported.count("attachment");
8310
+ yield {
8311
+ record: {
8312
+ kind: "attachment",
8313
+ id: attachment.id,
8314
+ owner: "row",
8315
+ owner_id: rowId,
8316
+ file_name: attachment.file_name,
8317
+ mime_type: attachment.mime_type,
8318
+ blob_bytes: attachment.size_bytes
8319
+ },
8320
+ blob: await this.kb.rowAttachmentFile(rowId, attachment.id)
8321
+ };
8322
+ }
8323
+ }
8324
+ }
8325
+ async* attachmentRows() {
8326
+ for (const shown of this.exported.attachmentRows) {
8327
+ const rowIds = shown.rowIds.filter((id) => this.exported.rows.has(id));
8328
+ if (rowIds.length === 0)
8329
+ continue;
8330
+ this.exported.count("attachment-rows");
8331
+ yield {
8332
+ record: {
8333
+ kind: "attachment-rows",
8334
+ document_id: shown.documentId,
8335
+ attachment_id: shown.attachmentId,
8336
+ row_ids: rowIds
8337
+ }
8338
+ };
8339
+ }
8340
+ }
8341
+ }
8342
+ function blockOf(block) {
8343
+ return {
8344
+ block_id: block.block_id,
8345
+ type: block.type,
8346
+ content: block.content ?? null,
8347
+ attrs: block.attrs ?? {},
8348
+ position: block.position,
8349
+ parent_block_id: block.parent_block_id ?? null
8350
+ };
8351
+ }
8352
+
8353
+ // src/backup/export/exported.ts
8354
+ class Exported {
8355
+ entities = {
8356
+ doc: new Set,
8357
+ task: new Set,
8358
+ project: new Set,
8359
+ decision: new Set
8360
+ };
8361
+ rows = new Set;
8362
+ canvases = [];
8363
+ databases = [];
8364
+ attachmentRows = [];
8365
+ counts = {};
8366
+ leftOut = [];
8367
+ count(kind, by = 1) {
8368
+ this.counts[kind] = (this.counts[kind] ?? 0) + by;
8369
+ }
8370
+ }
8371
+
8372
+ // src/backup/export/links.ts
8373
+ class LinkExporter {
8374
+ kb;
8375
+ exported;
8376
+ constructor(kb, exported) {
8377
+ this.kb = kb;
8378
+ this.exported = exported;
8379
+ }
8380
+ async* entries() {
8381
+ let leadingOut = 0;
8382
+ for (const type of ENTITY_TYPES) {
8383
+ for (const id of this.exported.entities[type]) {
8384
+ for (const link of await this.kb.outgoingLinks(type, id)) {
8385
+ const inside = link.target_tenant_id === null && this.exported.entities[link.target_type]?.has(link.target_id);
8386
+ if (!inside) {
8387
+ leadingOut++;
8388
+ continue;
8389
+ }
8390
+ this.exported.count("link");
8391
+ yield {
8392
+ record: {
8393
+ kind: "link",
8394
+ source: { type: link.source_type, id: link.source_id },
8395
+ target: { type: link.target_type, id: link.target_id },
8396
+ link_type: link.link_type,
8397
+ note: link.note
8398
+ }
8399
+ };
8400
+ }
8401
+ }
8402
+ }
8403
+ if (leadingOut > 0) {
8404
+ this.exported.leftOut.push(`${leadingOut} link(s) to something that is not in the export`);
8405
+ }
8406
+ }
8407
+ }
8408
+
8409
+ // src/backup/export/nodes.ts
8410
+ var DATABASE = "database";
8411
+ var CANVAS = "canvas";
8412
+ var JSON_BODY = "json";
8413
+
8414
+ class NodeExporter {
8415
+ from;
8416
+ exported;
8417
+ constructor(from, exported) {
8418
+ this.from = from;
8419
+ this.exported = exported;
8420
+ }
8421
+ async* entries(planned) {
8422
+ if (planned.sealRootId !== null) {
8423
+ if (!this.from.roots.isLocked(planned.sealRootId)) {
8424
+ yield* this.sealed(planned);
8425
+ }
8426
+ return;
8427
+ }
8428
+ const document = await this.from.kb.document(planned.id);
8429
+ const record = await this.record(planned, document);
8430
+ this.exported.count(record.kind);
8431
+ if (record.kind !== "vault-document") {
8432
+ this.exported.entities.doc.add(planned.id);
8433
+ }
8434
+ yield { record };
8435
+ if (record.kind === "document" || record.kind === "canvas") {
8436
+ yield* this.attachments(planned.id);
8437
+ }
8438
+ }
8439
+ async record(planned, document) {
8440
+ const node = {
8441
+ id: document.id,
8442
+ space_id: document.space_id,
8443
+ parent_id: planned.parentId,
8444
+ title: document.title,
8445
+ position: document.position,
8446
+ archived: document.archived
8447
+ };
8448
+ if (document.encrypted) {
8449
+ return {
8450
+ kind: "vault-document",
8451
+ ...node,
8452
+ tenant_id: this.from.sealed.tenant,
8453
+ enc_body: await this.vaultBody(document.id),
8454
+ enc_meta: document.enc_meta,
8455
+ enc_key_id: document.enc_key_id,
8456
+ cipher: document.cipher
8457
+ };
8458
+ }
8459
+ const labelled = {
8460
+ ...node,
8461
+ tags: document.tags,
8462
+ external_refs: document.external_refs,
8463
+ workflow_status: document.workflow_status,
8464
+ due_date: document.due_date,
8465
+ due_date_label: document.due_date_label
8466
+ };
8467
+ if (document.doc_type === DATABASE) {
8468
+ const database = await this.from.kb.database(document.id);
8469
+ this.exported.databases.push(document.id);
8470
+ return {
8471
+ kind: "database",
8472
+ ...labelled,
8473
+ description: database.description,
8474
+ columns: database.columns,
8475
+ compound_indexes: database.compound_indexes,
8476
+ primary_column_id: database.primary_column_id
8477
+ };
8478
+ }
8479
+ if (document.is_folder)
8480
+ return { kind: "folder", ...labelled };
8481
+ if (document.doc_type === CANVAS) {
8482
+ this.exported.canvases.push(document.id);
8483
+ return { kind: "canvas", ...labelled, source_id: document.source_id };
8484
+ }
8485
+ const asJson = document.body_format === JSON_BODY;
8486
+ return {
8487
+ kind: "document",
8488
+ ...labelled,
8489
+ source_id: document.source_id,
8490
+ body: asJson ? null : await this.from.kb.body(document.id),
8491
+ body_json: asJson ? await this.from.kb.bodyJson(document.id) : null
8492
+ };
8493
+ }
8494
+ async vaultBody(documentId) {
8495
+ const body = await this.from.kb.body(documentId);
8496
+ if (body === null) {
8497
+ throw new ClientError(`Vault document ${documentId} has no stored body`);
8498
+ }
8499
+ return body;
8500
+ }
8501
+ async* attachments(documentId) {
8502
+ for (const attachment of await this.from.kb.attachments(documentId)) {
8503
+ this.exported.count("attachment");
8504
+ if (attachment.linked_rows.length > 0) {
8505
+ this.exported.attachmentRows.push({
8506
+ documentId,
8507
+ attachmentId: attachment.id,
8508
+ rowIds: attachment.linked_rows
8509
+ });
8510
+ }
8511
+ yield {
8512
+ record: {
8513
+ kind: "attachment",
8514
+ id: attachment.id,
8515
+ owner: "document",
8516
+ owner_id: documentId,
8517
+ file_name: attachment.file_name,
8518
+ mime_type: attachment.mime_type,
8519
+ blob_bytes: attachment.size_bytes
8520
+ },
8521
+ blob: await this.from.kb.attachmentFile(documentId, attachment.id)
8522
+ };
8523
+ }
8524
+ }
8525
+ async* sealed(planned) {
8526
+ const open4 = await this.from.access.openNode(toSealedNode(await this.from.sealed.api.getDocument(planned.id)));
8527
+ const { node, meta } = open4;
8528
+ const placed = {
8529
+ id: node.id,
8530
+ space_id: node.space_id,
8531
+ parent_id: planned.parentId,
8532
+ title: meta.title,
8533
+ position: node.position,
8534
+ archived: node.archived,
8535
+ seal_root_id: node.seal_root_id
8536
+ };
8537
+ if (node.is_folder) {
8538
+ this.exported.count("sealed-folder");
8539
+ yield { record: { kind: "sealed-folder", ...placed } };
8540
+ return;
8541
+ }
8542
+ const indexed = this.from.sealed.session.require(node.seal_root_id).index.get(node.id)?.entry;
8543
+ const record = {
8544
+ kind: "sealed-document",
8545
+ ...placed,
8546
+ tags: meta.tags,
8547
+ body: (await this.from.services.documents.storedBody(open4)).body,
8548
+ summary: indexed?.summary ?? "",
8549
+ summary_short: meta.summary_short,
8550
+ keywords: indexed?.keywords ?? []
8551
+ };
8552
+ this.exported.count("sealed-document");
8553
+ yield { record };
8554
+ yield* this.sealedAttachments(node.id);
8555
+ }
8556
+ async* sealedAttachments(documentId) {
8557
+ const owner = "sealed-document";
8558
+ for (const file of await this.from.services.attachments.files(documentId)) {
8559
+ this.exported.count("attachment");
8560
+ yield {
8561
+ record: {
8562
+ kind: "attachment",
8563
+ id: file.id,
8564
+ owner,
8565
+ owner_id: documentId,
8566
+ file_name: file.meta.filename,
8567
+ mime_type: file.meta.mime,
8568
+ blob_bytes: file.meta.size
8569
+ },
8570
+ blob: file.bytes()
8571
+ };
8572
+ }
8573
+ }
8574
+ }
8575
+
8576
+ // src/backup/export/plan.ts
8577
+ var DELETING = "deleting";
8578
+ function* walk(nodes, parentId) {
8579
+ for (const node of nodes) {
8580
+ if (node.status === DELETING)
8581
+ continue;
8582
+ yield {
8583
+ id: node.id,
8584
+ parentId,
8585
+ sealRootId: node.key_mode === KEY_MODE_SEALED ? node.seal_root_id : null
8586
+ };
8587
+ yield* walk(node.children, node.id);
8588
+ }
8589
+ }
8590
+ function find(nodes, id) {
8591
+ for (const node of nodes) {
8592
+ if (node.id === id)
8593
+ return node;
8594
+ const below = find(node.children, id);
8595
+ if (below !== null)
8596
+ return below;
8597
+ }
8598
+ return null;
8599
+ }
8600
+
8601
+ class ExportPlanner {
8602
+ kb;
8603
+ constructor(kb) {
8604
+ this.kb = kb;
8605
+ }
8606
+ async plan(selection) {
8607
+ const reachable = await this.kb.spaces();
8608
+ const whole = selection.spaces === null && selection.documents.length === 0 ? reachable : (selection.spaces ?? []).map((id) => this.spaceOf(reachable, id));
8609
+ const wholeSpaceIds = new Set(whole.map((space) => space.id));
8610
+ const nodes = new Map;
8611
+ for (const space of whole) {
8612
+ for (const node of walk(await this.kb.tree(space.id), null)) {
8613
+ nodes.set(node.id, node);
8614
+ }
8615
+ }
8616
+ const spaces = new Map(whole.map((space) => [space.id, space]));
8617
+ for (const documentId of selection.documents) {
8618
+ if (nodes.has(documentId))
8619
+ continue;
8620
+ const document = await this.kb.document(documentId);
8621
+ const space = this.spaceOf(reachable, document.space_id);
8622
+ spaces.set(space.id, space);
8623
+ const top = find(await this.kb.tree(space.id), documentId);
8624
+ if (top === null) {
8625
+ throw new ClientError(`Document ${documentId} is not in the tree of its space ${space.id}`);
8626
+ }
8627
+ for (const node of walk([top], null))
8628
+ nodes.set(node.id, node);
8629
+ }
8630
+ return {
8631
+ spaces: [...spaces.values()],
8632
+ wholeSpaceIds,
8633
+ nodes: this.parentsFirst(nodes),
8634
+ everything: selection.spaces === null && selection.documents.length === 0
8635
+ };
8636
+ }
8637
+ spaceOf(reachable, id) {
8638
+ const space = reachable.find((candidate) => candidate.id === id);
8639
+ if (space === undefined) {
8640
+ throw new ClientError(`Space ${id} does not exist or is not one this token may use`);
8641
+ }
8642
+ return space;
8643
+ }
8644
+ parentsFirst(nodes) {
8645
+ const ordered = [];
8646
+ const placed = new Set;
8647
+ const place = (node) => {
8648
+ if (placed.has(node.id))
8649
+ return;
8650
+ const parent = node.parentId === null ? undefined : nodes.get(node.parentId);
8651
+ if (parent !== undefined)
8652
+ place(parent);
8653
+ placed.add(node.id);
8654
+ ordered.push(node);
8655
+ };
8656
+ for (const node of nodes.values())
8657
+ place(node);
8658
+ return ordered;
8659
+ }
8660
+ }
8661
+
8662
+ // src/backup/export/sealedRoots.ts
8663
+ function unanswered(error) {
8664
+ return error instanceof NetworkError || error instanceof ApiError;
8665
+ }
8666
+
8667
+ class SealedRoots {
8668
+ ctx;
8669
+ services;
8670
+ locked = new Set;
8671
+ constructor(ctx, services) {
8672
+ this.ctx = ctx;
8673
+ this.services = services;
8674
+ }
8675
+ async unlock(rootIds, skipLocked) {
8676
+ const refused = [];
8677
+ for (const rootId of rootIds) {
8678
+ try {
8679
+ await this.services.roots.unlock({ root_id: rootId });
8680
+ } catch (error) {
8681
+ if (!(error instanceof ClientError) || unanswered(error))
8682
+ throw error;
8683
+ this.locked.add(rootId);
8684
+ refused.push(` ${printable2(await this.ctx.names.describe(rootId))}: ${error.message}`);
8685
+ }
8686
+ }
8687
+ if (refused.length > 0 && !skipLocked) {
8688
+ throw new ClientError(`${refused.length} seal root(s) cannot be opened, so the export would be incomplete:
8689
+ ${refused.join(`
8690
+ `)}
8691
+ Put their secrets where this machine finds them (a passphrase: safekb-mcp --sidecar, then 'add <name or id>'), or pass --skip-locked to export without them.`);
8692
+ }
8693
+ }
8694
+ isLocked(rootId) {
8695
+ return this.locked.has(rootId);
8696
+ }
8697
+ lockedIds() {
8698
+ return [...this.locked];
8699
+ }
8700
+ }
8701
+
8702
+ // src/backup/export/work.ts
8703
+ class WorkExporter {
8704
+ kb;
8705
+ exported;
8706
+ constructor(kb, exported) {
8707
+ this.kb = kb;
8708
+ this.exported = exported;
8709
+ }
8710
+ async* entries(plan) {
8711
+ const { doc, project, task, decision } = this.exported.entities;
8712
+ const inSpace = (spaceId) => spaceId === null ? plan.everything : plan.wholeSpaceIds.has(spaceId);
8713
+ const exportedDoc = (id) => id !== null && doc.has(id) ? id : null;
8714
+ const projects = (await this.kb.projects()).filter((candidate) => inSpace(candidate.space_id));
8715
+ for (const found of projects) {
8716
+ project.add(found.id);
8717
+ this.exported.count("project");
8718
+ yield { record: this.project(found, exportedDoc(found.spec_doc_id)) };
8719
+ }
8720
+ const tasks = [];
8721
+ let merged = 0;
8722
+ const seen = new Set;
8723
+ for await (const found of this.kb.tasks()) {
8724
+ if (seen.has(found.id))
8725
+ continue;
8726
+ seen.add(found.id);
8727
+ if (found.project_id === null ? !inSpace(found.space_id) : !project.has(found.project_id)) {
8728
+ continue;
8729
+ }
8730
+ if (found.merged_into !== null)
8731
+ merged++;
8732
+ else
8733
+ tasks.push(found);
8734
+ }
8735
+ if (merged > 0) {
8736
+ this.exported.leftOut.push(`${merged} task(s) that were merged into another task`);
8737
+ }
8738
+ for (const found of parentsFirst(tasks)) {
8739
+ task.add(found.id);
8740
+ this.exported.count("task");
8741
+ yield {
8742
+ record: {
8743
+ kind: "task",
8744
+ id: found.id,
8745
+ title: found.title,
8746
+ status: found.status,
8747
+ priority: found.priority,
8748
+ due_date: found.due_date,
8749
+ due_date_label: found.due_date_label,
8750
+ assignee_id: found.assignee_id,
8751
+ waiting_on: found.waiting_on,
8752
+ project_id: found.project_id,
8753
+ parent_task_id: found.parent_task_id !== null && task.has(found.parent_task_id) ? found.parent_task_id : null,
8754
+ exec_order: found.exec_order,
8755
+ space_id: found.space_id,
8756
+ spec_doc_id: exportedDoc(found.spec_doc_id),
8757
+ external_refs: found.external_refs
8758
+ }
8759
+ };
8760
+ }
8761
+ for (const owner of projects) {
8762
+ const decisions = await this.kb.decisions(owner.id);
8763
+ for (const found of decisions.sort((a, b) => a.created_at - b.created_at)) {
8764
+ decision.add(found.id);
8765
+ this.exported.count("decision");
8766
+ yield {
8767
+ record: {
8768
+ kind: "decision",
8769
+ id: found.id,
8770
+ project_id: found.project_id,
8771
+ title: found.title,
8772
+ rationale: found.rationale,
8773
+ status: found.status
8774
+ }
8775
+ };
8776
+ }
8777
+ }
8778
+ }
8779
+ project(found, specDocId) {
8780
+ return {
8781
+ kind: "project",
8782
+ id: found.id,
8783
+ name: found.name,
8784
+ slug: found.slug,
8785
+ description: found.description,
8786
+ status: found.status,
8787
+ spec_doc_id: specDocId,
8788
+ space_id: found.space_id,
8789
+ icon: found.icon,
8790
+ color: found.color
8791
+ };
8792
+ }
8793
+ }
8794
+ function parentsFirst(tasks) {
8795
+ const byId = new Map(tasks.map((task) => [task.id, task]));
8796
+ const ordered = [];
8797
+ const placed = new Set;
8798
+ const place = (task) => {
8799
+ if (placed.has(task.id))
8800
+ return;
8801
+ placed.add(task.id);
8802
+ const parent = task.parent_task_id === null ? undefined : byId.get(task.parent_task_id);
8803
+ if (parent !== undefined)
8804
+ place(parent);
8805
+ ordered.push(task);
8806
+ };
8807
+ for (const task of tasks)
8808
+ place(task);
8809
+ return ordered;
8810
+ }
8811
+
8812
+ // src/backup/export/exporter.ts
8813
+ class Exporter {
8814
+ from;
8815
+ request;
8816
+ progress;
8817
+ exported = new Exported;
8818
+ roots;
8819
+ constructor(from, request, progress) {
8820
+ this.from = from;
8821
+ this.request = request;
8822
+ this.progress = progress;
8823
+ this.roots = new SealedRoots(from.sealed, from.services);
8824
+ }
8825
+ async prepare() {
8826
+ const { kb } = this.from;
8827
+ const plan = await new ExportPlanner(kb).plan(this.request.selection);
8828
+ const credentials = await this.credentialsOf(plan);
8829
+ await this.includeSpaces(plan, credentials.map((credential2) => credential2.space_id));
8830
+ const sealRoots = new Map;
8831
+ for (const credential2 of credentials) {
8832
+ if (!credential2.sealed)
8833
+ continue;
8834
+ const sealed = await this.from.sealed.api.fetchSealedCredential(credential2.id);
8835
+ sealRoots.set(credential2.id, sealed.seal_root_id);
8836
+ }
8837
+ await this.includeRoots(plan, new Set(sealRoots.values()));
8838
+ const needed = new Set(sealRoots.values());
8839
+ for (const node of plan.nodes) {
8840
+ if (node.sealRootId !== null)
8841
+ needed.add(node.sealRootId);
8842
+ }
8843
+ await this.roots.unlock(needed, this.request.skipLocked);
8844
+ return { plan, credentials, sealRoots };
8845
+ }
8846
+ async* entries(prepared) {
8847
+ const { kb, sealed, services } = this.from;
8848
+ const { plan } = prepared;
8849
+ yield { record: this.manifest() };
8850
+ for (const space of plan.spaces) {
8851
+ this.exported.count("space");
8852
+ yield { record: spaceRecord(space) };
8853
+ }
8854
+ this.progress(`Documents and folders: ${plan.nodes.length}`);
8855
+ const nodes = new NodeExporter({
8856
+ kb,
8857
+ sealed,
8858
+ services,
8859
+ access: new NodeAccess(sealed),
8860
+ roots: this.roots
8861
+ }, this.exported);
8862
+ for (const node of plan.nodes)
8863
+ yield* nodes.entries(node);
8864
+ this.progress(`Credentials: ${prepared.credentials.length}`);
8865
+ yield* new CredentialExporter(kb, new CredentialAccess(sealed), this.roots, this.exported).entries(prepared.credentials, prepared.sealRoots);
8866
+ this.progress("Projects, tasks and decisions");
8867
+ yield* new WorkExporter(kb, this.exported).entries(plan);
8868
+ this.progress("Canvas blocks and database rows");
8869
+ const content = new ContentExporter(kb, this.exported);
8870
+ yield* content.blocks();
8871
+ yield* content.rows();
8872
+ yield* content.attachmentRows();
8873
+ yield* content.views();
8874
+ this.progress("Links");
8875
+ yield* new LinkExporter(kb, this.exported).entries();
8876
+ yield { record: await this.end() };
8877
+ }
8878
+ get written() {
8879
+ return this.exported;
8880
+ }
8881
+ manifest() {
8882
+ return {
8883
+ kind: "manifest",
8884
+ format: RECORD_FORMAT,
8885
+ created_at: new Date().toISOString(),
8886
+ client_version: this.request.clientVersion,
8887
+ source: this.request.source,
8888
+ selection: this.request.selection
8889
+ };
8890
+ }
8891
+ async end() {
8892
+ const locked = [];
8893
+ for (const rootId of this.roots.lockedIds()) {
8894
+ locked.push(await this.from.sealed.names.describe(rootId));
8895
+ }
8896
+ if (locked.length > 0) {
8897
+ this.exported.leftOut.push(`everything of ${locked.length} seal root(s) that could not be opened: ${locked.join(", ")}`);
8898
+ }
8899
+ return {
8900
+ kind: "end",
8901
+ counts: { ...this.exported.counts },
8902
+ left_out: [...this.exported.leftOut]
8903
+ };
8904
+ }
8905
+ async credentialsOf(plan) {
8906
+ const folders = new Set(plan.nodes.map((node) => node.id));
8907
+ const chosen = [];
8908
+ for await (const credential2 of this.from.kb.credentials()) {
8909
+ const filed = credential2.folder_id !== null && folders.has(credential2.folder_id);
8910
+ if (plan.wholeSpaceIds.has(credential2.space_id) || filed) {
8911
+ chosen.push(credential2);
8912
+ }
8913
+ }
8914
+ return chosen;
8915
+ }
8916
+ async includeRoots(plan, rootIds) {
8917
+ const planned = new Set(plan.nodes.map((node) => node.id));
8918
+ const extra = [];
8919
+ const spaceIds = [];
8920
+ for (const rootId of rootIds) {
8921
+ if (planned.has(rootId))
8922
+ continue;
8923
+ extra.push({ id: rootId, parentId: null, sealRootId: rootId });
8924
+ spaceIds.push((await this.from.kb.document(rootId)).space_id);
8925
+ }
8926
+ plan.nodes.unshift(...extra);
8927
+ await this.includeSpaces(plan, spaceIds);
8928
+ }
8929
+ async includeSpaces(plan, spaceIds) {
8930
+ const missing = spaceIds.filter((id) => !plan.spaces.some((space) => space.id === id));
8931
+ if (missing.length === 0)
8932
+ return;
8933
+ for (const space of await this.from.kb.spaces()) {
8934
+ if (missing.includes(space.id))
8935
+ plan.spaces.push(space);
8936
+ }
8937
+ }
8938
+ }
8939
+ function spaceRecord(space) {
8940
+ return {
8941
+ kind: "space",
8942
+ id: space.id,
8943
+ name: space.name,
8944
+ icon: space.icon,
8945
+ color: space.color,
8946
+ description: space.description,
8947
+ access_mode: space.access_mode,
8948
+ workflow_states: space.workflow_states,
8949
+ allow_client_uploads: space.allow_client_uploads,
8950
+ encryption_policy: space.encryption_policy
8951
+ };
8952
+ }
8953
+
8954
+ // src/claudeCode.ts
8955
+ import { spawn as spawn2 } from "node:child_process";
8956
+ import { basename as basename2 } from "node:path";
8957
+ var SERVER_NAME = "safekb";
8958
+ var PACKAGE_NAME = "@safekb/mcp";
8959
+ var BIN_NAME = "safekb-mcp";
8960
+ var SERVE_FLAG = "--mcp";
8961
+ var PACKAGE_COMMAND = ["npx", "-y", PACKAGE_NAME, SERVE_FLAG];
8962
+ var SCOPES = ["user", "local", "project"];
8963
+ function quotedForCmd(arg) {
8964
+ return /[\s"&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
8965
+ }
8966
+ var runProgram = (program, args) => new Promise((resolve, reject) => {
8967
+ const windows = process.platform === "win32";
8968
+ const child = spawn2(program, windows ? args.map(quotedForCmd) : args, {
8969
+ stdio: ["ignore", "pipe", "pipe"],
8970
+ shell: windows
8971
+ });
8972
+ let output = "";
8973
+ child.stdout.on("data", (chunk) => {
8974
+ output += chunk;
8975
+ });
8976
+ child.stderr.on("data", (chunk) => {
8977
+ output += chunk;
8978
+ });
8979
+ child.on("error", (error) => {
8980
+ if (error.code === "ENOENT")
8981
+ resolve({ code: null, output: "" });
8982
+ else
8983
+ reject(error);
8984
+ });
8985
+ child.on("close", (code) => resolve({ code: code ?? 1, output }));
8986
+ });
8987
+ function addCommand(serverArgs) {
8988
+ return [
8989
+ "mcp",
8990
+ "add",
8991
+ "--scope",
8992
+ "user",
8993
+ SERVER_NAME,
8994
+ "--",
8995
+ ...PACKAGE_COMMAND,
8996
+ ...serverArgs
8997
+ ];
8998
+ }
8999
+ var REMOTE_TYPES = ["http", "sse"];
9000
+ var ENVIRONMENT = /^([ \t]*)Environment:[ \t]*\n((?:\1[ \t]+\S.*(?:\n|$))*)/m;
9001
+ var SERVE_FLAG_SINCE = [0, 5, 0];
9002
+ var EXACT_VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
9003
+ function field(description, name) {
9004
+ return (new RegExp(`^\\s*${name}:(.*)$`, "m").exec(description)?.[1] ?? "").trim();
9005
+ }
9006
+ function registrationOf(output) {
9007
+ const description = output.replace(/\r\n?/g, `
9008
+ `);
9009
+ if (REMOTE_TYPES.includes(field(description, "Type")))
9010
+ return null;
9011
+ const command = field(description, "Command");
9012
+ if (command.length === 0) {
9013
+ throw new StartupError(`Claude Code has an MCP server "${SERVER_NAME}", but what it starts could not be read from "claude mcp get ${SERVER_NAME}", so it is not known whether it starts this client with ${SERVE_FLAG}. Nothing was changed.`);
9014
+ }
9015
+ const scope = field(description, "Scope").split(" ")[0]?.toLowerCase();
9016
+ return {
9017
+ scope: SCOPES.find((name) => name === scope) ?? null,
9018
+ command,
9019
+ args: field(description, "Args"),
9020
+ environment: (ENVIRONMENT.exec(description)?.[2] ?? "").split(`
9021
+ `).map((line) => line.trim().split("=")[0] ?? "").filter((name) => name.length > 0),
9022
+ timeout: field(description, "Timeout")
9023
+ };
9024
+ }
9025
+ function words(registration) {
9026
+ return [registration.command, ...registration.args.split(/\s+/)];
9027
+ }
9028
+ function startsThisClient(registration) {
9029
+ return words(registration).some((word) => word === PACKAGE_NAME || word.startsWith(`${PACKAGE_NAME}@`) || [BIN_NAME, `${BIN_NAME}.js`].includes(basename2(word)));
9030
+ }
9031
+ function pinnedBeforeServeFlag(registration) {
9032
+ for (const word of words(registration)) {
9033
+ if (!word.startsWith(`${PACKAGE_NAME}@`))
9034
+ continue;
9035
+ const version = word.slice(PACKAGE_NAME.length + 1);
9036
+ const numbers = EXACT_VERSION.exec(version)?.slice(1).map(Number);
9037
+ if (numbers === undefined)
9038
+ return null;
9039
+ const differing = numbers.findIndex((number, index) => number !== SERVE_FLAG_SINCE[index]);
9040
+ return differing !== -1 && numbers[differing] < SERVE_FLAG_SINCE[differing] ? version : null;
9041
+ }
9042
+ return null;
9043
+ }
9044
+ function lacksServeFlag(registration) {
9045
+ return startsThisClient(registration) && !words(registration).includes(SERVE_FLAG);
9046
+ }
9047
+ function writtenBySetup(registration) {
9048
+ const { scope, command, args, environment, timeout } = registration;
9049
+ if (scope !== "user" || command !== "npx" || environment.length > 0 || timeout.length > 0) {
9050
+ return null;
9051
+ }
9052
+ const own = `-y ${PACKAGE_NAME}`;
9053
+ if (args === own)
9054
+ return [];
9055
+ const config = `${own} --config `;
9056
+ if (!args.startsWith(config))
9057
+ return null;
9058
+ const path = args.slice(config.length);
9059
+ return /\s-/.test(path) ? null : ["--config", path];
9060
+ }
9061
+ function quotedForSh(word) {
9062
+ return /^[\w@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, "'\\''")}'`;
9063
+ }
9064
+ function typed(command) {
9065
+ return command.map(process.platform === "win32" ? quotedForCmd : quotedForSh).join(" ");
9066
+ }
9067
+ function howToAddServeFlag(registration) {
6406
9068
  const { scope, command, args, environment, timeout } = registration;
6407
9069
  if (scope === null) {
6408
9070
  throw new StartupError(`Claude Code has the MCP server "${SERVER_NAME}" without ${SERVE_FLAG}, which this version needs to start, and did not say where it is registered. Run "claude mcp get ${SERVER_NAME}" and add ${SERVE_FLAG} to its arguments.`);
@@ -6485,7 +9147,7 @@ __export(exports_external, {
6485
9147
  undefined: () => _undefined3,
6486
9148
  ulid: () => ulid2,
6487
9149
  uint64: () => uint64,
6488
- uint32: () => uint32,
9150
+ uint32: () => uint322,
6489
9151
  tuple: () => tuple,
6490
9152
  trim: () => _trim,
6491
9153
  treeifyError: () => treeifyError,
@@ -6600,7 +9262,7 @@ __export(exports_external, {
6600
9262
  email: () => email2,
6601
9263
  e164: () => e1642,
6602
9264
  discriminatedUnion: () => discriminatedUnion,
6603
- describe: () => describe2,
9265
+ describe: () => describe3,
6604
9266
  decodeAsync: () => decodeAsync2,
6605
9267
  decode: () => decode2,
6606
9268
  date: () => date3,
@@ -6742,7 +9404,7 @@ __export(exports_core2, {
6742
9404
  extractDefs: () => extractDefs,
6743
9405
  encodeAsync: () => encodeAsync,
6744
9406
  encode: () => encode,
6745
- describe: () => describe,
9407
+ describe: () => describe2,
6746
9408
  decodeAsync: () => decodeAsync,
6747
9409
  decode: () => decode,
6748
9410
  createToJSONSchemaMethod: () => createToJSONSchemaMethod,
@@ -17079,7 +19741,7 @@ function _check(fn, params) {
17079
19741
  ch._zod.check = fn;
17080
19742
  return ch;
17081
19743
  }
17082
- function describe(description) {
19744
+ function describe2(description) {
17083
19745
  const ch = new $ZodCheck({ check: "describe" });
17084
19746
  ch._zod.onattach = [
17085
19747
  (inst) => {
@@ -18134,7 +20796,7 @@ __export(exports_schemas2, {
18134
20796
  undefined: () => _undefined3,
18135
20797
  ulid: () => ulid2,
18136
20798
  uint64: () => uint64,
18137
- uint32: () => uint32,
20799
+ uint32: () => uint322,
18138
20800
  tuple: () => tuple,
18139
20801
  transform: () => transform,
18140
20802
  templateLiteral: () => templateLiteral,
@@ -18198,7 +20860,7 @@ __export(exports_schemas2, {
18198
20860
  email: () => email2,
18199
20861
  e164: () => e1642,
18200
20862
  discriminatedUnion: () => discriminatedUnion,
18201
- describe: () => describe2,
20863
+ describe: () => describe3,
18202
20864
  date: () => date3,
18203
20865
  custom: () => custom,
18204
20866
  cuid2: () => cuid22,
@@ -18779,7 +21441,7 @@ function float64(params) {
18779
21441
  function int32(params) {
18780
21442
  return _int32(ZodNumberFormat, params);
18781
21443
  }
18782
- function uint32(params) {
21444
+ function uint322(params) {
18783
21445
  return _uint32(ZodNumberFormat, params);
18784
21446
  }
18785
21447
  var ZodBoolean = /* @__PURE__ */ $constructor("ZodBoolean", (inst, def) => {
@@ -19449,7 +22111,7 @@ function refine(fn, _params = {}) {
19449
22111
  function superRefine(fn) {
19450
22112
  return _superRefine(fn);
19451
22113
  }
19452
- var describe2 = describe;
22114
+ var describe3 = describe2;
19453
22115
  var meta2 = meta;
19454
22116
  function _instanceof(cls, params = {}) {
19455
22117
  const inst = new ZodCustom({
@@ -20277,7 +22939,7 @@ class HostedProxy {
20277
22939
  }
20278
22940
 
20279
22941
  // src/keyring.ts
20280
- import { readdir, stat as stat2 } from "node:fs/promises";
22942
+ import { readdir, stat as stat3 } from "node:fs/promises";
20281
22943
  import { join as join4 } from "node:path";
20282
22944
  var KEY_FILE_SUFFIX = ".json";
20283
22945
  async function readEntry(path) {
@@ -20306,7 +22968,7 @@ class Keyring {
20306
22968
  this.dir = dir;
20307
22969
  }
20308
22970
  async load() {
20309
- const info = await stat2(this.dir).catch(() => null);
22971
+ const info = await stat3(this.dir).catch(() => null);
20310
22972
  if (info === null || !info.isDirectory()) {
20311
22973
  throw new ClientError(`Keyring ${this.dir} is not a directory`);
20312
22974
  }
@@ -20350,17 +23012,6 @@ class Keyring {
20350
23012
  }
20351
23013
  }
20352
23014
 
20353
- // src/stores/secretStore.ts
20354
- var KEY_STORE_NAMES = ["system", "file"];
20355
- var KEY_STORE_LABELS = {
20356
- system: "the system keyring",
20357
- file: "the key file directory"
20358
- };
20359
- var STORE_NAMES = [...KEY_STORE_NAMES, "sidecar"];
20360
- function passphraseNote(seal2) {
20361
- return `seal root ${seal2.rootId} is opened by a passphrase, which only the sidecar can hold`;
20362
- }
20363
-
20364
23015
  // src/stores/secretStores.ts
20365
23016
  var HOW_TO_ENABLE = {
20366
23017
  system: "The system keyring is not set up for this client; run: safekb-mcp --configure",
@@ -20760,6 +23411,9 @@ Usage:
20760
23411
  safekb-mcp --rotate-key <name or id> [--store system|file]
20761
23412
  safekb-mcp --move-key <name or id> --store system|file
20762
23413
  safekb-mcp --delete-key <name or id>
23414
+ safekb-mcp --export <file> [--space <space id>]... [--doc <document id>]... [--compress]
23415
+ [--skip-locked]
23416
+ safekb-mcp --import <file> [--into-space <space id>] [--store system|file]
20763
23417
 
20764
23418
  Without arguments this text is printed.
20765
23419
 
@@ -20806,9 +23460,29 @@ Options:
20806
23460
  its name. The root on the server is not touched. Shows the keys first, warns
20807
23461
  unless the server says they are superseded, and asks you to type the root's
20808
23462
  name or id. Needs a terminal.
20809
- --space <space id> With --create-key: the space of the new seal root.
23463
+ --export <file> Write what this token reaches to a new file, encrypted under a passphrase you
23464
+ type: spaces, folders, documents, attachments, databases, credentials,
23465
+ projects, tasks, decisions and links. Sealed content and credential secrets
23466
+ are decrypted on this machine and are in the file, so every seal root in
23467
+ the selection must be openable with a secret this machine keeps or its
23468
+ sidecar holds. Without --space and --doc everything is exported. Needs a
23469
+ terminal.
23470
+ --import <file> Create what such a file holds, as new things with new ids, in the account
23471
+ of this token: nothing that exists is changed. Each exported space becomes
23472
+ a new space, each seal root a new seal root with a newly generated key.
23473
+ Needs a terminal.
23474
+ --space <space id> With --create-key: the space of the new seal root. With --export: export
23475
+ this space with everything in it; may be repeated.
23476
+ --doc <document id> With --export: export this document, or this folder with everything in
23477
+ it; may be repeated, and combined with --space.
23478
+ --compress With --export: compress (gzip) before encrypting.
23479
+ --skip-locked With --export: leave out what belongs to a seal root that cannot be opened,
23480
+ instead of stopping.
23481
+ --into-space <space id> With --import: put everything into this one existing space instead of
23482
+ creating the exported spaces again.
20810
23483
  --name <new name> With --rename-key: the new name.
20811
- --store system|file With --create-key, --rotate-key and --move-key: the key store.
23484
+ --store system|file With --create-key, --rotate-key and --move-key: the key store. With
23485
+ --import: where the keys of the new seal roots are kept.
20812
23486
  --sidecar-socket <path> The sidecar's socket. Also: SAFEKB_SIDECAR_SOCKET. Default: sidecar.sock
20813
23487
  next to the token file.
20814
23488
  --version, --help
@@ -20829,6 +23503,11 @@ Environment:
20829
23503
  function nonEmpty(value) {
20830
23504
  return value === undefined || value.length === 0 ? null : value;
20831
23505
  }
23506
+ function several(value) {
23507
+ if (value === undefined || typeof value === "boolean")
23508
+ return [];
23509
+ return typeof value === "string" ? [value] : value;
23510
+ }
20832
23511
  function trimTrailingSlashes(url2) {
20833
23512
  return url2.replace(/\/+$/, "");
20834
23513
  }
@@ -20854,7 +23533,9 @@ var EXCLUSIVE_MODES = [
20854
23533
  "rename-key",
20855
23534
  "rotate-key",
20856
23535
  "move-key",
20857
- "delete-key"
23536
+ "delete-key",
23537
+ "export",
23538
+ "import"
20858
23539
  ];
20859
23540
  var TAKEN_BY_EVERY_MODE = ["config"];
20860
23541
  var KEY_MODE_MODIFIERS = ["url", "keyring"];
@@ -20871,7 +23552,16 @@ var MODIFIERS = {
20871
23552
  "rename-key": [...KEY_MODE_MODIFIERS, "name"],
20872
23553
  "rotate-key": [...KEY_MODE_MODIFIERS, "store"],
20873
23554
  "move-key": [...KEY_MODE_MODIFIERS, "store"],
20874
- "delete-key": KEY_MODE_MODIFIERS
23555
+ "delete-key": KEY_MODE_MODIFIERS,
23556
+ export: [
23557
+ ...KEY_MODE_MODIFIERS,
23558
+ "sidecar-socket",
23559
+ "space",
23560
+ "doc",
23561
+ "compress",
23562
+ "skip-locked"
23563
+ ],
23564
+ import: [...KEY_MODE_MODIFIERS, "into-space", "store"]
20875
23565
  };
20876
23566
  var A_SEAL_ROOT = "the name or the id of a seal root";
20877
23567
  var MODE_VALUES = {
@@ -20881,7 +23571,9 @@ var MODE_VALUES = {
20881
23571
  "rename-key": A_SEAL_ROOT,
20882
23572
  "rotate-key": A_SEAL_ROOT,
20883
23573
  "move-key": A_SEAL_ROOT,
20884
- "delete-key": A_SEAL_ROOT
23574
+ "delete-key": A_SEAL_ROOT,
23575
+ export: "the path of the export file",
23576
+ import: "the path of the export file"
20885
23577
  };
20886
23578
  var REQUIRED_MODIFIERS = {
20887
23579
  "create-key": { flag: "space", usage: "--space <space id>" },
@@ -20898,10 +23590,13 @@ function assertFitting(chosen, flags) {
20898
23590
  throw new StartupError(`--${chosen} needs ${value}`);
20899
23591
  }
20900
23592
  const required2 = REQUIRED_MODIFIERS[chosen];
20901
- if (required2 !== undefined && nonEmpty(flags[required2.flag]) === null) {
23593
+ if (required2 !== undefined && several(flags[required2.flag]).every((value2) => value2 === "")) {
20902
23594
  throw new StartupError(`--${chosen} needs ${required2.usage}`);
20903
23595
  }
20904
- const empty = Object.entries(flags).filter(([, given]) => given === "").map(([name]) => name);
23596
+ if (chosen === "create-key" && several(flags.space).length > 1) {
23597
+ throw new StartupError("--create-key takes one --space");
23598
+ }
23599
+ const empty = Object.entries(flags).filter(([, given]) => several(given).includes("")).map(([name]) => name);
20905
23600
  if (empty.length > 0) {
20906
23601
  throw new StartupError(`${flagList(empty)} was passed without a value`);
20907
23602
  }
@@ -20965,7 +23660,13 @@ ${USAGE}`);
20965
23660
  transferFile: nonEmpty(values["export-keys"]) ?? nonEmpty(values["import-keys"]),
20966
23661
  rootRef: ROOT_REF_MODES.map((name) => nonEmpty(values[name])).find((value) => value !== null) ?? null,
20967
23662
  newName: nonEmpty(values["create-key"]) ?? nonEmpty(values.name),
20968
- spaceId: nonEmpty(values.space),
23663
+ spaceId: several(values.space)[0] ?? null,
23664
+ backupFile: nonEmpty(values.export) ?? nonEmpty(values.import),
23665
+ exportSpaceIds: several(values.space),
23666
+ exportDocumentIds: several(values.doc),
23667
+ compress: values.compress === true,
23668
+ skipLocked: values["skip-locked"] === true,
23669
+ intoSpaceId: nonEmpty(values["into-space"]),
20969
23670
  store: keyStoreOf(values.store),
20970
23671
  confirm: values.confirm === true,
20971
23672
  sealedOnly: values["sealed-only"] === true,
@@ -20999,7 +23700,13 @@ function parse5(argv) {
20999
23700
  "rotate-key": { type: "string" },
21000
23701
  "move-key": { type: "string" },
21001
23702
  "delete-key": { type: "string" },
21002
- space: { type: "string" },
23703
+ space: { type: "string", multiple: true },
23704
+ doc: { type: "string", multiple: true },
23705
+ export: { type: "string" },
23706
+ import: { type: "string" },
23707
+ compress: { type: "boolean" },
23708
+ "skip-locked": { type: "boolean" },
23709
+ "into-space": { type: "string" },
21003
23710
  name: { type: "string" },
21004
23711
  store: { type: "string" },
21005
23712
  help: { type: "boolean" },
@@ -21009,18 +23716,18 @@ function parse5(argv) {
21009
23716
  }
21010
23717
 
21011
23718
  // src/rootNames.ts
21012
- import { existsSync as existsSync2 } from "node:fs";
23719
+ import { existsSync as existsSync3 } from "node:fs";
21013
23720
  import { dirname as dirname3, join as join5 } from "node:path";
21014
23721
  var ROOT_NAMES_FILE = "roots.json";
21015
23722
  var ROOT_NAMES_VERSION = 1;
21016
- var OBJECT_ID = /^[0-9a-f]{24}$/;
23723
+ var OBJECT_ID3 = /^[0-9a-f]{24}$/;
21017
23724
  function rootNamesPath(tokenConfigPath) {
21018
23725
  return join5(dirname3(tokenConfigPath), ROOT_NAMES_FILE);
21019
23726
  }
21020
23727
  function assertNames(value, path) {
21021
23728
  const raw = value;
21022
23729
  const names = raw?.names;
21023
- if (raw?.v !== ROOT_NAMES_VERSION || typeof names !== "object" || names === null || Array.isArray(names) || !Object.entries(names).every(([id, name]) => OBJECT_ID.test(id) && typeof name === "string" && name.length > 0)) {
23730
+ if (raw?.v !== ROOT_NAMES_VERSION || typeof names !== "object" || names === null || Array.isArray(names) || !Object.entries(names).every(([id, name]) => OBJECT_ID3.test(id) && typeof name === "string" && name.length > 0)) {
21024
23731
  throw new ClientError(`Seal root names ${path} are malformed`);
21025
23732
  }
21026
23733
  return new Map(Object.entries(names));
@@ -21033,7 +23740,7 @@ class RootNames {
21033
23740
  this.path = path;
21034
23741
  }
21035
23742
  async all() {
21036
- if (!existsSync2(this.path))
23743
+ if (!existsSync3(this.path))
21037
23744
  return new Map;
21038
23745
  const text = await readPrivateFile(this.path, "Seal root names");
21039
23746
  let parsed;
@@ -21052,7 +23759,7 @@ class RootNames {
21052
23759
  return name === null ? rootId : `"${name}" (${rootId})`;
21053
23760
  }
21054
23761
  remember(rootId, name) {
21055
- if (name.length === 0 || OBJECT_ID.test(name))
23762
+ if (name.length === 0 || OBJECT_ID3.test(name))
21056
23763
  return this.forget(rootId);
21057
23764
  return this.change(async () => {
21058
23765
  const names = await this.all();
@@ -21078,108 +23785,64 @@ class RootNames {
21078
23785
  return followUp(done, "its name could not be updated on this machine", change);
21079
23786
  }
21080
23787
  async assertUsable(name, rootId) {
21081
- if (OBJECT_ID.test(name)) {
23788
+ if (OBJECT_ID3.test(name)) {
21082
23789
  throw new ClientError(`"${name}" has the shape of an id, so it cannot be the name of a seal root`);
21083
23790
  }
21084
23791
  for (const [id, taken] of await this.all()) {
21085
23792
  if (taken === name && id !== rootId) {
21086
23793
  throw new ClientError(`Seal root ${id} is already named "${name}" on this machine; choose another name`);
21087
23794
  }
21088
- }
21089
- }
21090
- async isName(reference) {
21091
- return [...(await this.all()).values()].includes(reference);
21092
- }
21093
- async resolve(reference) {
21094
- if (OBJECT_ID.test(reference))
21095
- return reference;
21096
- const ids = [...await this.all()].filter(([, name]) => name === reference).map(([id]) => id);
21097
- const [first, ...others] = ids;
21098
- if (first === undefined) {
21099
- throw new ClientError(`No seal root is known as "${reference}"; use its id`);
21100
- }
21101
- if (others.length > 0) {
21102
- throw new ClientError(`${ids.length} seal roots are named "${reference}" (${ids.join(", ")}); use the id`);
21103
- }
21104
- return first;
21105
- }
21106
- save(names) {
21107
- const sorted = Object.fromEntries([...names].sort(([a], [b]) => a.localeCompare(b)));
21108
- return writePrivateFile(this.path, `${JSON.stringify({ v: ROOT_NAMES_VERSION, names: sorted }, null, 2)}
21109
- `);
21110
- }
21111
- }
21112
-
21113
- // src/sealed/attachmentService.ts
21114
- import { createReadStream } from "node:fs";
21115
- import { open as open3, stat as stat3, unlink } from "node:fs/promises";
21116
- import { basename as basename3 } from "node:path";
21117
-
21118
- // src/sealed/nodeAccess.ts
21119
- function assertSeq(node, expectedSeq2) {
21120
- if (node.seal_seq !== expectedSeq2) {
21121
- throw new ClientError(`${node.id} changed: its seal_seq is ${node.seal_seq}, not ${expectedSeq2}. Read it again before writing.`);
21122
- }
21123
- }
21124
- function assertAccepted(node) {
21125
- if (node.deposited) {
21126
- throw new ClientError(`${node.id} is a pending deposit; accept_sealed_deposit or reject_sealed_deposit it first`);
21127
- }
21128
- }
21129
- function writeResult(node) {
21130
- return {
21131
- id: node.id,
21132
- seal_root_id: node.seal_root_id,
21133
- seal_seq: node.seal_seq
21134
- };
21135
- }
21136
-
21137
- class NodeAccess {
21138
- ctx;
21139
- constructor(ctx) {
21140
- this.ctx = ctx;
21141
- }
21142
- nodeContext(nodeId) {
21143
- return { tenant: this.ctx.tenant, node: nodeId };
21144
- }
21145
- async dekOf(node) {
21146
- const { keys } = this.ctx.session.require(node.seal_root_id);
21147
- if (node.enc_dek !== null) {
21148
- return unwrapDek(keys, node.id, parseEnvelope(node.enc_dek));
21149
- }
21150
- if (node.box_dek !== null) {
21151
- return openBoxDek(keys, node.id, parseBoxDek(node.box_dek));
21152
- }
21153
- throw new ClientError(`Sealed node ${node.id} has no wrapped key`);
21154
- }
21155
- async open(nodeId) {
21156
- return this.openNode(await this.ctx.api.getNode(nodeId));
23795
+ }
21157
23796
  }
21158
- async openNode(node) {
21159
- const root = this.ctx.session.require(node.seal_root_id);
21160
- const dek = await this.dekOf(node);
21161
- const meta3 = await decryptMeta(dek, this.nodeContext(node.id), parseEnvelope(node.enc_meta));
21162
- return { node, root, dek, meta: meta3 };
23797
+ async isName(reference) {
23798
+ return [...(await this.all()).values()].includes(reference);
21163
23799
  }
21164
- async sign(message, signerRootIds) {
21165
- const signers = signerRootIds.map((rootId) => this.ctx.session.require(rootId).keys);
21166
- return signMessage(await buildMessage(message), signers);
23800
+ async resolve(reference) {
23801
+ if (OBJECT_ID3.test(reference))
23802
+ return reference;
23803
+ const ids = [...await this.all()].filter(([, name]) => name === reference).map(([id]) => id);
23804
+ const [first, ...others] = ids;
23805
+ if (first === undefined) {
23806
+ throw new ClientError(`No seal root is known as "${reference}"; use its id`);
23807
+ }
23808
+ if (others.length > 0) {
23809
+ throw new ClientError(`${ids.length} seal roots are named "${reference}" (${ids.join(", ")}); use the id`);
23810
+ }
23811
+ return first;
21167
23812
  }
21168
- async signPlain(op, node) {
21169
- const sigs = await this.sign({
21170
- op,
21171
- tenant: this.ctx.tenant,
21172
- node: node.id,
21173
- root: node.seal_root_id,
21174
- baseSeq: node.seal_seq
21175
- }, [node.seal_root_id]);
21176
- return { base_seq: node.seal_seq, sigs };
23813
+ save(names) {
23814
+ const sorted = Object.fromEntries([...names].sort(([a], [b]) => a.localeCompare(b)));
23815
+ return writePrivateFile(this.path, `${JSON.stringify({ v: ROOT_NAMES_VERSION, names: sorted }, null, 2)}
23816
+ `);
21177
23817
  }
21178
23818
  }
21179
23819
 
21180
23820
  // src/sealed/attachmentService.ts
23821
+ import { createReadStream as createReadStream2 } from "node:fs";
23822
+ import { open as open4, stat as stat4, unlink } from "node:fs/promises";
23823
+ import { basename as basename3 } from "node:path";
21181
23824
  var DOWNLOAD_FILE_MODE = 384;
21182
23825
  var DEFAULT_MIME_TYPE = "application/octet-stream";
23826
+ async function* fileBytes(path) {
23827
+ try {
23828
+ for await (const chunk of createReadStream2(path))
23829
+ yield chunk;
23830
+ } catch (error48) {
23831
+ throw fileError(error48);
23832
+ }
23833
+ }
23834
+ async function* verified(plaintext, meta3) {
23835
+ const hash2 = await createStreamingSha256();
23836
+ let size = 0;
23837
+ for await (const chunk of plaintext) {
23838
+ hash2.update(chunk);
23839
+ size += chunk.length;
23840
+ yield chunk;
23841
+ }
23842
+ if (size !== meta3.size || hash2.digestHex() !== meta3.sha256_plain) {
23843
+ throw new ClientError("Downloaded attachment does not match its sealed size and hash");
23844
+ }
23845
+ }
21183
23846
 
21184
23847
  class AttachmentService {
21185
23848
  ctx;
@@ -21208,11 +23871,32 @@ class AttachmentService {
21208
23871
  }
21209
23872
  async upload(args) {
21210
23873
  const opened = await this.access.open(args.document_id);
23874
+ assertSeq(opened.node, args.expected_seq);
23875
+ assertAccepted(opened.node);
23876
+ const info = await stat4(args.file_path).catch((error48) => {
23877
+ throw fileError(error48);
23878
+ });
23879
+ if (!info.isFile()) {
23880
+ throw new ClientError(`${args.file_path} is not a regular file; only regular files can be attached`);
23881
+ }
23882
+ await this.assertWithinLimit(args.file_path, info.size);
23883
+ return this.attachTo(opened, {
23884
+ filename: basename3(args.file_path),
23885
+ mime: args.mime_type,
23886
+ bytes: fileBytes(args.file_path)
23887
+ });
23888
+ }
23889
+ async attach(args) {
23890
+ const opened = await this.access.open(args.document_id);
23891
+ assertSeq(opened.node, args.expected_seq);
23892
+ assertAccepted(opened.node);
23893
+ await this.assertWithinLimit(`'${args.file.filename}'`, args.size);
23894
+ return this.attachTo(opened, args.file);
23895
+ }
23896
+ async attachTo(opened, plain) {
21211
23897
  const { node, dek } = opened;
21212
- assertSeq(node, args.expected_seq);
21213
- assertAccepted(node);
21214
23898
  const ctx = this.attachmentContext(node.id, newObjectId());
21215
- const file2 = await this.encryptFile(dek, ctx, args.file_path, args.mime_type);
23899
+ const file2 = await this.encrypt(dek, ctx, plain);
21216
23900
  const attEncMeta = serializeEnvelope(await encryptAttachmentMeta(dek, ctx, file2.meta));
21217
23901
  const sigs = await this.access.sign({
21218
23902
  op: "attach",
@@ -21239,6 +23923,23 @@ class AttachmentService {
21239
23923
  size: file2.meta.size
21240
23924
  };
21241
23925
  }
23926
+ async files(documentId) {
23927
+ const opened = await this.access.open(documentId);
23928
+ const files = [];
23929
+ for (const record2 of await this.ctx.api.listAttachments(documentId)) {
23930
+ const meta3 = await this.metaOf(opened, record2);
23931
+ files.push({
23932
+ id: record2.id,
23933
+ meta: meta3,
23934
+ bytes: () => this.plaintextOf(opened, record2.id, meta3)
23935
+ });
23936
+ }
23937
+ return files;
23938
+ }
23939
+ async* plaintextOf(opened, attachmentId, meta3) {
23940
+ const ciphertext = await this.ctx.api.downloadAttachment(opened.node.id, attachmentId);
23941
+ yield* verified(decryptAttachment(opened.dek, this.attachmentContext(opened.node.id, attachmentId), ciphertext), meta3);
23942
+ }
21242
23943
  async download(args) {
21243
23944
  const opened = await this.access.open(args.document_id);
21244
23945
  const record2 = (await this.ctx.api.listAttachments(args.document_id)).find((candidate) => candidate.id === args.attachment_id);
@@ -21246,9 +23947,7 @@ class AttachmentService {
21246
23947
  throw new ClientError(`${args.document_id} has no attachment ${args.attachment_id}`);
21247
23948
  }
21248
23949
  const meta3 = await this.metaOf(opened, record2);
21249
- const ciphertext = await this.ctx.api.downloadAttachment(args.document_id, args.attachment_id);
21250
- const plaintext = decryptAttachment(opened.dek, this.attachmentContext(args.document_id, args.attachment_id), ciphertext);
21251
- await this.writeVerified(args.out_path, plaintext, meta3);
23950
+ await this.write(args.out_path, this.plaintextOf(opened, args.attachment_id, meta3));
21252
23951
  return {
21253
23952
  attachment_id: args.attachment_id,
21254
23953
  out_path: args.out_path,
@@ -21283,27 +23982,15 @@ class AttachmentService {
21283
23982
  }
21284
23983
  return decryptAttachmentMeta(opened.dek, this.attachmentContext(opened.node.id, record2.id), parseEnvelope(record2.att_enc_meta));
21285
23984
  }
21286
- async encryptFile(dek, ctx, path, mime) {
21287
- const info = await stat3(path).catch((error48) => {
21288
- throw fileError(error48);
21289
- });
21290
- if (!info.isFile()) {
21291
- throw new ClientError(`${path} is not a regular file; only regular files can be attached`);
21292
- }
21293
- await this.assertWithinLimit(path, info.size);
23985
+ async encrypt(dek, ctx, file2) {
21294
23986
  const plainHash = await createStreamingSha256();
21295
23987
  const cipherHash = await createStreamingSha256();
21296
23988
  let size = 0;
21297
23989
  async function* plaintext() {
21298
- try {
21299
- for await (const chunk of createReadStream(path)) {
21300
- const bytes = chunk;
21301
- plainHash.update(bytes);
21302
- size += bytes.length;
21303
- yield bytes;
21304
- }
21305
- } catch (error48) {
21306
- throw fileError(error48);
23990
+ for await (const bytes of file2.bytes) {
23991
+ plainHash.update(bytes);
23992
+ size += bytes.length;
23993
+ yield bytes;
21307
23994
  }
21308
23995
  }
21309
23996
  const parts = [];
@@ -21315,49 +24002,41 @@ class AttachmentService {
21315
24002
  ciphertext: new Blob(parts),
21316
24003
  blobSha256: cipherHash.digestHex(),
21317
24004
  meta: {
21318
- filename: basename3(path),
21319
- mime,
24005
+ filename: file2.filename,
24006
+ mime: file2.mime,
21320
24007
  size,
21321
24008
  sha256_plain: plainHash.digestHex()
21322
24009
  }
21323
24010
  };
21324
24011
  }
21325
- async assertWithinLimit(path, size) {
24012
+ async assertWithinLimit(what, size) {
21326
24013
  const { max_file_size_bytes: limit } = await this.ctx.api.limits();
21327
24014
  if (!Number.isSafeInteger(limit) || limit <= 0) {
21328
24015
  throw new ClientError("The server did not report a usable upload limit (max_file_size_bytes)");
21329
24016
  }
21330
24017
  const sealedSize = attachmentCiphertextSize(size);
21331
24018
  if (sealedSize > limit) {
21332
- throw new ClientError(`${path} is too large to attach: it is ${size} bytes, ${sealedSize} bytes once sealed, and the server accepts at most ${limit} bytes`);
24019
+ throw new ClientError(`${what} is too large to attach: it is ${size} bytes, ${sealedSize} bytes once sealed, and the server accepts at most ${limit} bytes`);
21333
24020
  }
21334
24021
  }
21335
- async writeVerified(path, plaintext, meta3) {
24022
+ async write(path, plaintext) {
21336
24023
  let handle;
21337
24024
  try {
21338
- handle = await open3(path, "wx", DOWNLOAD_FILE_MODE);
24025
+ handle = await open4(path, "wx", DOWNLOAD_FILE_MODE);
21339
24026
  } catch (error48) {
21340
24027
  if (error48.code === "EEXIST") {
21341
24028
  throw new ClientError(`${path} already exists; choose a new out_path`);
21342
24029
  }
21343
24030
  throw fileError(error48);
21344
24031
  }
21345
- let verified = false;
24032
+ let complete = false;
21346
24033
  try {
21347
- const hash2 = await createStreamingSha256();
21348
- let size = 0;
21349
- for await (const chunk of plaintext) {
21350
- hash2.update(chunk);
21351
- size += chunk.length;
24034
+ for await (const chunk of plaintext)
21352
24035
  await handle.write(chunk);
21353
- }
21354
- if (size !== meta3.size || hash2.digestHex() !== meta3.sha256_plain) {
21355
- throw new ClientError("Downloaded attachment does not match its sealed size and hash");
21356
- }
21357
- verified = true;
24036
+ complete = true;
21358
24037
  } finally {
21359
24038
  await handle.close();
21360
- if (!verified)
24039
+ if (!complete)
21361
24040
  await unlink(path);
21362
24041
  }
21363
24042
  }
@@ -21469,7 +24148,7 @@ class NewSecrets {
21469
24148
  asks(args) {
21470
24149
  return [args.passphrase, args.key_out_path, args.store].some((value) => value !== undefined);
21471
24150
  }
21472
- async prepare(ctx, args, passphraseArg, label) {
24151
+ async prepare(ctx, args, passphraseArg, label2) {
21473
24152
  const { passphrase, key_out_path: keyOutPath, store } = args;
21474
24153
  const given = [passphrase, keyOutPath, store].filter((value) => value !== undefined);
21475
24154
  if (given.length === 0)
@@ -21480,7 +24159,7 @@ class NewSecrets {
21480
24159
  if (passphrase !== undefined)
21481
24160
  return typedPassphrase(passphrase);
21482
24161
  if (store === "sidecar") {
21483
- return promptedPassphrase(await this.stores.sidecar().create(ctx.root, label));
24162
+ return promptedPassphrase(await this.stores.sidecar().create(ctx.root, label2));
21484
24163
  }
21485
24164
  const file2 = await buildKeyFile(ctx, generateSecretKey());
21486
24165
  if (keyOutPath !== undefined)
@@ -21559,6 +24238,7 @@ class CreateService {
21559
24238
  return this.create({
21560
24239
  id,
21561
24240
  placement: await this.placement(args.space_id, args.parent_id),
24241
+ position: args.position ?? NEW_NODE_POSITION,
21562
24242
  isFolder: false,
21563
24243
  title: args.title,
21564
24244
  dek,
@@ -21580,6 +24260,7 @@ class CreateService {
21580
24260
  return this.create({
21581
24261
  id,
21582
24262
  placement: await this.placement(args.space_id, args.parent_id),
24263
+ position: args.position ?? NEW_NODE_POSITION,
21583
24264
  isFolder: true,
21584
24265
  title: args.name,
21585
24266
  dek,
@@ -21593,8 +24274,8 @@ class CreateService {
21593
24274
  await this.ctx.names.assertUsable(name, null);
21594
24275
  }
21595
24276
  }
21596
- newSecret(id, args, label) {
21597
- return this.newSecrets.prepare({ tenant: this.ctx.tenant, root: id }, args, "passphrase", label);
24277
+ newSecret(id, args, label2) {
24278
+ return this.newSecrets.prepare({ tenant: this.ctx.tenant, root: id }, args, "passphrase", label2);
21598
24279
  }
21599
24280
  async placement(spaceId, parentId) {
21600
24281
  if (parentId === undefined) {
@@ -21679,7 +24360,7 @@ class CreateService {
21679
24360
  space: spaceId,
21680
24361
  parent: parentId,
21681
24362
  isFolder: node.isFolder,
21682
- position: NEW_NODE_POSITION,
24363
+ position: node.position,
21683
24364
  seal: seal2
21684
24365
  }, signerRootIds);
21685
24366
  return {
@@ -21687,7 +24368,7 @@ class CreateService {
21687
24368
  space_id: spaceId,
21688
24369
  parent_id: parentId,
21689
24370
  is_folder: node.isFolder,
21690
- position: NEW_NODE_POSITION,
24371
+ position: node.position,
21691
24372
  seal_root_id: sealRootId,
21692
24373
  seal: seal2,
21693
24374
  enc_dek: encDek,
@@ -21699,68 +24380,6 @@ class CreateService {
21699
24380
  }
21700
24381
  }
21701
24382
 
21702
- // src/sealed/credentialAccess.ts
21703
- function visibleOf(credential2) {
21704
- return {
21705
- title: credential2.title,
21706
- username: credential2.username,
21707
- url: credential2.url,
21708
- tags: credential2.tags,
21709
- notes: credential2.notes,
21710
- custom: credential2.custom.flatMap(({ key, value }) => value === undefined ? [] : [{ key, value }])
21711
- };
21712
- }
21713
- function assertDistinctCustomKeys(visible, values) {
21714
- const keys = [...visible.custom, ...values.custom].map(({ key }) => key);
21715
- if (keys.some((key) => key.length === 0)) {
21716
- throw new ClientError("A custom field needs a key");
21717
- }
21718
- if (new Set(keys).size !== keys.length) {
21719
- throw new ClientError("Custom field keys must be unique");
21720
- }
21721
- }
21722
-
21723
- class CredentialAccess {
21724
- ctx;
21725
- constructor(ctx) {
21726
- this.ctx = ctx;
21727
- }
21728
- context(credentialId) {
21729
- return { tenant: this.ctx.tenant, credential: credentialId };
21730
- }
21731
- async newDek(rootId, credentialId) {
21732
- const { keys } = this.ctx.session.require(rootId);
21733
- const dek = generateDek();
21734
- return {
21735
- dek,
21736
- encDek: serializeEnvelope(await wrapDek(keys, credentialId, dek))
21737
- };
21738
- }
21739
- async rewrapDek(dek, rootId, credentialId) {
21740
- const { keys } = this.ctx.session.require(rootId);
21741
- return serializeEnvelope(await wrapDek(keys, credentialId, dek));
21742
- }
21743
- async open(credentialId) {
21744
- const credential2 = await this.ctx.api.fetchSealedCredential(credentialId);
21745
- const { keys } = this.ctx.session.require(credential2.seal_root_id);
21746
- const dek = await unwrapDek(keys, credentialId, parseEnvelope(credential2.enc_dek));
21747
- const values = await unsealCredentialFields(dek, this.context(credentialId), credential2.enc_fields);
21748
- return { credential: credential2, dek, values };
21749
- }
21750
- async seal(credentialId, dek, visible, values) {
21751
- assertDistinctCustomKeys(visible, values);
21752
- return {
21753
- visible,
21754
- encFields: await sealCredentialFields(dek, this.context(credentialId), values),
21755
- visibleDigest: await visibleFieldsDigest(visible)
21756
- };
21757
- }
21758
- async sign(message, signerRootIds) {
21759
- const signers = signerRootIds.map((rootId) => this.ctx.session.require(rootId).keys);
21760
- return signMessage(await buildCredentialMessage(message), signers);
21761
- }
21762
- }
21763
-
21764
24383
  // src/sealed/secretScan.ts
21765
24384
  var PEM_BLOCK = /-----BEGIN [A-Z0-9 ]+-----/;
21766
24385
  var SECRET_WORD = "pass(?:word|wd|phrase|code|wort)?|kennwort|geheim(?:nis)?|pwd|pin|puk|tan|secret|token|api[ _-]?key|access[ _-]?key|private[ _-]?key|client[ _-]?secret|bearer|recovery[ _-]?codes?|backup[ _-]?codes?|seed|cvv|cvc";
@@ -21840,7 +24459,7 @@ function scanForSecrets(text) {
21840
24459
  var NOTES = "notes";
21841
24460
  var NAMED_SECRETS = ["password", "totp", NOTES];
21842
24461
  var SCAN_DISCLAIMER = "notes_look_secret is an advisory pattern scan. It misses secrets (ordinary-word passphrases, other languages, anything it has no pattern for), so do not treat an unflagged note as safe.";
21843
- var CONFLICT = 409;
24462
+ var CONFLICT2 = 409;
21844
24463
  async function hashOf(value) {
21845
24464
  return sha256Hex(utf8Encode(value));
21846
24465
  }
@@ -22063,8 +24682,8 @@ class CredentialMigration {
22063
24682
  }
22064
24683
  }
22065
24684
  async verify(rootId, credentialId, entry) {
22066
- const open4 = await this.withoutLeftoverHistory(rootId, await this.access.open(credentialId));
22067
- const { credential: credential2, values } = open4;
24685
+ const open5 = await this.withoutLeftoverHistory(rootId, await this.access.open(credentialId));
24686
+ const { credential: credential2, values } = open5;
22068
24687
  const visible = visibleOf(credential2);
22069
24688
  if (await visibleFieldsDigest(visible) !== credential2.visible_sig) {
22070
24689
  throw new ClientError("its visible fields no longer match the signed digest");
@@ -22081,10 +24700,10 @@ class CredentialMigration {
22081
24700
  throw new ClientError("its sealed values do not match the dry run");
22082
24701
  }
22083
24702
  }
22084
- async withoutLeftoverHistory(rootId, open4) {
22085
- const { credential: credential2 } = open4;
24703
+ async withoutLeftoverHistory(rootId, open5) {
24704
+ const { credential: credential2 } = open5;
22086
24705
  if (credential2.server_encrypted_history === 0)
22087
- return open4;
24706
+ return open5;
22088
24707
  try {
22089
24708
  await this.ctx.api.sealCredential(credential2.id, {
22090
24709
  base_seq: credential2.version,
@@ -22095,7 +24714,7 @@ class CredentialMigration {
22095
24714
  sigs: {}
22096
24715
  });
22097
24716
  } catch (error48) {
22098
- if (!(error48 instanceof ApiError && error48.status === CONFLICT)) {
24717
+ if (!(error48 instanceof ApiError && error48.status === CONFLICT2)) {
22099
24718
  throw error48;
22100
24719
  }
22101
24720
  }
@@ -22127,7 +24746,7 @@ class CredentialMigration {
22127
24746
  }
22128
24747
 
22129
24748
  // src/sealed/credentialService.ts
22130
- var NOT_FOUND = 404;
24749
+ var NOT_FOUND2 = 404;
22131
24750
  var CUSTOM_PREFIX2 = customFieldName("");
22132
24751
  var VISIBLE_NAMES = [
22133
24752
  "title",
@@ -22176,28 +24795,28 @@ class CredentialService {
22176
24795
  this.access = access;
22177
24796
  }
22178
24797
  async reveal(credentialId, field2) {
22179
- const open4 = await this.access.open(credentialId);
22180
- const value = this.valueOf(open4, field2);
24798
+ const open5 = await this.access.open(credentialId);
24799
+ const value = this.valueOf(open5, field2);
22181
24800
  return {
22182
24801
  id: credentialId,
22183
- version: open4.credential.version,
24802
+ version: open5.credential.version,
22184
24803
  field: field2,
22185
24804
  value,
22186
- ...await this.attestation(open4.credential)
24805
+ ...await this.attestation(open5.credential)
22187
24806
  };
22188
24807
  }
22189
24808
  async totp(credentialId, nowMs = Date.now()) {
22190
- const open4 = await this.access.open(credentialId);
22191
- const code = await totp(parseTotp(this.valueOf(open4, "totp")), nowMs);
24809
+ const open5 = await this.access.open(credentialId);
24810
+ const code = await totp(parseTotp(this.valueOf(open5, "totp")), nowMs);
22192
24811
  return {
22193
24812
  id: credentialId,
22194
24813
  code: code.code,
22195
24814
  seconds_remaining: code.secondsRemaining,
22196
- ...await this.attestation(open4.credential)
24815
+ ...await this.attestation(open5.credential)
22197
24816
  };
22198
24817
  }
22199
- valueOf(open4, field2) {
22200
- const { values, credential: credential2 } = open4;
24818
+ valueOf(open5, field2) {
24819
+ const { values, credential: credential2 } = open5;
22201
24820
  const value = field2.startsWith(CUSTOM_PREFIX2) ? values.custom.find((item) => item.key === field2.slice(CUSTOM_PREFIX2.length))?.value : SEALED_CREDENTIAL_FIELDS.includes(field2) ? values[field2] : undefined;
22202
24821
  if (value === undefined || value === null) {
22203
24822
  throw new ClientError(`Credential ${credential2.id} has no sealed field '${field2}'. Sealed fields: password, totp, sealed_notes, custom:{key}.`);
@@ -22221,7 +24840,7 @@ class CredentialService {
22221
24840
  try {
22222
24841
  attested = await this.ctx.api.lastAttestedVisibleFields(credential2.id);
22223
24842
  } catch (error48) {
22224
- if (error48 instanceof ApiError && error48.status === NOT_FOUND)
24843
+ if (error48 instanceof ApiError && error48.status === NOT_FOUND2)
22225
24844
  return null;
22226
24845
  throw error48;
22227
24846
  }
@@ -22269,12 +24888,13 @@ class CredentialService {
22269
24888
  visible: save.visible,
22270
24889
  enc_fields: save.encFields,
22271
24890
  enc_dek: encDek,
22272
- sigs
24891
+ sigs,
24892
+ ...args.display
22273
24893
  }));
22274
24894
  }
22275
24895
  async update(args) {
22276
- const open4 = await this.access.open(args.credential_id);
22277
- const { credential: credential2, values, dek } = open4;
24896
+ const open5 = await this.access.open(args.credential_id);
24897
+ const { credential: credential2, values, dek } = open5;
22278
24898
  assertVersion(credential2, args.expected_version);
22279
24899
  const current = visibleOf(credential2);
22280
24900
  const attestation = await this.attestation(credential2);
@@ -22366,8 +24986,8 @@ var UNREADABLE = {
22366
24986
  keywords: [],
22367
24987
  summary_readable: false
22368
24988
  };
22369
- async function readDepositedSummary(ctx, access, open4) {
22370
- const { node, dek, meta: meta3 } = open4;
24989
+ async function readDepositedSummary(ctx, access, open5) {
24990
+ const { node, dek, meta: meta3 } = open5;
22371
24991
  const [stored] = await ctx.api.indexEntries(node.seal_root_id, [node.id]);
22372
24992
  if (stored === undefined)
22373
24993
  return UNREADABLE;
@@ -22445,14 +25065,14 @@ class DepositService {
22445
25065
  }
22446
25066
  }
22447
25067
  async accept(documentId) {
22448
- const open4 = await this.access.open(documentId);
22449
- const { node, meta: meta3 } = open4;
25068
+ const open5 = await this.access.open(documentId);
25069
+ const { node, meta: meta3 } = open5;
22450
25070
  if (!node.deposited || node.box_dek === null) {
22451
25071
  throw new ClientError(`${documentId} is not a pending deposit`);
22452
25072
  }
22453
- const { keys } = open4.root;
22454
- const { body } = await this.documents.storedBody(open4);
22455
- const deposited = await readDepositedSummary(this.ctx, this.access, open4);
25073
+ const { keys } = open5.root;
25074
+ const { body } = await this.documents.storedBody(open5);
25075
+ const deposited = await readDepositedSummary(this.ctx, this.access, open5);
22456
25076
  const dek = generateDek();
22457
25077
  const fields2 = await sealDocument(dek, this.access.nodeContext(node.id), {
22458
25078
  title: meta3.title,
@@ -22529,12 +25149,12 @@ class DocumentService {
22529
25149
  this.access = access;
22530
25150
  }
22531
25151
  async read(documentId) {
22532
- const open4 = await this.access.open(documentId);
22533
- const { node, meta: meta3 } = open4;
25152
+ const open5 = await this.access.open(documentId);
25153
+ const { node, meta: meta3 } = open5;
22534
25154
  const deposit = node.deposited ? {
22535
25155
  pending: true,
22536
25156
  untrusted: UNTRUSTED_DEPOSIT_NOTICE,
22537
- ...await readDepositedSummary(this.ctx, this.access, open4)
25157
+ ...await readDepositedSummary(this.ctx, this.access, open5)
22538
25158
  } : { pending: false };
22539
25159
  return {
22540
25160
  id: node.id,
@@ -22547,31 +25167,31 @@ class DocumentService {
22547
25167
  archived: node.archived,
22548
25168
  attachment_count: node.attachment_count,
22549
25169
  ...deposit,
22550
- body: await this.bodyOf(open4)
25170
+ body: await this.bodyOf(open5)
22551
25171
  };
22552
25172
  }
22553
25173
  async toc(documentId) {
22554
- const open4 = await this.access.open(documentId);
25174
+ const open5 = await this.access.open(documentId);
22555
25175
  return {
22556
25176
  id: documentId,
22557
- seal_seq: open4.node.seal_seq,
22558
- title: open4.meta.title,
22559
- ...open4.node.deposited ? { untrusted: UNTRUSTED_DEPOSIT_NOTICE } : {},
22560
- headings: parseMarkdown(await this.bodyOf(open4)).headings
25177
+ seal_seq: open5.node.seal_seq,
25178
+ title: open5.meta.title,
25179
+ ...open5.node.deposited ? { untrusted: UNTRUSTED_DEPOSIT_NOTICE } : {},
25180
+ headings: parseMarkdown(await this.bodyOf(open5)).headings
22561
25181
  };
22562
25182
  }
22563
25183
  async section(documentId, anchor) {
22564
- const open4 = await this.access.open(documentId);
25184
+ const open5 = await this.access.open(documentId);
22565
25185
  return {
22566
25186
  id: documentId,
22567
- seal_seq: open4.node.seal_seq,
25187
+ seal_seq: open5.node.seal_seq,
22568
25188
  anchor,
22569
- ...open4.node.deposited ? { untrusted: UNTRUSTED_DEPOSIT_NOTICE } : {},
22570
- content: extractSection(await this.bodyOf(open4), anchor)
25189
+ ...open5.node.deposited ? { untrusted: UNTRUSTED_DEPOSIT_NOTICE } : {},
25190
+ content: extractSection(await this.bodyOf(open5), anchor)
22571
25191
  };
22572
25192
  }
22573
- async storedBody(open4) {
22574
- const { node, dek } = open4;
25193
+ async storedBody(open5) {
25194
+ const { node, dek } = open5;
22575
25195
  if (node.is_folder) {
22576
25196
  throw new ClientError(`${node.id} is a sealed folder; it has no body`);
22577
25197
  }
@@ -22584,13 +25204,13 @@ class DocumentService {
22584
25204
  body: await decryptBody(dek, this.access.nodeContext(node.id), parseEnvelope(stored.body))
22585
25205
  };
22586
25206
  }
22587
- async bodyOf(open4) {
22588
- return (await this.storedBody(open4)).body;
25207
+ async bodyOf(open5) {
25208
+ return (await this.storedBody(open5)).body;
22589
25209
  }
22590
25210
  async update(args) {
22591
- const open4 = await this.access.open(args.document_id);
22592
- assertSeq(open4.node, args.expected_seq);
22593
- assertAccepted(open4.node);
25211
+ const open5 = await this.access.open(args.document_id);
25212
+ assertSeq(open5.node, args.expected_seq);
25213
+ assertAccepted(open5.node);
22594
25214
  const { document_id: _id, expected_seq: _seq, ...changes } = args;
22595
25215
  if (Object.values(changes).every((value) => value === undefined)) {
22596
25216
  throw new ClientError("Nothing to update: pass at least one field");
@@ -22598,51 +25218,51 @@ class DocumentService {
22598
25218
  if (args.summary === undefined !== (args.summary_short === undefined)) {
22599
25219
  throw new ClientError("Pass summary and summary_short together");
22600
25220
  }
22601
- const { id } = open4.node;
25221
+ const { id } = open5.node;
22602
25222
  const { title } = args;
22603
- const renamesRoot = title !== undefined && id === open4.node.seal_root_id;
22604
- if (renamesRoot && title !== open4.meta.title) {
25223
+ const renamesRoot = title !== undefined && id === open5.node.seal_root_id;
25224
+ if (renamesRoot && title !== open5.meta.title) {
22605
25225
  await this.ctx.names.assertUsable(title, id);
22606
25226
  }
22607
- const result = await (open4.node.is_folder ? this.updateFolder(open4, args) : this.updateDocument(open4, args));
25227
+ const result = await (open5.node.is_folder ? this.updateFolder(open5, args) : this.updateDocument(open5, args));
22608
25228
  if (renamesRoot) {
22609
25229
  await this.ctx.names.follow(`Seal root ${id} was renamed`, () => this.ctx.names.remember(id, title));
22610
25230
  }
22611
25231
  return result;
22612
25232
  }
22613
25233
  async replace(args) {
22614
- const open4 = await this.access.open(args.document_id);
22615
- assertSeq(open4.node, args.expected_seq);
22616
- assertAccepted(open4.node);
22617
- const body = await this.bodyOf(open4);
25234
+ const open5 = await this.access.open(args.document_id);
25235
+ assertSeq(open5.node, args.expected_seq);
25236
+ assertAccepted(open5.node);
25237
+ const body = await this.bodyOf(open5);
22618
25238
  const occurrences = countOccurrences(body, args.old_text);
22619
25239
  if (occurrences !== 1) {
22620
25240
  throw new ClientError(`old_text must occur exactly once in the document; it occurs ${occurrences} times`);
22621
25241
  }
22622
- return this.updateDocument(open4, {
25242
+ return this.updateDocument(open5, {
22623
25243
  body: body.replace(args.old_text, () => args.new_text)
22624
25244
  });
22625
25245
  }
22626
- async updateFolder(open4, changes) {
25246
+ async updateFolder(open5, changes) {
22627
25247
  const { body, summary, keywords } = changes;
22628
25248
  if (body !== undefined || summary !== undefined || keywords !== undefined) {
22629
25249
  throw new ClientError("A sealed folder has only a title and tags");
22630
25250
  }
22631
25251
  const meta3 = {
22632
- ...open4.meta,
22633
- title: changes.title ?? open4.meta.title,
22634
- tags: changes.tags ?? open4.meta.tags
25252
+ ...open5.meta,
25253
+ title: changes.title ?? open5.meta.title,
25254
+ tags: changes.tags ?? open5.meta.tags
22635
25255
  };
22636
- return this.sendUpdate(open4, {
25256
+ return this.sendUpdate(open5, {
22637
25257
  encBody: null,
22638
- encMeta: await sealMeta(open4.dek, this.access.nodeContext(open4.node.id), meta3),
25258
+ encMeta: await sealMeta(open5.dek, this.access.nodeContext(open5.node.id), meta3),
22639
25259
  encIndex: null
22640
25260
  });
22641
25261
  }
22642
- async updateDocument(open4, changes) {
22643
- const { node, dek, meta: meta3 } = open4;
25262
+ async updateDocument(open5, changes) {
25263
+ const { node, dek, meta: meta3 } = open5;
22644
25264
  const ctx = this.access.nodeContext(node.id);
22645
- const current = await this.indexEntryOf(open4);
25265
+ const current = await this.indexEntryOf(open5);
22646
25266
  const content = {
22647
25267
  title: changes.title ?? meta3.title,
22648
25268
  tags: changes.tags ?? meta3.tags,
@@ -22659,7 +25279,7 @@ class DocumentService {
22659
25279
  summary_stale: content.summaryStale,
22660
25280
  keywords: content.keywords
22661
25281
  } : indexEntryOf({ ...content, body: changes.body });
22662
- return this.sendUpdate(open4, {
25282
+ return this.sendUpdate(open5, {
22663
25283
  encBody: changes.body === undefined ? null : await sealBody(dek, ctx, changes.body),
22664
25284
  encMeta: await sealMeta(dek, ctx, {
22665
25285
  title: content.title,
@@ -22670,8 +25290,8 @@ class DocumentService {
22670
25290
  encIndex: await sealIndexEntry(dek, ctx, entry)
22671
25291
  });
22672
25292
  }
22673
- async indexEntryOf(open4) {
22674
- const { node, dek } = open4;
25293
+ async indexEntryOf(open5) {
25294
+ const { node, dek } = open5;
22675
25295
  const [stored] = await this.ctx.api.indexEntries(node.seal_root_id, [
22676
25296
  node.id
22677
25297
  ]);
@@ -22680,8 +25300,8 @@ class DocumentService {
22680
25300
  }
22681
25301
  return decryptIndexEntry(dek, this.access.nodeContext(node.id), parseEnvelope(stored.enc_index));
22682
25302
  }
22683
- async sendUpdate(open4, fields2) {
22684
- const { node } = open4;
25303
+ async sendUpdate(open5, fields2) {
25304
+ const { node } = open5;
22685
25305
  const sigs = await this.access.sign({
22686
25306
  op: "update",
22687
25307
  tenant: this.ctx.tenant,
@@ -23139,13 +25759,13 @@ class TreeService {
23139
25759
  }
23140
25760
  }
23141
25761
  async export(args) {
23142
- const open4 = await this.access.open(args.document_id);
23143
- const { node, meta: meta3 } = open4;
25762
+ const open5 = await this.access.open(args.document_id);
25763
+ const { node, meta: meta3 } = open5;
23144
25764
  assertAccepted(node);
23145
25765
  if (node.attachment_count > 0) {
23146
25766
  throw new ClientError(`${node.id} has ${node.attachment_count} sealed attachment(s), which an export would destroy. Download them and delete them from the document first.`);
23147
25767
  }
23148
- const { body } = await this.documents.storedBody(open4);
25768
+ const { body } = await this.documents.storedBody(open5);
23149
25769
  const copy = await this.ctx.api.createPlainDocument({
23150
25770
  title: meta3.title,
23151
25771
  body,
@@ -23586,11 +26206,11 @@ class SidecarConsole {
23586
26206
  async run() {
23587
26207
  while (!await this.inTurn(() => this.commandTurn())) {}
23588
26208
  }
23589
- newSecret(rootId, label, signal) {
26209
+ newSecret(rootId, label2, signal) {
23590
26210
  return this.inTurn(async () => {
23591
26211
  signal.throwIfAborted();
23592
26212
  this.reader.write(`
23593
- The MCP client asks for a new passphrase for seal root ${rootId} ("${printable2(label)}").
26213
+ The MCP client asks for a new passphrase for seal root ${rootId} ("${printable2(label2)}").
23594
26214
  `);
23595
26215
  const secret2 = await this.readNewPassphrase(signal);
23596
26216
  if (secret2 === null)
@@ -23837,7 +26457,7 @@ class SecretHolder {
23837
26457
  import { chmod, lstat, rm as rm3 } from "node:fs/promises";
23838
26458
  import { connect as connect2, createServer } from "node:net";
23839
26459
  var SOCKET_MODE = 384;
23840
- var OBJECT_ID2 = /^[0-9a-f]{24}$/;
26460
+ var OBJECT_ID4 = /^[0-9a-f]{24}$/;
23841
26461
  var MAX_REQUEST_BYTES = 4096;
23842
26462
  var CLIENT_GONE = "The client that asked is gone";
23843
26463
  var MAX_SOCKET_PATH_BYTES = 103;
@@ -23868,7 +26488,7 @@ async function clearSocketPath(socketPath) {
23868
26488
  await rm3(socketPath);
23869
26489
  }
23870
26490
  function requireRootId2(value) {
23871
- if (typeof value !== "string" || !OBJECT_ID2.test(value)) {
26491
+ if (typeof value !== "string" || !OBJECT_ID4.test(value)) {
23872
26492
  throw new ClientError("root_id must be a 24-character id");
23873
26493
  }
23874
26494
  return value;
@@ -24004,8 +26624,8 @@ class SidecarServer {
24004
26624
  }
24005
26625
  return this.holder.secretsOf(rootId);
24006
26626
  }
24007
- async create(rootId, label, clientGone) {
24008
- const chosen = await this.prompter.newSecret(rootId, label, clientGone);
26627
+ async create(rootId, label2, clientGone) {
26628
+ const chosen = await this.prompter.newSecret(rootId, label2, clientGone);
24009
26629
  clientGone.throwIfAborted();
24010
26630
  if (chosen === null) {
24011
26631
  throw new ClientError(`The sidecar's user entered no new passphrase for seal root ${rootId}`);
@@ -24071,7 +26691,7 @@ async function runSidecar(settings, names) {
24071
26691
  import { mkdir as mkdir3 } from "node:fs/promises";
24072
26692
 
24073
26693
  // src/stores/storeConfig.ts
24074
- import { existsSync as existsSync3 } from "node:fs";
26694
+ import { existsSync as existsSync4 } from "node:fs";
24075
26695
  import { dirname as dirname5, join as join7 } from "node:path";
24076
26696
  var STORE_CONFIG_NAME = "stores.json";
24077
26697
  var DEFAULT_KEY_DIR_NAME = "keys";
@@ -24097,7 +26717,7 @@ function assertStoreConfig(value, path) {
24097
26717
  }
24098
26718
  async function loadStoreConfig(tokenConfigPath) {
24099
26719
  const path = storeConfigPath(tokenConfigPath);
24100
- if (!existsSync3(path))
26720
+ if (!existsSync4(path))
24101
26721
  return null;
24102
26722
  let text;
24103
26723
  try {
@@ -24126,7 +26746,7 @@ function keyFilesNotice(keyDir) {
24126
26746
  function defaultKeyFilesDir(config2) {
24127
26747
  return config2.default_store === "file" ? config2.key_dir : null;
24128
26748
  }
24129
- function describe3(config2) {
26749
+ function describe4(config2) {
24130
26750
  return config2.default_store === "system" ? "system keyring" : `key file directory ${config2.key_dir}`;
24131
26751
  }
24132
26752
  async function choose(request, before) {
@@ -24149,7 +26769,7 @@ async function save(request, chosen) {
24149
26769
  await mkdir3(keyDir, { recursive: true, mode: PRIVATE_DIR_MODE3 });
24150
26770
  }
24151
26771
  await saveStoreConfig(request.configPath, chosen);
24152
- request.log(keyDir === null ? `New sealed keys will be kept in the ${describe3(chosen)}.` : keyFilesNotice(keyDir));
26772
+ request.log(keyDir === null ? `New sealed keys will be kept in the ${describe4(chosen)}.` : keyFilesNotice(keyDir));
24153
26773
  request.log(`Store settings saved to ${storeConfigPath(request.configPath)}`);
24154
26774
  return chosen;
24155
26775
  }
@@ -24407,11 +27027,11 @@ class SidecarStore {
24407
27027
  note: `the sidecar holds no secret for seal root ${seal2.rootId} (enter it there with: add ${seal2.rootId})`
24408
27028
  };
24409
27029
  }
24410
- async create(rootId, label) {
27030
+ async create(rootId, label2) {
24411
27031
  const { secret: secret2, entry_id } = await this.client.request({
24412
27032
  op: "new",
24413
27033
  root_id: rootId,
24414
- label
27034
+ label: label2
24415
27035
  });
24416
27036
  return {
24417
27037
  value: secret2,
@@ -24757,28 +27377,72 @@ function createSealedTools(ctx) {
24757
27377
  ];
24758
27378
  }
24759
27379
 
27380
+ // src/transfer/console.ts
27381
+ class TransferConsole {
27382
+ reader;
27383
+ transfer;
27384
+ names;
27385
+ constructor(reader, transfer, names) {
27386
+ this.reader = reader;
27387
+ this.transfer = transfer;
27388
+ this.names = names;
27389
+ }
27390
+ async export(path) {
27391
+ await this.transfer.exportable();
27392
+ const keys = await this.transfer.export(path, await readNewPassphrase(this.reader, "every key in the file"));
27393
+ this.reader.write(`Exported ${keys.length} key(s) to ${path}:
27394
+ `);
27395
+ await this.writeRoots(keys);
27396
+ this.reader.write(`The file opens only with the passphrase you typed: keep both, and keep them apart.
27397
+ ` + `A seal root that is opened by a passphrase has no key on this machine and is not in the file.
27398
+ `);
27399
+ }
27400
+ async import(path) {
27401
+ const passphrase = await readPassphrase(this.reader);
27402
+ const { imported, store, present } = await this.transfer.import(path, passphrase);
27403
+ if (store !== null) {
27404
+ this.reader.write(`Imported ${imported.length} key(s) into ${KEY_STORE_LABELS[store]}:
27405
+ `);
27406
+ await this.writeRoots(imported);
27407
+ }
27408
+ if (present.length > 0) {
27409
+ this.reader.write(`${present.length} key(s) were already on this machine and were left as they are:
27410
+ `);
27411
+ await this.writeRoots(present);
27412
+ }
27413
+ }
27414
+ async writeRoots(keys) {
27415
+ for (const { root_id } of keys) {
27416
+ this.reader.write(` ${printable2(await this.names.describe(root_id))}
27417
+ `);
27418
+ }
27419
+ }
27420
+ }
27421
+
27422
+ // src/transfer/keyTransfer.ts
27423
+ import { readFile } from "node:fs/promises";
27424
+
24760
27425
  // src/transfer/keyBundle.ts
24761
27426
  var KEY_BUNDLE_KIND = "safekb-key-export";
24762
27427
  var KEY_BUNDLE_VERSION = 1;
24763
- var RECOMMENDED_EXPORT_PASSPHRASE_LENGTH = 12;
24764
- var KEY_INFO = "safekb/key-export/v1";
27428
+ var KEY_INFO2 = "safekb/key-export/v1";
24765
27429
  var EXPORT_KDF = MIN_KDF_PARAMS;
24766
- var MAX_KDF = { m: 256 * 1024, t: 8, p: 4 };
24767
- var OBJECT_ID3 = /^[0-9a-f]{24}$/;
24768
- function malformed(why) {
27430
+ var MAX_KDF2 = { m: 256 * 1024, t: 8, p: 4 };
27431
+ var OBJECT_ID5 = /^[0-9a-f]{24}$/;
27432
+ function malformed2(why) {
24769
27433
  return new ClientError(`Not a SafeKB key export: ${why}`);
24770
27434
  }
24771
27435
  function aadOf(salt, kdf) {
24772
- return `${KEY_INFO}|${salt}|${kdf.m}|${kdf.t}|${kdf.p}`;
27436
+ return `${KEY_INFO2}|${salt}|${kdf.m}|${kdf.t}|${kdf.p}`;
24773
27437
  }
24774
27438
  async function bundleKey(passphrase, salt, kdf) {
24775
- return deriveKey(await derivePassphraseIkm(passphrase, salt, kdf), KEY_INFO);
27439
+ return deriveKey(await derivePassphraseIkm(passphrase, salt, kdf), KEY_INFO2);
24776
27440
  }
24777
27441
  function assertContent(value) {
24778
27442
  const raw = value;
24779
27443
  const names = raw?.names;
24780
- if (!Array.isArray(raw?.keys) || typeof names !== "object" || names === null || Array.isArray(names) || !Object.entries(names).every(([rootId, name]) => OBJECT_ID3.test(rootId) && typeof name === "string" && name.length > 0)) {
24781
- throw malformed("its content is not keys and the names of their roots");
27444
+ if (!Array.isArray(raw?.keys) || typeof names !== "object" || names === null || Array.isArray(names) || !Object.entries(names).every(([rootId, name]) => OBJECT_ID5.test(rootId) && typeof name === "string" && name.length > 0)) {
27445
+ throw malformed2("its content is not keys and the names of their roots");
24782
27446
  }
24783
27447
  return { keys: raw.keys.map(assertKeyFile), names: { ...names } };
24784
27448
  }
@@ -24787,22 +27451,22 @@ function parseBundle(text) {
24787
27451
  try {
24788
27452
  value = JSON.parse(text);
24789
27453
  } catch {
24790
- throw malformed("it is not valid JSON");
27454
+ throw malformed2("it is not valid JSON");
24791
27455
  }
24792
27456
  const raw = value;
24793
27457
  if (raw?.kind !== KEY_BUNDLE_KIND) {
24794
- throw malformed(`its kind is not '${KEY_BUNDLE_KIND}'`);
27458
+ throw malformed2(`its kind is not '${KEY_BUNDLE_KIND}'`);
24795
27459
  }
24796
27460
  if (raw.v !== KEY_BUNDLE_VERSION) {
24797
- throw malformed(`its version is not ${KEY_BUNDLE_VERSION}; use the version of @safekb/mcp that wrote it`);
27461
+ throw malformed2(`its version is not ${KEY_BUNDLE_VERSION}; use the version of @safekb/mcp that wrote it`);
24798
27462
  }
24799
27463
  const { kdf, salt } = raw;
24800
27464
  if (typeof kdf !== "object" || kdf === null || typeof salt !== "string") {
24801
- throw malformed("it has no KDF parameters and salt");
27465
+ throw malformed2("it has no KDF parameters and salt");
24802
27466
  }
24803
27467
  const params = assertKdfParams(kdf);
24804
- if (params.m > MAX_KDF.m || params.t > MAX_KDF.t || params.p > MAX_KDF.p) {
24805
- throw malformed("its KDF parameters are beyond what this client runs");
27468
+ if (params.m > MAX_KDF2.m || params.t > MAX_KDF2.t || params.p > MAX_KDF2.p) {
27469
+ throw malformed2("its KDF parameters are beyond what this client runs");
24806
27470
  }
24807
27471
  return {
24808
27472
  v: raw.v,
@@ -24839,69 +27503,7 @@ async function openKeyBundle(text, passphrase) {
24839
27503
  return assertContent(JSON.parse(utf8Decode(plaintext)));
24840
27504
  }
24841
27505
 
24842
- // src/transfer/console.ts
24843
- class TransferConsole {
24844
- reader;
24845
- transfer;
24846
- names;
24847
- constructor(reader, transfer, names) {
24848
- this.reader = reader;
24849
- this.transfer = transfer;
24850
- this.names = names;
24851
- }
24852
- async export(path) {
24853
- await this.transfer.exportable();
24854
- const keys = await this.transfer.export(path, await this.readNewPassphrase());
24855
- this.reader.write(`Exported ${keys.length} key(s) to ${path}:
24856
- `);
24857
- await this.writeRoots(keys);
24858
- this.reader.write(`The file opens only with the passphrase you typed: keep both, and keep them apart.
24859
- ` + `A seal root that is opened by a passphrase has no key on this machine and is not in the file.
24860
- `);
24861
- }
24862
- async import(path) {
24863
- const passphrase = await this.reader.read("Passphrase of the export file (not shown): ", { hidden: true });
24864
- const { imported, store, present } = await this.transfer.import(path, passphrase);
24865
- if (store !== null) {
24866
- this.reader.write(`Imported ${imported.length} key(s) into ${KEY_STORE_LABELS[store]}:
24867
- `);
24868
- await this.writeRoots(imported);
24869
- }
24870
- if (present.length > 0) {
24871
- this.reader.write(`${present.length} key(s) were already on this machine and were left as they are:
24872
- `);
24873
- await this.writeRoots(present);
24874
- }
24875
- }
24876
- async writeRoots(keys) {
24877
- for (const { root_id } of keys) {
24878
- this.reader.write(` ${printable2(await this.names.describe(root_id))}
24879
- `);
24880
- }
24881
- }
24882
- async readNewPassphrase() {
24883
- for (;; ) {
24884
- const first = await this.reader.read(`Passphrase for the export file (not shown; ${RECOMMENDED_EXPORT_PASSPHRASE_LENGTH} or more characters recommended): `, { hidden: true });
24885
- if (first.length === 0) {
24886
- this.reader.write(`Nothing entered.
24887
- `);
24888
- continue;
24889
- }
24890
- if (first.length < RECOMMENDED_EXPORT_PASSPHRASE_LENGTH) {
24891
- this.reader.write(`Note: shorter than the recommended ${RECOMMENDED_EXPORT_PASSPHRASE_LENGTH} characters; every key in the file is only as safe as this passphrase.
24892
- `);
24893
- }
24894
- const second = await this.reader.read("Repeat it: ", { hidden: true });
24895
- if (first === second)
24896
- return first;
24897
- this.reader.write(`The two entries differ.
24898
- `);
24899
- }
24900
- }
24901
- }
24902
-
24903
27506
  // src/transfer/keyTransfer.ts
24904
- import { readFile } from "node:fs/promises";
24905
27507
  function idOf(file2) {
24906
27508
  return `${file2.tenant_id}/${file2.root_id}/${file2.kid}`;
24907
27509
  }
@@ -25041,6 +27643,50 @@ async function transferKeys(options, reader, systemKeyring = new OsKeyring) {
25041
27643
  const terminal = new TransferConsole(reader, new KeyTransfer(await openKeyStoresOnly(options, tenant, systemKeyring), names), names);
25042
27644
  await (options.mode === "export-keys" ? terminal.export(transferFile) : terminal.import(transferFile));
25043
27645
  }
27646
+ var WHILE_IT_RUNS = Number.POSITIVE_INFINITY;
27647
+ async function runBackup(options, reader, systemKeyring = new OsKeyring) {
27648
+ const file2 = parsed(options.backupFile);
27649
+ const config2 = await loadTokenConfig(options.configPath);
27650
+ const url2 = apiUrlFor(options, config2);
27651
+ const http = new HttpClient(url2, config2.token);
27652
+ const tenant = config2.tenant_id;
27653
+ const sealed = {
27654
+ api: new SealedApi(http),
27655
+ session: new SealedSession(WHILE_IT_RUNS),
27656
+ tenant,
27657
+ stores: options.mode === "export" ? await openStores(options, tenant, systemKeyring, await storesOf(options, systemKeyring, toStderr)) : await openKeyStoresOnly(options, tenant, systemKeyring),
27658
+ names: namesOf(options)
27659
+ };
27660
+ const parts = {
27661
+ kb: new KbApi(http),
27662
+ sealed,
27663
+ services: createSealedServices(sealed)
27664
+ };
27665
+ const terminal = new BackupConsole(reader);
27666
+ try {
27667
+ if (options.mode === "export") {
27668
+ const exporter = new Exporter(parts, {
27669
+ selection: {
27670
+ spaces: options.exportSpaceIds.length === 0 ? null : options.exportSpaceIds,
27671
+ documents: options.exportDocumentIds
27672
+ },
27673
+ skipLocked: options.skipLocked,
27674
+ source: { url: url2, tenant_id: tenant },
27675
+ clientVersion: VERSION
27676
+ }, terminal.progress);
27677
+ await terminal.export(exporter, file2, options.compress ? "gzip" : "none");
27678
+ return;
27679
+ }
27680
+ const importer = new Importer(parts, {
27681
+ tenantId: tenant,
27682
+ intoSpaceId: options.intoSpaceId,
27683
+ store: options.store ?? "default"
27684
+ }, terminal.progress);
27685
+ await terminal.import(importer, file2);
27686
+ } finally {
27687
+ sealed.session.lockAll();
27688
+ }
27689
+ }
25044
27690
  async function atTerminal(options, typed2, run, whenLeft = () => "was aborted") {
25045
27691
  if (!process.stdin.isTTY) {
25046
27692
  throw new StartupError(`--${options.mode} needs a terminal: ${typed2} is typed into it`);
@@ -25134,12 +27780,12 @@ async function serve(options, transport, systemKeyring = new OsKeyring, onboardi
25134
27780
  }) {
25135
27781
  const session = new SealedSession(options.idleTtlMs);
25136
27782
  let hosted = null;
25137
- const open4 = async (config2, chosen) => {
27783
+ const open5 = async (config2, chosen) => {
25138
27784
  const opened = await openTools(options, config2, session, systemKeyring, chosen);
25139
27785
  hosted = opened.hosted;
25140
27786
  return opened;
25141
27787
  };
25142
- const tools = existsSync4(options.configPath) ? await authorizedServer(options, open4, systemKeyring, onboarding) : await unauthorizedServer(options, open4, systemKeyring, onboarding);
27788
+ const tools = existsSync5(options.configPath) ? await authorizedServer(options, open5, systemKeyring, onboarding) : await unauthorizedServer(options, open5, systemKeyring, onboarding);
25143
27789
  const sweep = setInterval(() => session.expireIdle(), IDLE_SWEEP_MS);
25144
27790
  sweep.unref();
25145
27791
  tools.server.onclose = () => {
@@ -25155,16 +27801,16 @@ async function serve(options, transport, systemKeyring = new OsKeyring, onboardi
25155
27801
  }
25156
27802
  };
25157
27803
  }
25158
- async function authorizedServer(options, open4, systemKeyring, onboarding) {
27804
+ async function authorizedServer(options, open5, systemKeyring, onboarding) {
25159
27805
  const config2 = await loadTokenConfig(options.configPath);
25160
27806
  const chosen = await storesOf(options, systemKeyring, onboarding.log);
25161
27807
  const parts = {
25162
- ...await open4(config2, chosen),
27808
+ ...await open5(config2, chosen),
25163
27809
  localInstructions: SEALED_INSTRUCTIONS
25164
27810
  };
25165
27811
  return new ToolServer(VERSION, instructionsOf(parts), await toolSetOf(parts));
25166
27812
  }
25167
- async function unauthorizedServer(options, open4, systemKeyring, onboarding) {
27813
+ async function unauthorizedServer(options, open5, systemKeyring, onboarding) {
25168
27814
  const chosen = await storesOf(options, systemKeyring, onboarding.log);
25169
27815
  let server = null;
25170
27816
  const connection = new Connection({
@@ -25180,7 +27826,7 @@ async function unauthorizedServer(options, open4, systemKeyring, onboarding) {
25180
27826
  onInvited
25181
27827
  }),
25182
27828
  connected: async ({ config: config2 }) => {
25183
- const opened = await open4(config2, chosen);
27829
+ const opened = await open5(config2, chosen);
25184
27830
  try {
25185
27831
  await server?.replaceTools(await toolSetOf({
25186
27832
  localTools: [connection.tool(), ...opened.localTools],
@@ -25221,7 +27867,7 @@ function registeredArgs(options) {
25221
27867
  async function setUp(options, parts) {
25222
27868
  const { log } = parts;
25223
27869
  const chosen = await storesOf(options, parts.systemKeyring, log);
25224
- if (existsSync4(options.configPath)) {
27870
+ if (existsSync5(options.configPath)) {
25225
27871
  const config2 = await loadTokenConfig(options.configPath);
25226
27872
  apiUrlFor(options, config2);
25227
27873
  log(`This computer is already authorized for ${config2.url} (${options.configPath}).`);
@@ -25271,6 +27917,11 @@ async function main(argv, env) {
25271
27917
  await atTerminal(options, "the passphrase of the export file", (reader) => transferKeys(options, reader));
25272
27918
  return;
25273
27919
  }
27920
+ if (options.mode === "export" || options.mode === "import") {
27921
+ const importing = options.mode === "import";
27922
+ await atTerminal(options, "the passphrase of the export file", (reader) => runBackup(options, reader), () => importing ? "was interrupted: what was imported so far stays in the account" : "was interrupted: the export file was not completed");
27923
+ return;
27924
+ }
25274
27925
  if (options.mode === "delete-key") {
25275
27926
  let removing = false;
25276
27927
  await atTerminal(options, "the name or the id of the seal root", (reader) => deleteKey(options, reader, new OsKeyring, () => {