Conversation
Introduces a `folderVault` extension type so apps can transparently encrypt and decrypt the names and contents of folders flagged as vaults, plus the first implementation (`web-app-rclone-crypt`) that surfaces rclone-crypt compatible vaults under cleartext names. Wires vault-awareness into listing, AppWrapper, single-file and drag-drop upload, create-file, create-folder and delete. Adds a runtime-level unlock guard that mirrors the public-link flow, an in-memory passphrase store, empty-vault create-passphrase UX and a lock-vault context action. Covered by cucumber e2e scenarios that drive an rclone CLI fixture.
Replaces the hand-rolled collectStream loops and single-emit ReadableStream wrappings in every vault encrypt/decrypt call site with two small helpers (streamToArrayBuffer, streamToBlob) plus Blob.stream() for input. The engine's streaming contract stays unchanged; only the call sites get tidier and the future "swap engine for real block streaming" lands without touching them.
- useResourceIndicators: tolerate test mocks that omit requestExtensions - useFileActions: silently bail in openEditor when no route exists (instead of filtering the action out, which broke route-null tests) - useFileActionsCreateNewFile: null-safe appFileExtension access - useFileActionsDeleteResources: drop the sync/async zalgo path, always return Promises; matching tests now await - web-test-helpers defaultComponentMocks: hasRoute defaults to true so the overwhelming majority of suites get the expected behaviour - web-app-preview: only opt out of preview service on explicit hasPreview() === false, preserve legacy behaviour for missing method - web-app-rclone-crypt: drop unused imports
…c DragEvent
Headless Chrome silently swallows `new DragEvent(...).dispatchEvent(...)`
fired from inside `page.evaluate`, so the drag-drop upload scenario
worked headed and failed headless. Switching the helper to
`locator.dispatchEvent('drop', { dataTransfer })` routes the event
through CDP, where both modes behave the same way.
dataTransfer is built in the page via `evaluateHandle(new DataTransfer())`
so File objects and webkitRelativePath stay intact.
| if (vaultEngine) { | ||
| for (const child of children) { | ||
| await decryptResourceInPlace(vaultEngine, child) | ||
| } | ||
| } |
There was a problem hiding this comment.
Can this happen in parallel? And I guess it should be done one the filtered newResources array below.
There was a problem hiding this comment.
yeah, true! The api should handle a list, depending on the encryption implementation we can implement it much more efficiently that way
| router.push({ | ||
| name: 'files-spaces-generic', | ||
| params: { driveAliasAndItem }, | ||
| query: resource.fileId ? { fileId: resource.fileId as string } : undefined |
There was a problem hiding this comment.
| query: resource.fileId ? { fileId: resource.fileId as string } : undefined | |
| query: resource.fileId ? { fileId: resource.fileId } : undefined |
| // FIXME(poc-vault): this lookup currently lives next to the loaders and the | ||
| // AppWrapper. Once we lift vault-aware translation onto a higher layer (e.g. | ||
| // a webdav/client decorator or a folderService decorator), callers stop | ||
| // needing to resolve the engine themselves. | ||
| export function resolveFolderVault( |
There was a problem hiding this comment.
This only resolves unlocked vaults, right? Could you clarify this in the doc string?
| handler: ({ resources }: FileActionOptions) => { | ||
| const resource = resources?.[0] | ||
| if (!resource || !isVaultRoot(resource)) return | ||
| const spaceId = resource.storageId as string |
There was a problem hiding this comment.
| const spaceId = resource.storageId as string | |
| const spaceId = resource.storageId |
| isVisible: ({ resources }: FileActionOptions) => { | ||
| const resource = resources?.[0] | ||
| if (!resource || !isVaultRoot(resource)) return false | ||
| return vaultStore.isUnlocked(resource.storageId as string, resource.path) |
There was a problem hiding this comment.
| return vaultStore.isUnlocked(resource.storageId as string, resource.path) | |
| return vaultStore.isUnlocked(resource.storageId, resource.path) |
| if (!password) { | ||
| return null | ||
| } | ||
| return createEngine(space.id as string, vaultRoot, password) |
There was a problem hiding this comment.
| return createEngine(space.id as string, vaultRoot, password) | |
| return createEngine(space.id, vaultRoot, password) |
| return null | ||
| } | ||
| const vaultStore = useFolderVaultStore() | ||
| const password = vaultStore.getSecret<string>(space.id as string, vaultRoot) |
There was a problem hiding this comment.
| const password = vaultStore.getSecret<string>(space.id as string, vaultRoot) | |
| const password = vaultStore.getSecret<string>(space.id, vaultRoot) |
| for (const child of children) { | ||
| await decryptResourceInPlace(vaultEngine, child) | ||
| } |
| // The editor's view may already be torn down by tiptap's own | ||
| // onBeforeUnmount hook by the time we run, depending on hook | ||
| // ordering. In that case `serialize` blows up reading a null schema. | ||
| // Flush the pending update if it's still safe, otherwise drop it — | ||
| // throwing here would break the rest of Vue's teardown. | ||
| const inst = editor.value as unknown as { view?: { state?: unknown } } | null | ||
| if (options.onUpdate && editor.value && inst?.view?.state) { | ||
| try { | ||
| options.onUpdate(strategy.serialize(editor.value)) | ||
| } catch (e) { | ||
| console.warn('[useTextEditor] dropped pending update during destroy', e) | ||
| } |
There was a problem hiding this comment.
This has been fixed on current main meanwhile, I belive.
| for (const child of resources) { | ||
| yield* call(decryptResourceInPlace(vaultEngine, child)) | ||
| } |
|
List of to-dos:
|
|
Can we split this into a part with the changes to web and move the rest into a dedicated extension? |
We can, but I don't recommend it at this point. I would strongly suggest to keep it in web while it matures |
Summary
Adds a
folderVaultextension type and the first implementation(
web-app-rclone-crypt) that surfaces rclone-cryptcompatible vaults in the OpenCloud Web UI under cleartext names. End users see
a
*.vaultfolder, unlock it once per session via a passphrase, and read,edit, upload and delete content as if it were unencrypted, all crypto happens
in the browser.
Plugin surface
FolderVaultExtension, pluggable filename/path/content (de)cryption engineResourceIndicatorExtension, pluggable per resource status indicatorsCross cutting integrations
Vault awareness wired into listing (
loaderSpace),AppWrapper(load/save),single file upload, drag drop directory tree upload, create file,
create folder, delete, SSE postprocessing, route guard.
Test coverage
Cucumber scenarios under
tests/e2e/cucumber/features/rclone-crypt/drive anrclone CLI fixture to verify read, edit save, single file upload and drag
drop upload, all decrypt back to expected cleartext via the rclone CLI as a
ground truth oracle.
Notes
Known FIXMEs scattered:
decorator layer
foobaris gone but the unlock flow could grow more checksWith the touch points now known, the right helpers and abstractions (likely a
webdav decorator or folder service decorator layer that absorbs the path /
content translation) will be evaluated next.