@brftech/filex-core 0.30.1 → 0.32.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 (38) hide show
  1. package/README.md +37 -2
  2. package/dist/filex-core.js +10571 -8278
  3. package/dist/filex-core.js.map +1 -1
  4. package/dist/filex-core.umd.cjs +95 -87
  5. package/dist/filex-core.umd.cjs.map +1 -1
  6. package/dist/index.d.ts +548 -66
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/FileExplorer.vue +1104 -26
  10. package/src/components/Breadcrumb.vue +4 -2
  11. package/src/components/CommandPalette.vue +18 -2
  12. package/src/components/E2eRecoveryUnlockModal.vue +193 -0
  13. package/src/components/EncryptedFolderModal.vue +10 -0
  14. package/src/components/FilterBar.vue +244 -0
  15. package/src/components/GalleryView.vue +48 -0
  16. package/src/components/GridView.vue +85 -3
  17. package/src/components/InspectorPanel.vue +231 -8
  18. package/src/components/ListView.vue +11 -1
  19. package/src/components/RecoveryKeyModal.vue +133 -0
  20. package/src/components/SecondaryPane.vue +15 -1
  21. package/src/components/SideNav.vue +329 -6
  22. package/src/components/StarButton.vue +27 -15
  23. package/src/components/Toolbar.vue +235 -49
  24. package/src/components/ViewSwitcher.vue +81 -0
  25. package/src/composables/useFileApi.ts +83 -0
  26. package/src/composables/useKeyboardShortcuts.ts +7 -0
  27. package/src/index.ts +33 -1
  28. package/src/lib/e2ecrypto.ts +716 -70
  29. package/src/lib/fileFilters.ts +143 -0
  30. package/src/lib/listing.ts +103 -1
  31. package/src/lib/star.ts +42 -0
  32. package/src/lib/tags.ts +105 -0
  33. package/src/locales/en.ts +154 -2
  34. package/src/locales/tr.ts +154 -2
  35. package/src/modals/PermissionsModal.vue +18 -2
  36. package/src/styles/base.css +743 -0
  37. package/src/types/ExplorerConfig.ts +53 -1
  38. package/src/types/FileNode.ts +23 -0
package/dist/index.d.ts CHANGED
@@ -54,6 +54,13 @@ declare type __VLS_Props_11 = {
54
54
  /** ui-fix — mirror the main panel's virtual `.trash` row at storage root
55
55
  * so both split panes list identical rows (no row-offset). Defaults on. */
56
56
  trashVisible?: boolean;
57
+ /**
58
+ * The navigation panel is already offering Trash, so neither pane draws the
59
+ * virtual row. Passed down rather than worked out here: the panel belongs to
60
+ * the host explorer, and a pane that guessed at it is how the two halves of a
61
+ * split end up listing different rows.
62
+ */
63
+ navOffersTrash?: boolean;
57
64
  };
58
65
 
