(store: string, key: string, value: string)
| 160 | |
| 161 | /** Write an entry to a store. Creates the store dir if needed. */ |
| 162 | export function setEntry(store: string, key: string, value: string): void { |
| 163 | validateStoreName(store) |
| 164 | validateKey(key) |
| 165 | |
| 166 | // D2: Guard against unbounded value sizes (1 MB limit). |
| 167 | // File-fallback vault is not designed for large data blobs. |
| 168 | const byteLength = Buffer.byteLength(value, 'utf8') |
| 169 | if (byteLength > MAX_VALUE_BYTES) { |
| 170 | throw new Error( |
| 171 | `Entry value too large: ${byteLength} bytes exceeds the 1 MB limit. ` + |
| 172 | 'Use external storage for large data.', |
| 173 | ) |
| 174 | } |
| 175 | |
| 176 | const storeDir = getStoreDir(store) |
| 177 | if (!existsSync(storeDir)) { |
| 178 | mkdirSync(storeDir, { recursive: true }) |
| 179 | } |
| 180 | const entryPath = getEntryPath(store, key) |
| 181 | |
| 182 | // C2: Atomic write — write to a .tmp file then rename. |
| 183 | // On POSIX, rename(2) is atomic; on Windows it is best-effort but safe. |
| 184 | // This prevents half-written files on crash mid-write. |
| 185 | const tmpPath = join(storeDir, `.${randomBytes(8).toString('hex')}.tmp`) |
| 186 | try { |
| 187 | writeFileSync(tmpPath, value, 'utf8') |
| 188 | renameSync(tmpPath, entryPath) |
| 189 | } catch (err) { |
| 190 | // Clean up tmp file on error |
| 191 | try { |
| 192 | rmSync(tmpPath, { force: true }) |
| 193 | } catch { |
| 194 | /* ignore cleanup error */ |
| 195 | } |
| 196 | throw err |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | /** Read an entry from a store. Returns null if not found. */ |
| 201 | export function getEntry(store: string, key: string): string | null { |
no test coverage detected