@@ -8,29 +8,16 @@ import { useAuthState } from "react-firebase-hooks/auth"
88import { backendFetch } from "@/lib/backend-auth"
99import { broadcastApiClientUpdate , useApiClientSyncListener } from "@/lib/api-client-sync"
1010import { isDesktop } from "@/lib/desktop/is-desktop"
11- import { diffFiles , newManifest , parseCollection , serializeCollection , MANIFEST_FILE } from "@/lib/api-client/file-store"
11+ import { countLiteralAuthSecrets , diffFiles , newManifest , parseCollection , serializeCollection , MANIFEST_FILE } from "@/lib/api-client/file-store"
12+ import { moveCollectionSecretsToVault } from "@/lib/api-client/move-to-vault"
13+ import { useCipherKey } from "@/lib/use-cipher-key"
1214
1315const STORAGE_KEY = "api-client-collections"
14- /** Registered folder-collection paths (desktop only — paths are machine-local). */
15- const FILE_REGISTRY_KEY = "api-client-file-collections"
1616
1717function sortCollections ( cols : Collection [ ] ) {
1818 return [ ...cols ] . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
1919}
2020
21- function readFileRegistry ( ) : string [ ] {
22- try {
23- const parsed = JSON . parse ( localStorage . getItem ( FILE_REGISTRY_KEY ) ?? "[]" )
24- return Array . isArray ( parsed ) ? parsed . filter ( ( p ) : p is string => typeof p === "string" ) : [ ]
25- } catch {
26- return [ ]
27- }
28- }
29-
30- function writeFileRegistry ( paths : string [ ] ) {
31- localStorage . setItem ( FILE_REGISTRY_KEY , JSON . stringify ( paths ) )
32- }
33-
3421/** Carry UI-only open/closed state (stripped from disk) across reloads, by folder id. */
3522function applyOpenState ( prev : Collection | undefined , next : Collection ) : Collection {
3623 if ( ! prev ) return next
@@ -61,6 +48,12 @@ export function useCollections() {
6148 const migrationRanRef = React . useRef ( false )
6249 /** Paths already toasted about, so a failing folder doesn't re-toast on every window focus. */
6350 const failedFilePathsRef = React . useRef ( new Set < string > ( ) )
51+ /** Collections already warned about literal credentials — once per session is enough. */
52+ const secretWarnedRef = React . useRef ( new Set < string > ( ) )
53+ const cipherKey = useCipherKey ( )
54+ // Latest file collections for toast-action callbacks (they outlive the closing render).
55+ const fileCollectionsRef = React . useRef ( fileCollections )
56+ fileCollectionsRef . current = fileCollections
6457
6558 // DB collections + folder-backed collections, one list for consumers.
6659 const allCollections = React . useMemo (
@@ -71,12 +64,14 @@ export function useCollections() {
7164 /** (Re-)read every registered folder collection from disk. Desktop only. */
7265 const reloadFileCollections = React . useCallback ( async ( ) => {
7366 if ( ! isDesktop ( ) ) return
74- const paths = readFileRegistry ( )
67+ const { readCollectionFiles, allowCollectionDir, loadCollectionRegistry } = await import (
68+ "@/lib/desktop/collection-files"
69+ )
70+ const paths = await loadCollectionRegistry ( )
7571 if ( paths . length === 0 ) {
7672 setFileCollections ( ( cur ) => ( cur . length === 0 ? cur : [ ] ) )
7773 return
7874 }
79- const { readCollectionFiles, allowCollectionDir } = await import ( "@/lib/desktop/collection-files" )
8075 const loaded : Collection [ ] = [ ]
8176 for ( const path of paths ) {
8277 try {
@@ -121,6 +116,22 @@ export function useCollections() {
121116 ) : Promise < boolean > => {
122117 const next = { ...target , ...patch , items : nextItems }
123118 setFileCollections ( ( cur ) => cur . map ( ( c ) => ( c . id === target . id ? next : c ) ) )
119+ // Literal credentials are redacted by the serializer (never written to git-able
120+ // files) — tell the user once so a "missing" token isn't a mystery, and offer
121+ // to move them into the encrypted vault (rewrites fields to {{vault.*}} tokens).
122+ if ( ! secretWarnedRef . current . has ( target . id ) && countLiteralAuthSecrets ( next ) > 0 ) {
123+ secretWarnedRef . current . add ( target . id )
124+ toast . warning (
125+ "Credentials typed directly are not saved into collection files — keep them in the encrypted vault as {{vault.NAME}} tokens" ,
126+ {
127+ duration : 10000 ,
128+ action : {
129+ label : "Move to vault" ,
130+ onClick : ( ) => void moveFileCollectionSecrets ( target . id ) ,
131+ } ,
132+ }
133+ )
134+ }
124135 try {
125136 const { writes, deletes } = diffFiles ( serializeCollection ( target ) , serializeCollection ( next ) )
126137 if ( writes . length > 0 || deletes . length > 0 ) {
@@ -136,6 +147,26 @@ export function useCollections() {
136147 }
137148 }
138149
150+ /** Toast action: move every literal credential in a folder collection into the vault. */
151+ const moveFileCollectionSecrets = async ( collectionId : string ) => {
152+ const target = fileCollectionsRef . current . find ( ( c ) => c . id === collectionId )
153+ if ( ! target ) return
154+ try {
155+ const { collection : moved , moved : count } = await moveCollectionSecretsToVault ( target , cipherKey )
156+ if ( count === 0 ) return
157+ if ( await mutateFileCollection ( target , moved . items ) ) {
158+ toast . success (
159+ count === 1
160+ ? "1 credential moved to the vault"
161+ : `${ count } credentials moved to the vault`
162+ )
163+ }
164+ } catch ( e ) {
165+ console . error ( "Error moving credentials to vault" , e )
166+ toast . error ( e instanceof Error ? e . message : "Failed to move credentials to the vault" )
167+ }
168+ }
169+
139170 React . useEffect ( ( ) => {
140171 if ( ! user ) migrationRanRef . current = false
141172 } , [ user ] )
@@ -325,7 +356,8 @@ export function useCollections() {
325356 // Deleting a folder collection = forget it, never touch the user's files.
326357 const fileCol = fileCollections . find ( ( c ) => c . id === itemId )
327358 if ( fileCol ?. source ) {
328- writeFileRegistry ( readFileRegistry ( ) . filter ( ( p ) => p !== fileCol . source ! . path ) )
359+ const { loadCollectionRegistry, saveCollectionRegistry } = await import ( "@/lib/desktop/collection-files" )
360+ await saveCollectionRegistry ( ( await loadCollectionRegistry ( ) ) . filter ( ( p ) => p !== fileCol . source ! . path ) )
329361 setFileCollections ( ( cur ) => cur . filter ( ( c ) => c . id !== itemId ) )
330362 toast . success ( "Folder collection removed from list (files kept)" )
331363 return
@@ -705,7 +737,8 @@ export function useCollections() {
705737 const removedPaths = new Set (
706738 fileCollections . filter ( ( c ) => fileIds . has ( c . id ) ) . map ( ( c ) => c . source ! . path )
707739 )
708- writeFileRegistry ( readFileRegistry ( ) . filter ( ( p ) => ! removedPaths . has ( p ) ) )
740+ const { loadCollectionRegistry, saveCollectionRegistry } = await import ( "@/lib/desktop/collection-files" )
741+ await saveCollectionRegistry ( ( await loadCollectionRegistry ( ) ) . filter ( ( p ) => ! removedPaths . has ( p ) ) )
709742 setFileCollections ( ( cur ) => cur . filter ( ( c ) => ! fileIds . has ( c . id ) ) )
710743 ids = ids . filter ( ( id ) => ! fileIds . has ( id ) )
711744 if ( ids . length === 0 ) {
@@ -781,13 +814,12 @@ export function useCollections() {
781814 const openFolderCollection = async ( ) : Promise < Collection | null > => {
782815 if ( ! isDesktop ( ) ) return null
783816 try {
784- const { pickFolder, readCollectionFiles, writeCollectionFiles } = await import (
785- "@/lib/desktop/collection-files"
786- )
817+ const { pickFolder, readCollectionFiles, writeCollectionFiles, loadCollectionRegistry, saveCollectionRegistry } =
818+ await import ( "@/lib/desktop/collection-files" )
787819 const path = await pickFolder ( )
788820 if ( ! path ) return null
789821
790- const registry = readFileRegistry ( )
822+ const registry = await loadCollectionRegistry ( )
791823 if ( registry . includes ( path ) ) {
792824 toast . info ( "This folder collection is already open" )
793825 return fileCollections . find ( ( c ) => c . source ?. path === path ) ?? null
@@ -806,7 +838,7 @@ export function useCollections() {
806838 col = { id : manifest . id , name, items : [ ] , source : { kind : "file" , path } }
807839 }
808840
809- writeFileRegistry ( [ ...registry , path ] )
841+ await saveCollectionRegistry ( [ ...registry , path ] )
810842 setFileCollections ( ( cur ) => [ ...cur . filter ( ( c ) => c . id !== col . id ) , col ] )
811843 toast . success ( `Opened folder collection "${ col . name } "` )
812844 return col
0 commit comments