Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ await secrets.keys("openai.*"); // ["openai.apiKey"]
await secrets.delete("openai.apiKey");

// Clean up
secrets.close();
await secrets.close();
```

## Storage Location
Expand Down Expand Up @@ -101,7 +101,11 @@ List all key names, optionally filtered by glob pattern (e.g., `"openai.*"`).

### `secrets.close()`

Close the database connection. Instance cannot be reused.
Close the database connection and release resources. **This method is async and must be awaited.**

Returns a `Promise<void>` that resolves when the database is closed and integrity is finalized.

**Breaking Change (v2.0.0):** This method is now async. Update your code to `await secrets.close()`.

### `secrets.size`

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wgtechlabs/secrets-engine",
"version": "1.0.2",
"version": "2.0.0",
"description": "Bun-first TypeScript SDK for securely storing and managing secrets with zero-friction, machine-bound AES-256-GCM encryption.",
"type": "module",
"main": "dist/index.js",
Expand Down
65 changes: 45 additions & 20 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,23 +101,33 @@ export class SecretsEngine {
// 5. Open SQLite database
const store = SecretStore.open(dirPath);

// 6. Verify integrity (skip for brand-new stores)
if (!isNewStore) {
await verifyIntegrity(masterKey, store.filePath, dirPath);
}
try {
// 6. Verify integrity (skip for brand-new stores)
if (!isNewStore) {
await verifyIntegrity(masterKey, store.filePath, dirPath, () => store.checkpoint());
}

// 7. Build the instance
const engine = new SecretsEngine(masterKey, store, dirPath, salt);
// 7. Build the instance
const engine = new SecretsEngine(masterKey, store, dirPath, salt);

// 8. Build in-memory key index
engine.buildKeyIndex();
// 8. Build in-memory key index
engine.buildKeyIndex();

// 9. Write initial integrity HMAC for new stores
if (isNewStore) {
await updateIntegrity(masterKey, store.filePath, dirPath, salt);
}
// 9. Write initial integrity HMAC for new stores
if (isNewStore) {
await updateIntegrity(masterKey, store.filePath, dirPath, salt, () => store.checkpoint());
}

return engine;
return engine;
} catch (error) {
// Cleanup: close the store if initialization fails
try {
store.close();
} catch {
// Intentionally ignore errors during close to preserve original error
}
throw error;
}
}

// -----------------------------------------------------------------------
Expand Down Expand Up @@ -183,8 +193,10 @@ export class SecretsEngine {
// Update in-memory key index
this.keyIndex.set(keyHash, key);

// Update integrity HMAC
await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt);
// Update integrity HMAC, checkpointing first to keep store.db and meta.json in sync
await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt, () =>
this.store.checkpoint(),
);
}

/**
Expand All @@ -209,7 +221,10 @@ export class SecretsEngine {

if (deleted) {
this.keyIndex.delete(keyHash);
await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt);
// Update integrity HMAC, checkpointing first to keep store.db and meta.json in sync
await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt, () =>
this.store.checkpoint(),
);
}

return deleted;
Expand Down Expand Up @@ -257,13 +272,23 @@ export class SecretsEngine {

/**
* Close the database connection and release resources.
* Checkpoints the WAL and updates integrity HMAC before closing.
* The instance cannot be used after calling `close()`.
*/
close(): void {
async close(): Promise<void> {
if (!this.closed) {
this.store.close();
this.keyIndex.clear();
this.closed = true;
try {
// Checkpoint WAL to ensure all data is flushed to the main database file
this.store.checkpoint();

// Update integrity HMAC to reflect the final checkpointed state
await updateIntegrity(this.masterKey, this.store.filePath, this.dirPath, this.salt);
} finally {
// Always close the store and clear state, even if integrity update fails
this.store.close();
this.keyIndex.clear();
this.closed = true;
}
}
}

Expand Down
22 changes: 19 additions & 3 deletions src/integrity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ import { CONSTANTS } from "./types.ts";
* integrity_hmac = HMAC-SHA256(master_key, SHA256(store.db))
* ```
*/
export async function computeIntegrityHmac(masterKey: Buffer, dbFilePath: string): Promise<string> {
export async function computeIntegrityHmac(
masterKey: Buffer,
dbFilePath: string,
checkpointFn?: () => void,
): Promise<string> {
// Checkpoint WAL to ensure all data is flushed to the main database file
if (checkpointFn) {
try {
checkpointFn();
} catch (err) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new IntegrityError(`Integrity checkpoint failed: ${originalMessage}`);
}
}

const dbBytes = Buffer.from(await readFile(dbFilePath));
const dbHash = sha256(dbBytes);
return hmac(masterKey, dbHash);
Expand All @@ -33,6 +47,7 @@ export async function verifyIntegrity(
masterKey: Buffer,
dbFilePath: string,
dirPath: string,
checkpointFn?: () => void,
): Promise<StoreMeta> {
const metaRaw = await readMetaFile(dirPath);

Expand All @@ -53,7 +68,7 @@ export async function verifyIntegrity(
);
}

const computedHmac = await computeIntegrityHmac(masterKey, dbFilePath);
const computedHmac = await computeIntegrityHmac(masterKey, dbFilePath, checkpointFn);

if (computedHmac !== meta.integrity) {
throw new IntegrityError();
Expand All @@ -70,8 +85,9 @@ export async function updateIntegrity(
dbFilePath: string,
dirPath: string,
salt: string,
checkpointFn?: () => void,
): Promise<void> {
const integrity = await computeIntegrityHmac(masterKey, dbFilePath);
const integrity = await computeIntegrityHmac(masterKey, dbFilePath, checkpointFn);

const meta: StoreMeta = {
version: CONSTANTS.STORE_VERSION,
Expand Down
Loading