59
66
  declare type __VLS_Props_12 = {
@@ -61,9 +68,46 @@ declare type __VLS_Props_12 = {
61
68
  locale: LocaleCode;
62
69
  /** True while the parent is creating the folder + uploading the marker. */
63
70
  busy?: boolean;
71
+ /** Short id of this installation's E2E escrow key, when one is configured.
72
+ * Shown BEFORE the folder is created: escrow means the operator can open
73
+ * it without the password, and that is not a detail to discover later. */
74
+ escrowKid?: string | null;
64
75
  };
65
76
 
66
77
  declare type __VLS_Props_13 = {
78
+ open: boolean;
79
+ locale: LocaleCode;
80
+ /** The key itself. Shown, copied, downloaded — never emitted or stored. */
81
+ recoveryKey: string;
82
+ /** Folder name, for the downloaded file and the dialog copy. */
83
+ folderName?: string;
84
+ /** Set when this installation holds an escrow key for the folder too. */
85
+ escrowKid?: string | null;
86
+ /** 'created' = a new folder; 'upgraded' = an existing folder gained recovery. */
87
+ variant?: 'created' | 'upgraded';
88
+ };
89
+
90
+ declare type __VLS_Props_14 = {
91
+ open: boolean;
92
+ locale: LocaleCode;
93
+ /** The folder has a user recovery key slot (v2 markers created since 0.31). */
94
+ hasRecovery: boolean;
95
+ /**
96
+ * Whether the escrow door applies to THIS folder — see escrowAvailability.
97
+ * 'predates' and 'other-key' are not "no escrow tab"; they are two
98
+ * different facts the dialog has to state, because an admin who knows the
99
+ * installation has escrow reads a missing tab as a bug and tries the key
100
+ * anyway.
101
+ */
102
+ escrowState: EscrowAvailability;
103
+ /** Short id of the escrow key THIS FOLDER was sealed to, when it has one. */
104
+ escrowKid?: string | null;
105
+ busy?: boolean;
106
+ /** Set by the parent after a failed attempt. */
107
+ error?: string | null;
108
+ };
109
+
110
+ declare type __VLS_Props_15 = {
67
111
  config: ExplorerConfig;
68
112
  /** Which half to open on. */
69
113
  initialTab?: 'storages' | 'connect';
@@ -72,7 +116,7 @@ declare type __VLS_Props_13 = {
72
116
  closable?: boolean;
73
117
  };
74
118
 
75
- declare type __VLS_Props_14 = {
119
+ declare type __VLS_Props_16 = {
76
120
  fields: StorageField[];
77
121
  modelValue: Record<string, unknown>;
78
122
  locale: LocaleCode;
@@ -81,18 +125,18 @@ declare type __VLS_Props_14 = {
81
125
  disabled?: boolean;
82
126
  };
83
127
 
84
- declare type __VLS_Props_15 = {
128
+ declare type __VLS_Props_17 = {
85
129
  guide: ProtocolGuide;
86
130
  locale: LocaleCode;
87
131
  };
88
132
 
89
- declare type __VLS_Props_16 = {
133
+ declare type __VLS_Props_18 = {
90
134
  config: ExplorerConfig;
91
135
  /** Storage names the caller may see, for the confinement picker. */
92
136
  storages: string[];
93
137
  };
94
138
 
95
- declare type __VLS_Props_17 = {
139
+ declare type __VLS_Props_19 = {
96
140
  config: ExplorerConfig;
97
141
  /**
98
142
  * Draw the key list and the paste box.
@@ -106,27 +150,6 @@ declare type __VLS_Props_17 = {
106
150
  keysVisible?: boolean;
107
151
  };
108
152
 
109
- declare type __VLS_Props_18 = {
110
- config: ExplorerConfig;
111
- /** Storage names the caller may see, for the confinement picker. */
112
- storages: string[];
113
- };
114
-
115
- declare type __VLS_Props_19 = {
116
- config: ExplorerConfig;
117
- /**
118
- * Render the full self-service key manager (scopes, folder confinement,
119
- * expiry) instead of the one-field minter the guides embed.
120
- */
121
- full?: boolean;
122
- /**
123
- * Which protocol the surrounding guide is showing. It only changes the
124
- * default label — a token minted here works on all of them, and pretending
125
- * otherwise would have people mint one per protocol.
126
- */
127
- protocol?: string;
128
- };
129
-
130
153
  declare type __VLS_Props_2 = {
131
154
  open: boolean;
132
155
  locale: LocaleCode;
@@ -161,6 +184,27 @@ declare type __VLS_Props_2 = {
161
184
  theme?: 'light' | 'dark' | 'auto';
162
185
  };
163
186
 
187
+ declare type __VLS_Props_20 = {
188
+ config: ExplorerConfig;
189
+ /** Storage names the caller may see, for the confinement picker. */
190
+ storages: string[];
191
+ };
192
+
193
+ declare type __VLS_Props_21 = {
194
+ config: ExplorerConfig;
195
+ /**
196
+ * Render the full self-service key manager (scopes, folder confinement,
197
+ * expiry) instead of the one-field minter the guides embed.
198
+ */
199
+ full?: boolean;
200
+ /**
201
+ * Which protocol the surrounding guide is showing. It only changes the
202
+ * default label — a token minted here works on all of them, and pretending
203
+ * otherwise would have people mint one per protocol.
204
+ */
205
+ protocol?: string;
206
+ };
207
+
164
208
  declare type __VLS_Props_3 = {
165
209
  starred: boolean;
166
210
  nodeId: number;
@@ -174,6 +218,17 @@ declare type __VLS_Props_3 = {
174
218
  authCredentials?: RequestCredentials;
175
219
  /** Compact mode for grid view (no label, just the icon). */
176
220
  compact?: boolean;
221
+ /**
222
+ * Card mode — the same button sitting ON a grid/gallery tile instead of in
223
+ * a list cell: a round translucent chip in the tile's corner. It is the SAME
224
+ * component, deliberately: a card star written separately is a second
225
+ * starring path, and the two drift the first time one of them is fixed.
226
+ */
227
+ card?: boolean;
228
+ /** Locale for the title/label. ⚠ The strings used to be hardcoded English
229
+ * ("Star"/"Unstar"), which is a Turkish user's only untranslated control in
230
+ * the row. */
231
+ locale?: LocaleCode;
177
232
  };
178
233
 
179
234
  declare type __VLS_Props_4 = {
@@ -245,6 +300,40 @@ declare type __VLS_Props_9 = {
245
300
  narrow?: boolean;
246
301
  };
247
302
 
303
+ /**
304
+ * Give an EXISTING v2 folder an escrow slot, in place, using the folder
305
+ * password its owner has just typed.
306
+ *
307
+ * ── Why this exists ─────────────────────────────────────────────────
308
+ *
309
+ * Escrow used to be all-or-nothing at install time, and then adoptable but
310
+ * never retroactive: on any installation that had been running for a while,
311
+ * escrow covered only folders nobody had created yet. On a real deployment
312
+ * the folders that matter already exist, so "new folders only" means escrow
313
+ * covers nothing anyone cares about.
314
+ *
315
+ * The server still cannot do this, and that has not changed: adding a slot
316
+ * needs the folder master key, which needs a credential the server has never
317
+ * held. What CAN do it is the browser, at the one moment the password is in
318
+ * memory — exactly where `upgradeMarkerV1` already lives. Same moment, same
319
+ * shape, different slot.
320
+ *
321
+ * ⚠⚠ Accepting hands the operator of this installation a second, permanent
322
+ * way into this folder. It is the folder's owner who decides, from inside,
323
+ * with the password; no configuration change and no admin action can do it
324
+ * for them. The caller MUST say that in those words before calling this —
325
+ * see `e2e.escrowoffer.*` in the locales.
326
+ *
327
+ * ⚠ v2 only. A v1 marker has no slots at all; the path for those is
328
+ * `upgradeMarkerV1`, which already seals an escrow slot when the
329
+ * installation has a key and already discloses it. Two doors into the same
330
+ * room would be two chances to get the disclosure wrong.
331
+ *
332
+ * ⚠ No file is re-encrypted, moved or rewritten. Only `.filex-e2e.json`
333
+ * changes, and only by gaining `esc` (and losing `esc_declined`).
334
+ */
335
+ export declare function addEscrowSlot(marker: E2eMarker, password: string, escrowPublicKey: string): Promise<E2eMarker>;
336
+
248
337
  /**
249
338
  * The self-service API-token surface (`/api/tokens`).
250
339
  *
@@ -399,11 +488,33 @@ export declare interface Capabilities {
399
488
  /** Longest life a new share link may be given, in days (0 = no ceiling).
400
489
  * Read by the share dialogs so they offer only expiries the server keeps. */
401
490
  share_max_ttl_days?: number;
491
+ /** Is the caller a person (`'user'` — a session OR their own API token) or an
492
+ * integration (`'app'` — a host proxy, a bot, an MCP client)? The explorer
493
+ * reads it to decide whether to draw the identity-bearing surfaces; see
494
+ * ExplorerConfig.callerKind. Absent on a server older than the app/user
495
+ * token split, which is why every reader treats "missing" as a person. */
496
+ caller_kind?: 'user' | 'app';
402
497
  external?: {
403
498
  onlyoffice?: ExternalServiceStatus;
404
499
  drawio?: ExternalServiceStatus;
405
500
  mermaid?: ExternalServiceStatus;
406
501
  };
502
+ /** Whether this installation holds an escrow key for E2E-encrypted folders,
503
+ * and the public half the browser wraps new folders' master keys to.
504
+ *
505
+ * Published on purpose. Escrow means the operator can open the folders you
506
+ * create here without your password, and someone about to create one is
507
+ * entitled to know that BEFORE they create it. Fixed at install time
508
+ * (FILEX_INSTALLATION_E2E_ESCROW_KEY), so this answer never changes for a
509
+ * running installation. */
510
+ e2e_escrow?: {
511
+ enabled: boolean;
512
+ /** Short id of the escrow key (SHA-256(SPKI)[:8], hex). */
513
+ kid?: string;
514
+ alg?: string;
515
+ /** Base64 SPKI. Public material — it can only seal, never open. */
516
+ public_key?: string;
517
+ };
407
518
  }
408
519
 
409
520
  export declare function clearResume(store: ResumeStorage | null, key: string): void;
@@ -421,7 +532,7 @@ export declare interface ClipboardState {
421
532
  */
422
533
  export declare function comboFromEvent(e: KeyboardEvent): string | null;
423
534
 
424
- export declare const ConnectionGuideView: DefineComponent<__VLS_Props_15, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<__VLS_Props_15> & Readonly<{}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, HTMLDivElement>;
535
+ export declare const ConnectionGuideView: DefineComponent<__VLS_Props_17, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<__VLS_Props_17> & Readonly<{}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, HTMLDivElement>;
425
536
 
426
537
  export declare type ConnectionsApi = ReturnType<typeof useConnections>;
427
538
 
@@ -446,13 +557,13 @@ export declare function connectionsBase(config: ExplorerConfig): string;
446
557
  */
447
558
  export declare function connectionsOrigin(config: ExplorerConfig): string;
448
559
 
449
- export declare const ConnectionsPanel: DefineComponent<__VLS_Props_13, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
560
+ export declare const ConnectionsPanel: DefineComponent<__VLS_Props_15, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
450
561
  error: (err: {
451
562
  message: string;
452
563
  }) => any;
453
564
  close: () => any;
454
565
  changed: () => any;
455
- }, string, PublicProps, Readonly<__VLS_Props_13> & Readonly<{
566
+ }, string, PublicProps, Readonly<__VLS_Props_15> & Readonly<{
456
567
  onError?: ((err: {
457
568
  message: string;
458
569
  }) => any) | undefined;
@@ -478,31 +589,72 @@ declare interface ContextAction {
478
589
  divider?: boolean;
479
590
  }
480
591
 
592
+ export declare interface CreatedFolder {
593
+ marker: E2eMarker;
594
+ /** The folder master key, ready for encryptFile/decryptFile. */
595
+ fmk: CryptoKey;
596
+ /** Show this ONCE. filex never stores it and can never show it again. */
597
+ recoveryKey: string;
598
+ }
599
+
481
600
  /**
482
- * Tiny per-explorer key ring: encrypted-folder root (wire path) KEK.
483
- * Lives ONLY in memory "Kilitle" drops the entry, a reload drops all.
601
+ * Create a v2 encrypted folder: random FMK, wrapped under the password KEK,
602
+ * under a freshly minted user recovery key, and when the installation has
603
+ * escrow enabled — to the escrow public key.
604
+ */
605
+ export declare function createEncryptedFolder(password: string, opts?: CreateFolderOptions): Promise<CreatedFolder>;
606
+
607
+ export declare interface CreateFolderOptions {
608
+ iterations?: number;
609
+ /** Base64 SPKI of the installation escrow key, when escrow is enabled. */
610
+ escrowPublicKey?: string | null;
611
+ }
612
+
613
+ /**
614
+ * Tiny per-explorer key ring: encrypted-folder root (wire path) → FMK.
615
+ * Lives ONLY in memory — "Lock" drops the entry, a reload drops all.
484
616
  */
485
617
  export declare function createKeyRing(): {
486
618
  get(root: string): CryptoKey | undefined;
487
- set(root: string, kek: CryptoKey): void;
488
- /** Drop one folder's key ("Kilitle"). */
619
+ set(root: string, fmk: CryptoKey): void;
620
+ /** Drop one folder's key ("Lock"). */
489
621
  lock(root: string): void;
490
622
  has(root: string): boolean;
491
623
  clear(): void;
492
624
  };
493
625
 
494
- /** Create a fresh folder marker for `password` (also returns the derived KEK). */
626
+ /**
627
+ * Create a v1 folder marker — the pre-0.31 format, with NO recovery of any
628
+ * kind.
629
+ *
630
+ * @deprecated Use `createEncryptedFolder`. Kept exported, and kept producing
631
+ * a genuine v1 marker, so an embedder pinned to the old API keeps creating
632
+ * folders this build can still open rather than half-formed v2 ones.
633
+ */
495
634
  export declare function createMarker(password: string, iterations?: number): Promise<{
496
635
  marker: E2eMarker;
497
636
  kek: CryptoKey;
498
637
  }>;
499
638
 
500
639
  /**
501
- * Decrypt a 'filexe2e' blob with the folder KEK. Throws E2eDecryptError on
502
- * a wrong key / tampered data, and a plain Error when the header is not an
503
- * e2e file at all.
640
+ * Record that this folder's owner was offered an escrow slot and declined.
641
+ *
642
+ * A refusal is a decision, not a delay: without this the offer would come
643
+ * back on every single unlock, which is how people learn to click past
644
+ * security dialogs without reading them. Nothing about the folder's keys
645
+ * changes — the only effect is that filex stops asking.
646
+ *
647
+ * Reversible by `addEscrowSlot`, which is the way back for somebody who
648
+ * says no today and changes their mind next month.
649
+ */
650
+ export declare function declineEscrowSlot(marker: E2eMarker, when: string): E2eMarker;
651
+
652
+ /**
653
+ * Decrypt a 'filexe2e' blob with the folder master key. Throws
654
+ * E2eDecryptError on a wrong key / tampered data, and a plain Error when the
655
+ * header is not an e2e file at all.
504
656
  */
505
- export declare function decryptFile(kek: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer>;
657
+ export declare function decryptFile(fmk: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer>;
506
658
 
507
659
  export declare const DEFAULT_THEME_ID = "default";
508
660
 
@@ -522,6 +674,9 @@ export declare function deriveKek(password: string, salt: Uint8Array, iterations
522
674
 
523
675
  export declare const E2E_DEFAULT_ITERATIONS = 600000;
524
676
 
677
+ /** The only escrow algorithm this version understands. */
678
+ export declare const E2E_ESCROW_ALG = "RSA-OAEP-256";
679
+
525
680
  export declare const E2E_MAGIC = "filexe2e";
526
681
 
527
682
  /**
@@ -529,33 +684,80 @@ export declare const E2E_MAGIC = "filexe2e";
529
684
  *
530
685
  * WebCrypto ONLY — zero dependencies. Design doc: docs/E2E-ENCRYPTION.md.
531
686
  *
532
- * Scheme (v1):
533
- * folder password ─PBKDF2-SHA256(600k iter, per-folder 16B salt)─▶ KEK (AES-256-GCM)
534
- * per-file random 32B DEK (AES-256-GCM) encrypts the content one-shot;
535
- * the DEK is wrapped with the KEK and stored in the file's own header.
687
+ * ── Scheme ────────────────────────────────────────────────────────────
688
+ *
689
+ * Every encrypted file wraps its own random DEK under ONE key, the folder
690
+ * master key (FMK), and stores the wrapped copy in its own 97-byte header.
691
+ * The FMK is what a "key slot" in the folder marker hands back:
692
+ *
693
+ * password ─PBKDF2-SHA256(600k, 16B salt)─▶ KEK ─┐
694
+ * recovery key ─HKDF-SHA256(16B salt)─▶ RKEK ────┼─▶ unwraps the FMK
695
+ * escrow private key ─RSA-OAEP-256───────────────┘
696
+ * │
697
+ * per-file random 32B DEK ◀── AES-GCM-wrapped by the FMK, in the header
698
+ *
699
+ * Adding a recovery path therefore costs one more wrapped copy of a single
700
+ * 32-byte key in the marker — not a re-encrypt of anything. The file format
701
+ * below is UNCHANGED from v1 and stays that way; only the marker grew.
702
+ *
703
+ * ── Marker versions ───────────────────────────────────────────────────
536
704
  *
537
- * File layout ('filexe2e' magic, fixed 97-byte header):
705
+ * v1 (shipped up to 0.30.1) has no slots: the DEK is wrapped directly by
706
+ * the password KEK. Read that as "the FMK *is* the KEK". Such folders keep
707
+ * opening with nothing but their password, forever — the v1 read path is a
708
+ * first-class path here, not a migration shim.
709
+ *
710
+ * v2 adds the slots. It comes in two flavours, told apart by `fmk`:
711
+ * - `fmk: 'wrapped'` — a fresh random FMK, held in `fmk_pw` wrapped under
712
+ * the password KEK. Every folder created from 0.31 on.
713
+ * - `fmk: 'kek'` — a v1 folder that was given recovery keys in place.
714
+ * Its files were already wrapped under the KEK and are not rewritten, so
715
+ * the FMK stays defined as "the password-derived KEK" and the recovery
716
+ * slots wrap those raw 32 bytes. The password path is byte-identical to
717
+ * v1; only the extra slots are new.
718
+ *
719
+ * ── Invariants ────────────────────────────────────────────────────────
720
+ *
721
+ * - No key, password or recovery key is ever stored, logged or sent to a
722
+ * server. The FMK lives in an in-memory key ring and dies with the tab.
723
+ * - `deriveKek` imports non-extractable. Raw KEK bytes are produced ONLY
724
+ * by `deriveKekBits`, only for a marker whose FMK *is* the KEK
725
+ * (`upgradeMarkerV1`, and `addEscrowSlot` on a folder it produced), and
726
+ * only long enough to wrap them into a slot.
727
+ * - A folder created while escrow was off carries no escrow slot, so the
728
+ * escrow key cannot open it, and nothing the OPERATOR does changes
729
+ * that — not enabling escrow, not adopting it, not any admin action or
730
+ * future version. That is arithmetic, not policy: adding a slot needs
731
+ * the folder master key, and the server has never held a credential
732
+ * that produces one.
733
+ * - The folder's OWNER can, from inside, with the password:
734
+ * `addEscrowSlot`. That is the only door, it opens from one side only,
735
+ * and it is the reason `escrowAvailability` says "not as things stand"
736
+ * rather than "never".
737
+ *
738
+ * File layout ('filexe2e' magic, fixed 97-byte header) — UNCHANGED in v2:
538
739
  * [0..8) magic "filexe2e"
539
740
  * [8] version 0x01
540
741
  * [9..21) wrapIV (12B) — GCM IV of the DEK wrap
541
- * [21..69) wrappedDEK (48B = 32B DEK + 16B GCM tag)
742
+ * [21..69) wrappedDEK (48B = 32B DEK + 16B GCM tag), wrapped by the FMK
542
743
  * [69..81) dataIV (12B) — GCM IV of the content
543
744
  * [81..97) reserved (zeros; v2 chunking/metadata)
544
745
  * [97..) ciphertext (content + 16B GCM tag)
545
- *
546
- * Folder marker `.filex-e2e.json` at the encrypted-folder root:
547
- * { v:1, salt:<b64 16B>, iter:600000, verify:<b64 12B IV || GCM('filex-e2e-verify-v1')> }
548
- *
549
- * The KEK NEVER leaves memory — no storage of any kind. Password loss is
550
- * data loss by design (no recovery path exists anywhere).
551
746
  */
552
747
  export declare const E2E_MARKER_NAME = ".filex-e2e.json";
553
748
 
749
+ /** Marker schema version written by this build. v1 markers still read. */
750
+ export declare const E2E_MARKER_VERSION = 2;
751
+
554
752
  /** MVP single-shot in-memory ceiling — larger uploads are refused with a warning. */
555
753
  export declare const E2E_MAX_FILE_BYTES: number;
556
754
 
557
755
  export declare const E2E_MIN_PASSWORD_LEN = 8;
558
756
 
757
+ /** Entropy of a user recovery key: 20 bytes = 160 bits = exactly 32 base32 chars. */
758
+ export declare const E2E_RECOVERY_KEY_BYTES = 20;
759
+
760
+ /** File-header version byte. Unchanged by the recovery work. */
559
761
  export declare const E2E_VERSION = 1;
560
762
 
561
763
  /** Thrown on wrong password / corrupted ciphertext (GCM tag mismatch). */
@@ -563,6 +765,17 @@ export declare class E2eDecryptError extends Error {
563
765
  constructor(msg?: string);
564
766
  }
565
767
 
768
+ /** Escrow slot: the FMK encrypted to the installation's escrow public key. */
769
+ export declare interface E2eEscrowSlot {
770
+ /** First 8 bytes of SHA-256(SPKI), hex — names WHICH escrow key this is. */
771
+ kid: string;
772
+ alg: string;
773
+ blob: string;
774
+ }
775
+
776
+ /** How the folder master key is obtained from the password slot. */
777
+ export declare type E2eFmkMode = 'kek' | 'wrapped';
778
+
566
779
  export declare type E2eKeyRing = ReturnType<typeof createKeyRing>;
567
780
 
568
781
  export declare interface E2eMarker {
@@ -570,8 +783,53 @@ export declare interface E2eMarker {
570
783
  salt: string;
571
784
  iter: number;
572
785
  verify: string;
786
+ /** v2 only. Absent on a v1 marker, where the FMK is implicitly the KEK. */
787
+ fmk?: E2eFmkMode;
788
+ /** v2 + fmk==='wrapped' only: base64 12B IV || AES-GCM(KEK, FMK). */
789
+ fmk_pw?: string;
790
+ /** v2 only, optional: the user recovery key slot. */
791
+ rk?: E2eRecoverySlot;
792
+ /** v2 only, optional: the operator escrow slot. */
793
+ esc?: E2eEscrowSlot;
794
+ /**
795
+ * v2 only, optional: an ISO timestamp recording that this folder's owner
796
+ * was OFFERED an escrow slot and said no.
797
+ *
798
+ * It lives in the marker rather than in browser storage because the unit
799
+ * of the decision is the FOLDER, not the device: the same person opening
800
+ * the folder from their phone must not be asked again, and a decision
801
+ * that vanished when someone cleared their site data would be no decision
802
+ * at all. It travels with the folder through a move, a backup and a
803
+ * restore, for the same reason the key slots do.
804
+ *
805
+ * It holds no key material and hides nothing from the operator — it is a
806
+ * record of an answer, and its only effect is that filex stops asking.
807
+ * `addEscrowSlot` clears it, so a decline is reversible by the one person
808
+ * who can reverse it.
809
+ */
810
+ esc_declined?: string;
811
+ }
812
+
813
+ /** User-recovery-key slot: HKDF salt + the FMK wrapped under the derived key. */
814
+ export declare interface E2eRecoverySlot {
815
+ salt: string;
816
+ blob: string;
573
817
  }
574
818
 
819
+ export declare const E2eRecoveryUnlockModal: DefineComponent<__VLS_Props_14, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
820
+ close: () => any;
821
+ submit: (payload: {
822
+ mode: "recovery" | "escrow";
823
+ value: string;
824
+ }) => any;
825
+ }, string, PublicProps, Readonly<__VLS_Props_14> & Readonly<{
826
+ onClose?: (() => any) | undefined;
827
+ onSubmit?: ((payload: {
828
+ mode: "recovery" | "escrow";
829
+ value: string;
830
+ }) => any) | undefined;
831
+ }>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
832
+
575
833
  /** Current combo for an action ('' = unbound). */
576
834
  export declare function effectiveCombo(id: string): string;
577
835
 
@@ -593,11 +851,14 @@ password: string;
593
851
  }>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
594
852
 
595
853
  /**
596
- * Encrypt `content` under the folder KEK: mints a fresh DEK, encrypts the
597
- * content one-shot, wraps the DEK with the KEK and prepends the fixed
854
+ * Encrypt `content` under the folder master key: mints a fresh DEK, encrypts
855
+ * the content one-shot, wraps the DEK with the FMK and prepends the fixed
598
856
  * 'filexe2e' header. Throws when content exceeds E2E_MAX_FILE_BYTES.
857
+ *
858
+ * `fmk` is the key an unlock returned. On a v1 folder that is the password
859
+ * KEK, which is why v1 files keep working untouched.
599
860
  */
600
- export declare function encryptFile(kek: CryptoKey, content: ArrayBuffer): Promise<ArrayBuffer>;
861
+ export declare function encryptFile(fmk: CryptoKey, content: ArrayBuffer): Promise<ArrayBuffer>;
601
862
 
602
863
  /**
603
864
  * Resolved endpoint map. `useFileApi` derives this once on construction
@@ -635,6 +896,8 @@ export declare interface EndpointMap {
635
896
  restore: string | null;
636
897
  trashList: string | null;
637
898
  trashRestore: string | null;
899
+ e2eEscrowChallenge: string | null;
900
+ e2eEscrowUsed: string | null;
638
901
  }
639
902
 
640
903
  export declare function ensureHighlight(): Promise<unknown | null>;
@@ -646,6 +909,69 @@ export declare function ensureHighlight(): Promise<unknown | null>;
646
909
  */
647
910
  export declare function ensureMonaco(): Promise<unknown | null>;
648
911
 
912
+ /**
913
+ * Why the escrow door is, or is not, on offer for this folder.
914
+ *
915
+ * 'off' this installation has no escrow key at all.
916
+ * 'available' the folder is sealed to THIS installation's escrow key.
917
+ * 'predates' the installation has an escrow key, and this folder has no
918
+ * escrow slot: it was created before escrow existed here.
919
+ * 'other-key' the folder carries an escrow slot sealed to a DIFFERENT
920
+ * key id — it came from another installation, via a restore
921
+ * or a copied data directory.
922
+ *
923
+ * ⚠ 'predates' exists because escrow can be ADOPTED by an installation
924
+ * that already has folders (FILEX_INSTALLATION_E2E_ESCROW_ADOPT), and
925
+ * adoption is not retroactive: the folder's master key was wrapped to its
926
+ * recovery paths when the folder was created. Before this distinction
927
+ * existed the dialog simply showed no Escrow tab, which is true but says
928
+ * nothing — an admin who knows escrow is on reads a missing tab as a bug,
929
+ * tries the key anyway, and learns the real answer from a failure. The UI
930
+ * has to say it instead.
931
+ *
932
+ * ⚠⚠ 'predates' means "not as things stand", NOT "never". The folder's
933
+ * owner can add a slot from inside with the password (`addEscrowSlot`,
934
+ * offered at unlock). Any wording built on this state has to leave that
935
+ * door visible, or it tells an operator their escrow key can never reach a
936
+ * folder whose owner could hand it over this afternoon.
937
+ *
938
+ * ⚠ 'other-key' was a quieter lie: the dialog labelled the escrow field
939
+ * with the INSTALLATION's key id whatever the folder's slot said, so a
940
+ * folder restored from another install looked openable by the key the
941
+ * operator has, and was not.
942
+ */
943
+ export declare type EscrowAvailability = 'off' | 'available' | 'predates' | 'other-key';
944
+
945
+ export declare function escrowAvailability(m: E2eMarker | null, installationKid: string | null | undefined): EscrowAvailability;
946
+
947
+ /**
948
+ * Stable short name for an escrow key: first 8 bytes of SHA-256(SPKI), hex.
949
+ * Written into every escrow slot so a marker says WHICH key opens it, and so
950
+ * the UI can tell "this server's escrow key" from "some other one".
951
+ */
952
+ export declare function escrowKeyId(spkiB64: string): Promise<string>;
953
+
954
+ /**
955
+ * Whether this folder's owner should be offered an escrow slot, and whether
956
+ * they have already answered.
957
+ *
958
+ * 'n/a' nothing to offer: the installation has no escrow key, the
959
+ * folder already has a slot, or the marker is v1 (whose path
960
+ * is `upgradeMarkerV1`).
961
+ * 'offer' the offer applies and no answer has been recorded.
962
+ * 'declined' the offer applies and the owner said no. Do not ask again;
963
+ * leave a way back.
964
+ *
965
+ * ⚠ This deliberately does NOT look at whether the folder is unlocked. That
966
+ * is the caller's business, and it matters: the offer may only be shown
967
+ * after an unlock actually succeeded, because accepting needs the password
968
+ * and because asking someone who cannot open the folder to give away a key
969
+ * to it is asking the wrong person.
970
+ */
971
+ export declare type EscrowOfferState = 'n/a' | 'offer' | 'declined';
972
+
973
+ export declare function escrowOfferState(m: E2eMarker | null, installationKid: string | null | undefined): EscrowOfferState;
974
+
649
975
  export declare interface ExplorerConfig {
650
976
  /**
651
977
  * Modern shorthand: URL prefix for the standard /api/files/* layout.
@@ -707,6 +1033,10 @@ export declare interface ExplorerConfig {
707
1033
  trashList?: string;
708
1034
  /** filex trash restore endpoint — `POST { node_id }`. */
709
1035
  trashRestore?: string;
1036
+ /** E2E escrow proof-of-possession — `POST { path } → { id, challenge }`. */
1037
+ e2eEscrowChallenge?: string;
1038
+ /** E2E escrow use report — `POST { path, id, nonce }`. */
1039
+ e2eEscrowUsed?: string;
710
1040
  /** Public share base URL — `${shareBase}/${uuid}` */
711
1041
  shareBase?: string;
712
1042
  /** Auth strategy (see AuthConfig). */
@@ -800,6 +1130,22 @@ export declare interface ExplorerConfig {
800
1130
  * navigation panel starts expanded, the tab strip
801
1131
  * and split pane are off, the gallery view mode and
802
1132
  * the host's "How to connect" surface are hidden.
1133
+ * 'drive' — everything `simple` does, plus the shell an end
1134
+ * user already knows: one primary "New" menu, one
1135
+ * search field in the header (with its ⌘K/Ctrl+K
1136
+ * escalation into the command palette), a filter row
1137
+ * under the breadcrumb, Folders and Files as
1138
+ * labelled sections in grid view, the details panel
1139
+ * split into Details / Activity, and a storage line
1140
+ * under the navigation.
1141
+ *
1142
+ * ⚠ `drive` is a SUPERSET of `simple`, not a sibling: everything `simple`
1143
+ * turns off stays off, and the code asks `simpleUi` for those questions so a
1144
+ * later change to `simple` cannot silently miss `drive`. It is a third value
1145
+ * rather than a second boolean because "which chrome" is ONE question with
1146
+ * three answers — a `driveShell: true` next to `uiProfile: 'standard'` would
1147
+ * be a combination nobody can describe, and keeping that question single is
1148
+ * why `uiProfile` was a preset to begin with.
803
1149
  *
804
1150
  * Why it exists (GitHub #14): the reporter's users are not in IT and read
805
1151
  * split panes, tabs and mount instructions as a file manager they would have
@@ -811,7 +1157,7 @@ export declare interface ExplorerConfig {
811
1157
  * rest of the chrome differ. A viewer's own collapse choice, once made,
812
1158
  * outranks the profile — it is a per-viewer preference, not a policy.
813
1159
  */
814
- uiProfile?: 'standard' | 'simple';
1160
+ uiProfile?: 'standard' | 'simple' | 'drive';
815
1161
  /**
816
1162
  * Render the navigation panel (Upload · Recent / Starred / Shared with me /
817
1163
  * Trash · the storage list). Collapsible to an icon rail by the viewer, whose
@@ -848,11 +1194,37 @@ export declare interface ExplorerConfig {
848
1194
  * the bug this became: for a year the sole place to mint the token the FTPS
849
1195
  * guide told you to use was the admin panel.
850
1196
  *
1197
+ * ⚠ Role is not the same question as `callerKind`. "API keys" IS hidden for
1198
+ * an app token — not because of what that caller may do, but because there is
1199
+ * no single person behind it whose keys they would be. See `callerKind`.
1200
+ *
851
1201
  * ⚠ The entries live in the panel, so `sideNav: false` takes them with it.
852
1202
  * Hosts that want the surface without the panel mount `<filex-connections>`
853
1203
  * (or `ConnectionsPanel`) on a page of their own.
854
1204
  */
855
1205
  connections?: boolean;
1206
+ /**
1207
+ * Who is behind this explorer — a person, or an integration?
1208
+ *
1209
+ * `'user'` (a signed-in human, or their own API token) draws everything.
1210
+ * `'app'` suppresses the surfaces that only mean something for ONE person:
1211
+ * **API keys**, **Recent**, **Starred** and **Shared with me**. Upload, the
1212
+ * storage list, Trash and "How to connect" stay — an embed's users still
1213
+ * upload files and still need mount instructions.
1214
+ *
1215
+ * Why it exists: a filex API token authenticates AS its owner, and the embeds
1216
+ * we run authenticate every visitor with ONE shared token injected by the
1217
+ * host's proxy. v0.30.0 put "API keys" in the panel, so under that token an
1218
+ * embed visitor could list and revoke the credential the embed itself runs
1219
+ * on — and "your Recent" meant the token owner's history shown to a stranger.
1220
+ *
1221
+ * Default: read from `GET /api/files/capabilities` (`caller_kind`), which is
1222
+ * authoritative because only the server knows the token's kind (migration
1223
+ * 00030). Set this only to answer BEFORE that request lands — a host that
1224
+ * already knows it proxies with an app token spares its users the flash of a
1225
+ * Starred row that then disappears. A value here wins over the server's.
1226
+ */
1227
+ callerKind?: 'user' | 'app';
856
1228
  /** Default view. */
857
1229
  viewMode?: 'list' | 'grid';
858
1230
  /** Override max upload size (MB) — falls back to /limits otherwise. */
@@ -1110,6 +1482,8 @@ inspectorOpen?: boolean;
1110
1482
  navOpen?: boolean;
1111
1483
  navEnabled?: boolean;
1112
1484
  viewModes?: ViewMode[];
1485
+ shell?: "classic" | "drive";
1486
+ scopeLabel?: string;
1113
1487
  }> & Readonly<{
1114
1488
  onAction?: ((key: string) => any) | undefined;
1115
1489
  onUpload?: (() => any) | undefined;
@@ -1124,6 +1498,7 @@ onRefresh?: (() => any) | undefined;
1124
1498
  "onToggle-nav"?: (() => any) | undefined;
1125
1499
  "onOpen-theme"?: (() => any) | undefined;
1126
1500
  "onOpen-shortcut-settings"?: (() => any) | undefined;
1501
+ "onOpen-palette"?: ((query: string) => any) | undefined;
1127
1502
  }>, {
1128
1503
  focusSearch: () => Promise<void>;
1129
1504
  }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
@@ -1140,6 +1515,7 @@ refresh: () => any;
1140
1515
  "toggle-nav": () => any;
1141
1516
  "open-theme": () => any;
1142
1517
  "open-shortcut-settings": () => any;
1518
+ "open-palette": (query: string) => any;
1143
1519
  }, PublicProps, {}, false, {}, {}, GlobalComponents, GlobalDirectives, string, {
1144
1520
  primaryEl: HTMLDivElement;
1145
1521
  wideMoreBtnEl: HTMLButtonElement;
@@ -1249,6 +1625,8 @@ inspectorOpen?: boolean;
1249
1625
  navOpen?: boolean;
1250
1626
  navEnabled?: boolean;
1251
1627
  viewModes?: ViewMode[];
1628
+ shell?: "classic" | "drive";
1629
+ scopeLabel?: string;
1252
1630
  }> & Readonly<{
1253
1631
  onAction?: ((key: string) => any) | undefined;
1254
1632
  onUpload?: (() => any) | undefined;
@@ -1263,6 +1641,7 @@ onRefresh?: (() => any) | undefined;
1263
1641
  "onToggle-nav"?: (() => any) | undefined;
1264
1642
  "onOpen-theme"?: (() => any) | undefined;
1265
1643
  "onOpen-shortcut-settings"?: (() => any) | undefined;
1644
+ "onOpen-palette"?: ((query: string) => any) | undefined;
1266
1645
  }>, {
1267
1646
  focusSearch: () => Promise<void>;
1268
1647
  }, {}, {}, {}, {}> | null;
@@ -1282,6 +1661,7 @@ viewMode?: ViewMode;
1282
1661
  thumbSrc?: (n: FileNode) => string | null;
1283
1662
  keepBadgeFor?: (n: FileNode) => "kept" | "syncing" | "cloud" | "partial" | null;
1284
1663
  trashVisible?: boolean;
1664
+ navOffersTrash?: boolean;
1285
1665
  }> & Readonly<{
1286
1666
  onTransfer?: ((p: {
1287
1667
  sources: string[];
@@ -1336,6 +1716,7 @@ viewMode?: ViewMode;
1336
1716
  thumbSrc?: (n: FileNode) => string | null;
1337
1717
  keepBadgeFor?: (n: FileNode) => "kept" | "syncing" | "cloud" | "partial" | null;
1338
1718
  trashVisible?: boolean;
1719
+ navOffersTrash?: boolean;
1339
1720
  }> & Readonly<{
1340
1721
  onTransfer?: ((p: {
1341
1722
  sources: string[];
@@ -1454,6 +1835,12 @@ export declare interface FileNode {
1454
1835
  */
1455
1836
  export declare function findShortcutConflict(combo: string, excludeId: string): ShortcutConflict | null;
1456
1837
 
1838
+ /**
1839
+ * Format 20 raw bytes as the string the user writes down:
1840
+ * `XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX` (160 bits, no padding waste).
1841
+ */
1842
+ export declare function formatRecoveryKey(raw: Uint8Array): string;
1843
+
1457
1844
  /** What an FTP client needs to be told, computed on the server. */
1458
1845
  declare interface FTPSFacts {
1459
1846
  enabled: boolean;
@@ -1470,6 +1857,9 @@ declare interface FTPSFacts {
1470
1857
  self_signed: boolean;
1471
1858
  }
1472
1859
 
1860
+ /** Mint a fresh user recovery key. Shown once, never stored by filex. */
1861
+ export declare function generateRecoveryKey(): string;
1862
+
1473
1863
  /**
1474
1864
  * Generate a stylesheet that mirrors styles/variables.css' selector
1475
1865
  * cascade 1:1 (light base → explicit-dark selectors → prefers-dark media
@@ -1627,11 +2017,17 @@ export declare interface GuideFact {
1627
2017
  export declare function guideProtocols(): string[];
1628
2018
 
1629
2019
  /** True when the buffer starts with the 'filexe2e' magic. */
1630
- export declare function hasMagic(buf: ArrayBuffer | Uint8Array): boolean;
2020
+ export declare function hasMagic(data: ArrayBuffer | Uint8Array): boolean;
1631
2021
 
1632
2022
  /** `https://fm.example.com` → `fm.example.com`; anything unparseable comes back whole. */
1633
2023
  export declare function hostOf(origin: string): string;
1634
2024
 
2025
+ /** Import the escrow private key the admin pastes in (base64 PKCS#8, PEM tolerated). */
2026
+ export declare function importEscrowPrivateKey(pkcs8B64: string): Promise<CryptoKey>;
2027
+
2028
+ /** Import the installation escrow public key (base64 SPKI, as the server serves it). */
2029
+ export declare function importEscrowPublicKey(spkiB64: string): Promise<CryptoKey>;
2030
+
1635
2031
  declare interface InviteResponse {
1636
2032
  mode: 'granted' | 'user_created' | 'shared';
1637
2033
  user_id?: number;
@@ -1685,6 +2081,12 @@ export declare interface ManagerResponse {
1685
2081
  files: FileNode[];
1686
2082
  }
1687
2083
 
2084
+ /** True when the folder has an operator escrow slot. */
2085
+ export declare function markerHasEscrow(m: E2eMarker | null): boolean;
2086
+
2087
+ /** True when the folder has a user recovery key slot. */
2088
+ export declare function markerHasRecovery(m: E2eMarker | null): boolean;
2089
+
1688
2090
  /** True when the hit matched (at least partly) inside file CONTENT. */
1689
2091
  export declare function matchedInContent(matched: unknown): boolean;
1690
2092
 
@@ -1737,7 +2139,7 @@ export declare interface NFSExportCreated extends NFSConnection {
1737
2139
  path: string;
1738
2140
  }
1739
2141
 
1740
- export declare const NFSExportsPanel: DefineComponent<__VLS_Props_18, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2142
+ export declare const NFSExportsPanel: DefineComponent<__VLS_Props_20, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
1741
2143
  active: (v: {
1742
2144
  host: string;
1743
2145
  port: number;
@@ -1745,7 +2147,7 @@ enabled: boolean;
1745
2147
  path?: string;
1746
2148
  readOnly: boolean;
1747
2149
  }) => any;
1748
- }, string, PublicProps, Readonly<__VLS_Props_18> & Readonly<{
2150
+ }, string, PublicProps, Readonly<__VLS_Props_20> & Readonly<{
1749
2151
  onActive?: ((v: {
1750
2152
  host: string;
1751
2153
  port: number;
@@ -1865,9 +2267,16 @@ export declare type OperationsStore = ReturnType<typeof useOperations>;
1865
2267
 
1866
2268
  export declare type OperationStatus = 'running' | 'done' | 'error' | 'aborted';
1867
2269
 
1868
- /** Parse marker JSON text; returns null when the shape is not a v1 marker. */
2270
+ /** Parse marker JSON text; returns null when the shape is not a marker we read. */
1869
2271
  export declare function parseMarker(text: string): E2eMarker | null;
1870
2272
 
2273
+ /**
2274
+ * Parse a typed-in recovery key back to its 20 bytes, or null when it is not
2275
+ * one. Forgiving about how a human retypes it: case, dashes, spaces and the
2276
+ * Crockford look-alikes (O to 0, I/L to 1) are all normalised away.
2277
+ */
2278
+ export declare function parseRecoveryKey(s: string): Uint8Array | null;
2279
+
1871
2280
  export declare interface PendingOp {
1872
2281
  id: number;
1873
2282
  op_type: 'copy' | 'move' | 'delete';
@@ -1971,6 +2380,14 @@ onNav?: ((delta: number) => any) | undefined;
1971
2380
  "onOpen-full"?: (() => any) | undefined;
1972
2381
  }>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
1973
2382
 
2383
+ /** surucu:d1 — `GET /api/files/quota/me` (quota.Snapshot). */
2384
+ declare interface QuotaSnapshot {
2385
+ used_bytes: number;
2386
+ quota_bytes: number;
2387
+ percent_used: number;
2388
+ unlimited: boolean;
2389
+ }
2390
+
1974
2391
  export declare const RecentlyOpened: DefineComponent<__VLS_Props_5, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
1975
2392
  error: (message: string) => any;
1976
2393
  open: (node: RecentNode) => any;
@@ -1988,6 +2405,12 @@ declare interface RecentNode {
1988
2405
  last_opened?: string;
1989
2406
  }
1990
2407
 
2408
+ export declare const RecoveryKeyModal: DefineComponent<__VLS_Props_13, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2409
+ close: () => any;
2410
+ }, string, PublicProps, Readonly<__VLS_Props_13> & Readonly<{
2411
+ onClose?: (() => any) | undefined;
2412
+ }>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
2413
+
1991
2414
  declare function reload(): Promise<void>;
1992
2415
 
1993
2416
  export declare function resetAllShortcuts(): void;
@@ -2096,14 +2519,14 @@ export declare interface S3KeyRequest {
2096
2519
  expires_at?: string;
2097
2520
  }
2098
2521
 
2099
- export declare const S3KeysPanel: DefineComponent<__VLS_Props_16, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2522
+ export declare const S3KeysPanel: DefineComponent<__VLS_Props_18, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2100
2523
  active: (v: {
2101
2524
  accessKeyID: string;
2102
2525
  secret?: string;
2103
2526
  endpoint: string;
2104
2527
  pathStyle: boolean;
2105
2528
  }) => any;
2106
- }, string, PublicProps, Readonly<__VLS_Props_16> & Readonly<{
2529
+ }, string, PublicProps, Readonly<__VLS_Props_18> & Readonly<{
2107
2530
  onActive?: ((v: {
2108
2531
  accessKeyID: string;
2109
2532
  secret?: string;
@@ -2220,6 +2643,7 @@ export declare interface ShortcutHandlers {
2220
2643
  onShowHelp?: () => void;
2221
2644
  onToggleInspector?: () => void;
2222
2645
  onToggleHidden?: () => void;
2646
+ onStar?: () => void;
2223
2647
  onQuickLook?: () => void;
2224
2648
  onTabNew?: () => void;
2225
2649
  onTabClose?: () => void;
@@ -2286,7 +2710,7 @@ export declare interface SSHConnection {
2286
2710
  ftps?: FTPSFacts;
2287
2711
  }
2288
2712
 
2289
- export declare const SSHKeysPanel: DefineComponent<__VLS_Props_17, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2713
+ export declare const SSHKeysPanel: DefineComponent<__VLS_Props_19, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2290
2714
  active: (v: {
2291
2715
  host: string;
2292
2716
  port: number;
@@ -2295,7 +2719,7 @@ enabled: boolean;
2295
2719
  hasKey: boolean;
2296
2720
  ftps?: FTPSFacts;
2297
2721
  }) => any;
2298
- }, string, PublicProps, Readonly<__VLS_Props_17> & Readonly<{
2722
+ }, string, PublicProps, Readonly<__VLS_Props_19> & Readonly<{
2299
2723
  onActive?: ((v: {
2300
2724
  host: string;
2301
2725
  port: number;
@@ -2391,9 +2815,9 @@ export declare interface StorageFieldOption {
2391
2815
  i18n_key?: string;
2392
2816
  }
2393
2817
 
2394
- export declare const StorageFields: DefineComponent<__VLS_Props_14, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2818
+ export declare const StorageFields: DefineComponent<__VLS_Props_16, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2395
2819
  "update:modelValue": (v: Record<string, unknown>) => any;
2396
- }, string, PublicProps, Readonly<__VLS_Props_14> & Readonly<{
2820
+ }, string, PublicProps, Readonly<__VLS_Props_16> & Readonly<{
2397
2821
  "onUpdate:modelValue"?: ((v: Record<string, unknown>) => any) | undefined;
2398
2822
  }>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, HTMLDivElement>;
2399
2823
 
@@ -2550,11 +2974,11 @@ export declare const THEMES: ThemeDef[];
2550
2974
  /** Map of `--fe-*` custom property → value. */
2551
2975
  export declare type ThemeTokenMap = Record<string, string>;
2552
2976
 
2553
- export declare const TokensPanel: DefineComponent<__VLS_Props_19, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2977
+ export declare const TokensPanel: DefineComponent<__VLS_Props_21, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
2554
2978
  active: (v: {
2555
2979
  hasToken: boolean;
2556
2980
  }) => any;
2557
- }, string, PublicProps, Readonly<__VLS_Props_19> & Readonly<{
2981
+ }, string, PublicProps, Readonly<__VLS_Props_21> & Readonly<{
2558
2982
  onActive?: ((v: {
2559
2983
  hasToken: boolean;
2560
2984
  }) => any) | undefined;
@@ -2600,6 +3024,44 @@ declare interface TrashEntry {
2600
3024
  ttl_days?: number | null;
2601
3025
  }
2602
3026
 
3027
+ /**
3028
+ * Unlock with the installation escrow private key.
3029
+ *
3030
+ * Returns null when the folder has no escrow slot — which is the case for
3031
+ * every folder created while escrow was off, and is why escrow cannot be
3032
+ * turned on retroactively.
3033
+ */
3034
+ export declare function unlockWithEscrowKey(marker: E2eMarker, privateKey: CryptoKey): Promise<CryptoKey | null>;
3035
+
3036
+ /**
3037
+ * Unlock with the folder password. Returns the FMK (the key `decryptFile`
3038
+ * wants) or null when the password is wrong.
3039
+ */
3040
+ export declare function unlockWithPassword(marker: E2eMarker, password: string): Promise<CryptoKey | null>;
3041
+
3042
+ /**
3043
+ * Unlock with the user recovery key shown when the folder was created.
3044
+ * Returns null for a malformed key, a wrong key, or a folder that has no
3045
+ * recovery slot at all — the caller cannot tell those apart, and neither can
3046
+ * an attacker.
3047
+ */
3048
+ export declare function unlockWithRecoveryKey(marker: E2eMarker, recoveryKey: string): Promise<CryptoKey | null>;
3049
+
3050
+ /**
3051
+ * Give an existing v1 folder recovery keys, in place and without rewriting a
3052
+ * single file.
3053
+ *
3054
+ * The v1 files are wrapped under the password KEK, so the FMK stays defined
3055
+ * as "the KEK" (`fmk: 'kek'`) and the new slots wrap those raw bytes. The
3056
+ * password path afterwards is byte-identical to what it was.
3057
+ *
3058
+ * ⚠ Requires the password — this is only callable at the one moment filex
3059
+ * ever has it. There is no way to give a v1 folder recovery without it.
3060
+ * ⚠ When the installation has escrow on, this ALSO hands the operator a key
3061
+ * to a folder that did not have one. The caller must say so before asking.
3062
+ */
3063
+ export declare function upgradeMarkerV1(marker: E2eMarker, password: string, opts?: CreateFolderOptions): Promise<CreatedFolder>;
3064
+
2603
3065
  export declare interface UploadFinalizeResponse {
2604
3066
  s3Key: string;
2605
3067
  url?: string;
@@ -2721,6 +3183,7 @@ export declare function useFileApi(config: ExplorerConfig): {
2721
3183
  limit?: number;
2722
3184
  scope?: GlobalSearchScope;
2723
3185
  }) => Promise<GlobalSearchHit[]>;
3186
+ quotaMe: () => Promise<QuotaSnapshot | null>;
2724
3187
  subfolders: (path: string) => Promise<{
2725
3188
  folders: FileNode[];
2726
3189
  }>;
@@ -2751,7 +3214,9 @@ export declare function useFileApi(config: ExplorerConfig): {
2751
3214
  uploadMultipart: (path: string, files: File[], onProgress?: (p: number) => void) => Promise<ManagerResponse>;
2752
3215
  downloadUrl: (path: string) => string;
2753
3216
  previewUrl: (path: string) => string;
2754
- fetchBlob: (path: string) => Promise<{
3217
+ fetchBlob: (path: string, opts?: {
3218
+ fresh?: boolean;
3219
+ }) => Promise<{
2755
3220
  url: string;
2756
3221
  blob: Blob;
2757
3222
  mime: string;
@@ -2759,6 +3224,19 @@ export declare function useFileApi(config: ExplorerConfig): {
2759
3224
  fetchArrayBuffer: (path: string) => Promise<ArrayBuffer>;
2760
3225
  limits: () => Promise<UploadLimits>;
2761
3226
  capabilities: () => Promise<Capabilities>;
3227
+ e2eEscrowChallenge: (path: string) => Promise<{
3228
+ id: string;
3229
+ challenge: string;
3230
+ kid: string;
3231
+ }>;
3232
+ e2eEscrowUsed: (payload: {
3233
+ path: string;
3234
+ id: string;
3235
+ nonce: string;
3236
+ }) => Promise<{
3237
+ ok: boolean;
3238
+ notified: boolean;
3239
+ }>;
2762
3240
  createShare: (payload: {
2763
3241
  path: string;
2764
3242
  password?: boolean;
@@ -3271,6 +3749,10 @@ export declare function useUploadChunked(config: ExplorerConfig, api: FileApi, s
3271
3749
  * Check `password` against a folder marker. Resolves to the derived KEK on
3272
3750
  * success, or `null` on a wrong password (GCM tag mismatch on the verify
3273
3751
  * blob). Never talks to any server.
3752
+ *
3753
+ * ⚠ This returns the KEK, not the FMK. On a v1 folder they are the same key;
3754
+ * on a v2 `fmk: 'wrapped'` folder they are not. Use `unlockWithPassword` to
3755
+ * get the key that actually decrypts files.
3274
3756
  */
3275
3757
  export declare function verifyPassword(marker: E2eMarker, password: string): Promise<CryptoKey | null>;
3276
3758