If you post a snapshot to POST /api/v1/snapshots/import whose flows.credentials is a plain object rather than the {"$": "..."} shape an export produces, it's accepted and written to the snapshot row verbatim, unencrypted.
uploadSnapshot only re-encrypts when it sees the $:
if (importSnapshot.flows.credentials?.$) {
...exportCredentials(...)
}
anything else falls through to flows: { credentials: importSnapshot.flows.credentials || {} }.
Reproduced locally: imported {"n1": {"user": "alice", "password": "hunter2"}} with no credentialSecret and got a 200, then exported the new snapshot, decrypted it, and the values came back intact. So they were sitting in the database in the clear in between.
The route already guards the encrypted case ("Credential secret is required when importing a snapshot with credentials"), so the intent looks like encrypted-only. Might be worth rejecting a credentials object with no $, or encrypting it on the way in.
If you post a snapshot to
POST /api/v1/snapshots/importwhoseflows.credentialsis a plain object rather than the{"$": "..."}shape an export produces, it's accepted and written to the snapshot row verbatim, unencrypted.uploadSnapshotonly re-encrypts when it sees the$:anything else falls through to
flows: { credentials: importSnapshot.flows.credentials || {} }.Reproduced locally: imported
{"n1": {"user": "alice", "password": "hunter2"}}with nocredentialSecretand got a 200, then exported the new snapshot, decrypted it, and the values came back intact. So they were sitting in the database in the clear in between.The route already guards the encrypted case ("Credential secret is required when importing a snapshot with credentials"), so the intent looks like encrypted-only. Might be worth rejecting a credentials object with no
$, or encrypting it on the way in.